fa-mcp-sdk 0.4.3 → 0.4.6

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.
Files changed (153) hide show
  1. package/bin/fa-mcp.js +1040 -1039
  2. package/cli-template/eslint.config.js +16 -136
  3. package/cli-template/package.json +9 -10
  4. package/cli-template/tsconfig.json +1 -0
  5. package/dist/core/_types_/active-directory-config.d.ts.map +1 -1
  6. package/dist/core/_types_/config.d.ts +1 -1
  7. package/dist/core/_types_/config.d.ts.map +1 -1
  8. package/dist/core/_types_/types.d.ts.map +1 -1
  9. package/dist/core/ad/group-checker.d.ts.map +1 -1
  10. package/dist/core/ad/group-checker.js.map +1 -1
  11. package/dist/core/agent-tester/agent-tester-router.d.ts.map +1 -1
  12. package/dist/core/agent-tester/agent-tester-router.js +8 -8
  13. package/dist/core/agent-tester/agent-tester-router.js.map +1 -1
  14. package/dist/core/agent-tester/check-llm.d.ts.map +1 -1
  15. package/dist/core/agent-tester/check-llm.js +1 -1
  16. package/dist/core/agent-tester/check-llm.js.map +1 -1
  17. package/dist/core/agent-tester/services/TesterAgentService.d.ts.map +1 -1
  18. package/dist/core/agent-tester/services/TesterAgentService.js +53 -53
  19. package/dist/core/agent-tester/services/TesterAgentService.js.map +1 -1
  20. package/dist/core/agent-tester/services/TesterMcpClientService.d.ts.map +1 -1
  21. package/dist/core/agent-tester/services/TesterMcpClientService.js +2 -2
  22. package/dist/core/agent-tester/services/TesterMcpClientService.js.map +1 -1
  23. package/dist/core/auth/admin-auth.d.ts.map +1 -1
  24. package/dist/core/auth/admin-auth.js +3 -3
  25. package/dist/core/auth/admin-auth.js.map +1 -1
  26. package/dist/core/auth/basic.d.ts.map +1 -1
  27. package/dist/core/auth/basic.js.map +1 -1
  28. package/dist/core/auth/jwt.d.ts.map +1 -1
  29. package/dist/core/auth/jwt.js +6 -16
  30. package/dist/core/auth/jwt.js.map +1 -1
  31. package/dist/core/auth/middleware.d.ts.map +1 -1
  32. package/dist/core/auth/middleware.js +3 -2
  33. package/dist/core/auth/middleware.js.map +1 -1
  34. package/dist/core/auth/multi-auth.d.ts +0 -3
  35. package/dist/core/auth/multi-auth.d.ts.map +1 -1
  36. package/dist/core/auth/multi-auth.js +10 -7
  37. package/dist/core/auth/multi-auth.js.map +1 -1
  38. package/dist/core/auth/permanent.d.ts.map +1 -1
  39. package/dist/core/auth/permanent.js +1 -1
  40. package/dist/core/auth/permanent.js.map +1 -1
  41. package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.d.ts.map +1 -1
  42. package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.js +2 -2
  43. package/dist/core/auth/token-generator/ntlm/ntlm-auth-options.js.map +1 -1
  44. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.d.ts.map +1 -1
  45. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js +1 -1
  46. package/dist/core/auth/token-generator/ntlm/ntlm-domain-config.js.map +1 -1
  47. package/dist/core/auth/token-generator/ntlm/ntlm-integration.d.ts.map +1 -1
  48. package/dist/core/auth/token-generator/ntlm/ntlm-integration.js +1 -1
  49. package/dist/core/auth/token-generator/ntlm/ntlm-integration.js.map +1 -1
  50. package/dist/core/auth/token-generator/ntlm/ntlm-templates.d.ts.map +1 -1
  51. package/dist/core/auth/token-generator/ntlm/ntlm-templates.js +222 -221
  52. package/dist/core/auth/token-generator/ntlm/ntlm-templates.js.map +1 -1
  53. package/dist/core/auth/token-generator/server.d.ts.map +1 -1
  54. package/dist/core/auth/token-generator/server.js +8 -8
  55. package/dist/core/auth/token-generator/server.js.map +1 -1
  56. package/dist/core/bootstrap/init-config.d.ts.map +1 -1
  57. package/dist/core/bootstrap/init-config.js +4 -4
  58. package/dist/core/bootstrap/init-config.js.map +1 -1
  59. package/dist/core/bootstrap/startup-info.d.ts.map +1 -1
  60. package/dist/core/bootstrap/startup-info.js +4 -4
  61. package/dist/core/bootstrap/startup-info.js.map +1 -1
  62. package/dist/core/cache/cache.d.ts.map +1 -1
  63. package/dist/core/cache/cache.js +3 -3
  64. package/dist/core/cache/cache.js.map +1 -1
  65. package/dist/core/consul/access-points-updater.d.ts.map +1 -1
  66. package/dist/core/consul/access-points-updater.js +3 -3
  67. package/dist/core/consul/access-points-updater.js.map +1 -1
  68. package/dist/core/consul/deregister.d.ts.map +1 -1
  69. package/dist/core/consul/deregister.js +1 -1
  70. package/dist/core/consul/deregister.js.map +1 -1
  71. package/dist/core/consul/get-consul-api.d.ts.map +1 -1
  72. package/dist/core/consul/get-consul-api.js +3 -3
  73. package/dist/core/consul/get-consul-api.js.map +1 -1
  74. package/dist/core/db/pg-db.d.ts +1 -1
  75. package/dist/core/db/pg-db.d.ts.map +1 -1
  76. package/dist/core/db/pg-db.js +2 -2
  77. package/dist/core/db/pg-db.js.map +1 -1
  78. package/dist/core/debug.js +1 -1
  79. package/dist/core/debug.js.map +1 -1
  80. package/dist/core/init-mcp-server.d.ts.map +1 -1
  81. package/dist/core/init-mcp-server.js +9 -9
  82. package/dist/core/init-mcp-server.js.map +1 -1
  83. package/dist/core/logger.d.ts.map +1 -1
  84. package/dist/core/logger.js +3 -3
  85. package/dist/core/logger.js.map +1 -1
  86. package/dist/core/mcp/create-mcp-server.d.ts.map +1 -1
  87. package/dist/core/mcp/create-mcp-server.js +1 -1
  88. package/dist/core/mcp/create-mcp-server.js.map +1 -1
  89. package/dist/core/mcp/prompts.d.ts.map +1 -1
  90. package/dist/core/mcp/prompts.js +1 -3
  91. package/dist/core/mcp/prompts.js.map +1 -1
  92. package/dist/core/mcp/resources.d.ts.map +1 -1
  93. package/dist/core/mcp/resources.js +8 -10
  94. package/dist/core/mcp/resources.js.map +1 -1
  95. package/dist/core/mcp/server-stdio.d.ts.map +1 -1
  96. package/dist/core/mcp/server-stdio.js.map +1 -1
  97. package/dist/core/utils/formatToolResult.d.ts.map +1 -1
  98. package/dist/core/utils/formatToolResult.js +1 -3
  99. package/dist/core/utils/formatToolResult.js.map +1 -1
  100. package/dist/core/utils/port-checker.d.ts.map +1 -1
  101. package/dist/core/utils/port-checker.js +1 -1
  102. package/dist/core/utils/port-checker.js.map +1 -1
  103. package/dist/core/utils/rate-limit.js +2 -2
  104. package/dist/core/utils/testing/McpSseClient.d.ts.map +1 -1
  105. package/dist/core/utils/testing/McpSseClient.js.map +1 -1
  106. package/dist/core/utils/testing/McpStdioClient.d.ts.map +1 -1
  107. package/dist/core/utils/testing/McpStdioClient.js.map +1 -1
  108. package/dist/core/utils/utils.d.ts.map +1 -1
  109. package/dist/core/utils/utils.js.map +1 -1
  110. package/dist/core/web/admin-router.d.ts.map +1 -1
  111. package/dist/core/web/admin-router.js +4 -4
  112. package/dist/core/web/admin-router.js.map +1 -1
  113. package/dist/core/web/cors.d.ts.map +1 -1
  114. package/dist/core/web/cors.js.map +1 -1
  115. package/dist/core/web/favicon-svg.d.ts.map +1 -1
  116. package/dist/core/web/favicon-svg.js.map +1 -1
  117. package/dist/core/web/home-api.d.ts.map +1 -1
  118. package/dist/core/web/home-api.js +4 -4
  119. package/dist/core/web/home-api.js.map +1 -1
  120. package/dist/core/web/openapi.d.ts.map +1 -1
  121. package/dist/core/web/openapi.js.map +1 -1
  122. package/dist/core/web/server-http.d.ts.map +1 -1
  123. package/dist/core/web/server-http.js +20 -22
  124. package/dist/core/web/server-http.js.map +1 -1
  125. package/dist/core/web/static/agent-tester/script.js +1503 -1513
  126. package/dist/core/web/static/home/script.js +646 -646
  127. package/dist/core/web/static/token-gen/script.js +561 -561
  128. package/dist/core/web/svg-icons.d.ts.map +1 -1
  129. package/dist/core/web/svg-icons.js +1 -1
  130. package/dist/core/web/svg-icons.js.map +1 -1
  131. package/package.json +2 -6
  132. package/scripts/copy-static.js +31 -31
  133. package/scripts/kill-port.js +107 -107
  134. package/scripts/npm/patch_node_modules.js +8 -8
  135. package/scripts/npm/run.js +31 -31
  136. package/scripts/remove-nul.js +53 -53
  137. package/scripts/update-doc.js +18 -18
  138. package/src/template/_types_/custom-config.ts +83 -83
  139. package/src/template/api/router.ts +86 -89
  140. package/src/template/custom-resources.ts +11 -11
  141. package/src/template/prompts/agent-brief.ts +8 -8
  142. package/src/template/prompts/agent-prompt.ts +10 -10
  143. package/src/template/prompts/custom-prompts.ts +12 -12
  144. package/src/template/start.ts +71 -72
  145. package/src/template/tools/handle-tool-call.ts +57 -56
  146. package/src/template/tools/tools.ts +89 -88
  147. package/src/tests/jest-simple-reporter.js +10 -10
  148. package/src/tests/mcp/sse/test-sse-npm-package.js +96 -96
  149. package/src/tests/mcp/test-cases.js +143 -143
  150. package/src/tests/mcp/test-http.js +76 -75
  151. package/src/tests/mcp/test-sse.js +80 -79
  152. package/src/tests/mcp/test-stdio.js +83 -81
  153. package/src/tests/utils.ts +157 -156
@@ -1,561 +1,561 @@
1
- let keyValuePairCount = 0;
2
-
3
- // ===========================
4
- // Token Authentication Module
5
- // ===========================
6
-
7
- const AUTH_TOKEN_KEY = 'adminAuthToken';
8
- let requiresBearerToken = false;
9
-
10
- // Get stored auth token from sessionStorage
11
- function getStoredToken () {
12
- return sessionStorage.getItem(AUTH_TOKEN_KEY);
13
- }
14
-
15
- // Store auth token in sessionStorage
16
- function storeToken (token) {
17
- sessionStorage.setItem(AUTH_TOKEN_KEY, token);
18
- }
19
-
20
- // Clear stored auth token
21
- function clearStoredToken () {
22
- sessionStorage.removeItem(AUTH_TOKEN_KEY);
23
- }
24
-
25
- // Show token authentication modal
26
- function showTokenModal (errorMessage = null) {
27
- const modal = document.getElementById('tokenModal');
28
- const errorDiv = document.getElementById('tokenAuthError');
29
-
30
- if (errorMessage) {
31
- errorDiv.innerHTML = `<strong>Error:</strong> ${errorMessage}`;
32
- errorDiv.style.display = 'block';
33
- } else {
34
- errorDiv.style.display = 'none';
35
- }
36
-
37
- modal.style.display = 'flex';
38
- }
39
-
40
- // Hide token authentication modal
41
- function hideTokenModal () {
42
- const modal = document.getElementById('tokenModal');
43
- modal.style.display = 'none';
44
- }
45
-
46
- // Authenticated fetch wrapper - adds Authorization header if token auth is required
47
- async function authFetch (url, options = {}) {
48
- const token = getStoredToken();
49
-
50
- if (requiresBearerToken && token) {
51
- options.headers = {
52
- ...options.headers,
53
- 'Authorization': `Bearer ${token}`,
54
- };
55
- }
56
-
57
- const response = await fetch(url, options);
58
-
59
- // Handle 401 Unauthorized - show token modal if available
60
- if (response.status === 401 && requiresBearerToken) {
61
- clearStoredToken();
62
- const errorData = await response.json().catch(() => ({}));
63
- const errorMessage = errorData.error || 'Authentication failed';
64
-
65
- // Try to show modal, but if it's not available, throw with descriptive error
66
- const modal = document.getElementById('tokenModal');
67
- if (modal) {
68
- showTokenModal(errorMessage + '. Please enter a valid token.');
69
- }
70
-
71
- // Throw error with status code for form error handling
72
- const error = new Error(`401 Unauthorized: ${errorMessage}`);
73
- error.status = 401;
74
- throw error;
75
- }
76
-
77
- return response;
78
- }
79
-
80
- // Check auth config and initialize authentication if needed
81
- async function initializeAuth () {
82
- try {
83
- // Get auth config from public endpoint (no auth required)
84
- const response = await fetch('/admin/api/auth-config');
85
- const config = await response.json();
86
-
87
- if (config.success && config.requiresBearerToken) {
88
- requiresBearerToken = true;
89
-
90
- // Check if we have a stored token
91
- const storedToken = getStoredToken();
92
- if (!storedToken) {
93
- showTokenModal();
94
- return false;
95
- }
96
-
97
- // Verify token is still valid by making an authenticated request
98
- try {
99
- const verifyResponse = await authFetch('/admin/api/auth-status');
100
- const verifyData = await verifyResponse.json();
101
-
102
- if (!verifyData.success || !verifyData.isAuthenticated) {
103
- clearStoredToken();
104
- showTokenModal('Token is invalid or expired.');
105
- return false;
106
- }
107
- } catch {
108
- // authFetch already handles 401 and shows modal
109
- return false;
110
- }
111
- }
112
-
113
- return true;
114
- } catch (error) {
115
- console.error('Error checking auth config:', error);
116
- return true; // Continue anyway if config check fails
117
- }
118
- }
119
-
120
- // Handle token authentication form submission
121
- function setupTokenAuthForm () {
122
- const form = document.getElementById('tokenAuthForm');
123
- if (!form) {return;}
124
-
125
- form.addEventListener('submit', async (e) => {
126
- e.preventDefault();
127
-
128
- const tokenInput = document.getElementById('authTokenInput');
129
- const token = tokenInput.value.trim();
130
-
131
- if (!token) {
132
- showTokenModal('Please enter a token.');
133
- return;
134
- }
135
-
136
- // Store token and try to authenticate
137
- storeToken(token);
138
-
139
- try {
140
- const response = await authFetch('/admin/api/auth-status');
141
- const data = await response.json();
142
-
143
- if (data.success && data.isAuthenticated) {
144
- hideTokenModal();
145
- tokenInput.value = '';
146
- // Reload auth status and initialize form
147
- loadAuthStatus();
148
- initializeForm();
149
- } else {
150
- clearStoredToken();
151
- showTokenModal(data.error || 'Invalid token.');
152
- }
153
- } catch (error) {
154
- // Error already handled in authFetch
155
- if (error.message !== 'Unauthorized') {
156
- clearStoredToken();
157
- showTokenModal('Authentication failed: ' + error.message);
158
- }
159
- }
160
- });
161
- }
162
-
163
- // Set primary color CSS variable
164
- function setPrimaryColor (color) {
165
- if (color) {
166
- document.documentElement.style.setProperty('--primary-color', color);
167
- }
168
- }
169
-
170
- function switchTab (tabName) {
171
- document.querySelectorAll('.tab-content').forEach(content => {
172
- content.classList.remove('active');
173
- });
174
- document.querySelectorAll('.tab').forEach(tab => {
175
- tab.classList.remove('active');
176
- });
177
- document.getElementById(tabName).classList.add('active');
178
-
179
- // Activate the corresponding tab button
180
- const tabs = document.querySelectorAll('.tab');
181
- tabs.forEach(tab => {
182
- const onclick = tab.getAttribute('onclick');
183
- if (onclick && onclick.includes('switchTab(\'' + tabName + '\')')) {
184
- tab.classList.add('active');
185
- }
186
- });
187
- }
188
-
189
- function addKeyValuePair (key = '', value = '', readonly = false, placeholder = 'Value') {
190
- if (keyValuePairCount >= 15) {
191
- alert('Maximum of 15 key-value pairs');
192
- return;
193
- }
194
- const container = document.getElementById('keyValuePairs');
195
- const pairDiv = document.createElement('div');
196
- pairDiv.className = 'key-value-pair';
197
-
198
- const keyInput = readonly ?
199
- '<input type="text" placeholder="Key" name="keys" value="' + key + '" readonly style="background-color: #f8f9fa;">' :
200
- '<input type="text" placeholder="Key" name="keys" value="' + key + '">';
201
-
202
- const valueInput = '<input type="text" placeholder="' + placeholder + '" name="values" value="' + value + '">';
203
-
204
- pairDiv.innerHTML = keyInput + valueInput +
205
- '<button type="button" class="remove-btn" onclick="removeKeyValuePair(this)">×</button>';
206
- container.appendChild(pairDiv);
207
- keyValuePairCount++;
208
- }
209
-
210
- // eslint-disable-next-line unused-imports/no-unused-vars
211
- function removeKeyValuePair (button) {
212
- button.parentElement.remove();
213
- keyValuePairCount--;
214
- }
215
-
216
- function addCopyButtonToTokenOutput (tokenOutput, token) {
217
- if (!tokenOutput || tokenOutput.hasAttribute('data-copy-added')) {
218
- return;
219
- }
220
-
221
- tokenOutput.setAttribute('data-copy-added', 'true');
222
-
223
- const copyButton = document.createElement('button');
224
- copyButton.className = 'copy-button';
225
- copyButton.innerHTML = '📋';
226
- copyButton.title = 'Copy to clipboard';
227
- copyButton.setAttribute('aria-label', 'Copy to clipboard');
228
-
229
- const validateButton = document.createElement('button');
230
- validateButton.className = 'validate-token-button';
231
- validateButton.innerHTML = '✓';
232
- validateButton.title = 'Validate token';
233
- validateButton.setAttribute('aria-label', 'Validate token');
234
-
235
- const notification = document.createElement('div');
236
- notification.className = 'copy-notification';
237
- notification.textContent = 'Copied';
238
-
239
- tokenOutput.appendChild(copyButton);
240
- tokenOutput.appendChild(validateButton);
241
- tokenOutput.appendChild(notification);
242
-
243
- copyButton.addEventListener('click', async function () {
244
- try {
245
- await navigator.clipboard.writeText(token);
246
-
247
- // Show notification
248
- notification.classList.add('show');
249
-
250
- // Hide notification after 1 second
251
- setTimeout(() => {
252
- notification.classList.remove('show');
253
- }, 1000);
254
-
255
- } catch {
256
- // Fallback for browsers that don't support clipboard API
257
- const textArea = document.createElement('textarea');
258
- textArea.value = token;
259
- textArea.style.position = 'fixed';
260
- textArea.style.opacity = '0';
261
- document.body.appendChild(textArea);
262
- textArea.focus();
263
- textArea.select();
264
-
265
- try {
266
- document.execCommand('copy');
267
-
268
- // Show notification
269
- notification.classList.add('show');
270
-
271
- // Hide notification after 1 second
272
- setTimeout(() => {
273
- notification.classList.remove('show');
274
- }, 1000);
275
- } catch (fallbackErr) {
276
- console.error('Failed to copy text:', fallbackErr);
277
- }
278
-
279
- document.body.removeChild(textArea);
280
- }
281
- });
282
-
283
- validateButton.addEventListener('click', function () {
284
- // Switch to validation tab
285
- switchTab('validate');
286
-
287
- // Set the token in the validation textarea
288
- const tokenTextarea = document.getElementById('tokenInput');
289
- if (tokenTextarea) {
290
- tokenTextarea.value = token;
291
-
292
- // Trigger validation form submit
293
- const validateForm = document.getElementById('validateForm');
294
- if (validateForm) {
295
- validateForm.dispatchEvent(new Event('submit'));
296
- }
297
- }
298
- });
299
- }
300
-
301
- function formatTime (ms) {
302
- const seconds = Math.floor(ms / 1000);
303
- const minutes = Math.floor(seconds / 60);
304
- const hours = Math.floor(minutes / 60);
305
- const days = Math.floor(hours / 24);
306
-
307
- if (days > 0) {return days + ' d. ' + (hours % 24) + ' h.';}
308
- if (hours > 0) {return hours + ' h. ' + (minutes % 60) + ' min.';}
309
- if (minutes > 0) {return minutes + ' min.';}
310
- return seconds + ' s.';
311
- }
312
-
313
- // Render authentication status in header
314
- function renderAuthStatus (data) {
315
- const container = document.getElementById('authStatusContainer');
316
- if (!container) {return;}
317
-
318
- const { authType, isAuthenticated, user, canLogout } = data;
319
-
320
- // Don't show anything if no auth is configured or not authenticated
321
- if (!authType || !isAuthenticated || !user) {
322
- container.style.display = 'none';
323
- return;
324
- }
325
-
326
- let html = '<div class="header-auth-info">';
327
- html += '<img src="/svg/token-gen/user.svg" alt="" class="user-icon">';
328
- html += '<span class="username">' + user + '</span>';
329
- html += '</div>';
330
-
331
- // Show logout button only for basic and ntlm auth types
332
- if (canLogout) {
333
- html += '<button class="header-logout-btn" onclick="logout()" title="Log out">';
334
- html += '<img src="/svg/token-gen/logout.svg" alt="Log out">';
335
- html += '</button>';
336
- }
337
-
338
- container.innerHTML = html;
339
- container.style.display = 'flex';
340
- }
341
-
342
- // Load authentication status from API
343
- async function loadAuthStatus () {
344
- try {
345
- const response = await authFetch('/admin/api/auth-status');
346
- const data = await response.json();
347
- if (data.success) {
348
- renderAuthStatus(data);
349
- }
350
- } catch (error) {
351
- console.error('Error loading auth status:', error);
352
- }
353
- }
354
-
355
- // Processing the Generation Form
356
- document.getElementById('generateForm').addEventListener('submit', async (e) => {
357
- e.preventDefault();
358
-
359
- const formData = new FormData(e.target);
360
- const keys = formData.getAll('keys').filter(k => k.trim());
361
- const values = formData.getAll('values').filter(v => v.trim());
362
-
363
- const payload = {};
364
- for (let i = 0; i < keys.length; i++) {
365
- if (keys[i] && values[i]) {
366
- payload[keys[i]] = values[i];
367
- }
368
- }
369
-
370
- const ipValue = document.getElementById('tokenIp').value.trim();
371
- if (ipValue) {
372
- payload.ip = ipValue;
373
- }
374
-
375
- const requestData = {
376
- user: formData.get('user'),
377
- timeValue: parseInt(formData.get('timeValue')),
378
- timeUnit: formData.get('timeUnit'),
379
- payload: payload,
380
- };
381
-
382
- try {
383
- const response = await authFetch('/admin/api/generate-token', {
384
- method: 'POST',
385
- headers: { 'Content-Type': 'application/json' },
386
- body: JSON.stringify(requestData),
387
- });
388
-
389
- const result = await response.json();
390
- const resultDiv = document.getElementById('generateResult');
391
-
392
- if (result.success) {
393
- resultDiv.innerHTML =
394
- `<div class="result success">
395
- <strong>The token has been successfully created!</strong><br>
396
- <div class="token-output">${result.token}</div>
397
- </div>`;
398
-
399
- // Add floating copy button to the token output
400
- const tokenOutput = resultDiv.querySelector('.token-output');
401
- if (tokenOutput) {
402
- addCopyButtonToTokenOutput(tokenOutput, result.token);
403
- }
404
-
405
- // Automatically populate the validation field with the generated token
406
- const tokenTextarea = document.getElementById('tokenInput');
407
- if (tokenTextarea) {
408
- tokenTextarea.value = result.token;
409
- }
410
- } else {
411
- resultDiv.innerHTML =
412
- `<div class="result error">
413
- <strong>Error:</strong> ${result.error}
414
- </div>`;
415
- }
416
- } catch (error) {
417
- document.getElementById('generateResult').innerHTML =
418
- `<div class="result error">
419
- <strong>Error:</strong> ${error.message}
420
- </div>`;
421
- }
422
- });
423
-
424
- // Processing the Verification Form
425
- document.getElementById('validateForm').addEventListener('submit', async (e) => {
426
- e.preventDefault();
427
-
428
- const formData = new FormData(e.target);
429
- const token = formData.get('token').trim();
430
-
431
- try {
432
- const response = await authFetch('/admin/api/validate-token', {
433
- method: 'POST',
434
- headers: { 'Content-Type': 'application/json' },
435
- body: JSON.stringify({ token }),
436
- });
437
-
438
- const result = await response.json();
439
- const resultDiv = document.getElementById('validateResult');
440
-
441
- if (result.success) {
442
- const remainingTime = result.payload.expire - Date.now();
443
- const payloadKeys = Object.keys(result.payload).filter((k) => !/^(user|expire|iat|service|ip)$/.test(k));
444
-
445
- let payloadHtml = '';
446
- if (payloadKeys.length > 0) {
447
- payloadHtml = '<h4>Additional data:</h4>';
448
- payloadKeys.forEach(key => {
449
- payloadHtml += '<p><strong>' + key + ':</strong> ' + result.payload[key] + '</p>';
450
- });
451
- }
452
-
453
- // Format issued at time
454
- const issuedAtTime = result.payload.iat ? new Date(result.payload.iat).toLocaleString('ru-RU') : 'N/A';
455
-
456
- resultDiv.innerHTML =
457
- `<div class="result success">
458
- <strong>The token is valid!</strong>
459
- <div class="token-info">
460
- <h4>Token Information:</h4>
461
- <p><strong>User:</strong> ${result.payload.user}</p>
462
- ${result.payload.service ? `<p><strong>Service:</strong> ${result.payload.service}</p>` : ''}
463
- ${result.payload.ip ? `<p><strong>Allowed IPs:</strong> ${result.payload.ip}</p>` : ''}
464
- <p><strong>Issued at:</strong> ${issuedAtTime}</p>
465
- <p><strong>Time remaining:</strong> ${formatTime(remainingTime)}</p>
466
- <p><strong>Expires:</strong> ${new Date(result.payload.expire).toLocaleString('ru-RU')}</p>
467
- ${payloadHtml}
468
- </div>
469
- </div>`;
470
- } else {
471
- resultDiv.innerHTML =
472
- `<div class="result error">
473
- <strong>Token invalid!</strong><br>
474
- Reason: ${result.error}
475
- </div>`;
476
- }
477
- } catch (error) {
478
- document.getElementById('validateResult').innerHTML =
479
- `<div class="result error">
480
- <strong>Error:</strong> ${error.message}
481
- </div>`;
482
- }
483
- });
484
-
485
- // Function to initialize the form
486
- async function initializeForm () {
487
- try {
488
- // Getting information about the service
489
- const response = await authFetch('/admin/api/service-info');
490
- const data = await response.json();
491
- const serviceName = data.serviceName;
492
-
493
- // Set theme color
494
- setPrimaryColor(data.primaryColor);
495
-
496
- // Clear existing key-value pairs before re-initializing
497
- const container = document.getElementById('keyValuePairs');
498
- container.innerHTML = '';
499
- keyValuePairCount = 0;
500
-
501
- // Adding a pre-filled pair serviceName
502
- addKeyValuePair('service', serviceName, true);
503
- addKeyValuePair('issue', '', true, 'URL of request for the issuance of a token in JIRA');
504
-
505
- } catch (error) {
506
- console.error('Error loading service info:', error);
507
- return;
508
- }
509
- // Add one empty pair for the user
510
- addKeyValuePair();
511
- }
512
-
513
- // Logout function
514
- // eslint-disable-next-line unused-imports/no-unused-vars
515
- async function logout () {
516
- try {
517
- // For token-based auth, just clear the stored token
518
- if (requiresBearerToken) {
519
- clearStoredToken();
520
- showTokenModal();
521
- // Clear auth status display
522
- const container = document.getElementById('authStatusContainer');
523
- if (container) {
524
- container.style.display = 'none';
525
- }
526
- return;
527
- }
528
-
529
- // For other auth types (NTLM, Basic), make logout request
530
- const response = await fetch('/admin/logout', {
531
- method: 'GET',
532
- credentials: 'include',
533
- });
534
-
535
- if (response.status === 401) {
536
- // Authentication cleared, reload page to trigger browser auth prompt
537
- window.location.reload();
538
- } else {
539
- console.error('Logout failed');
540
- alert('Logout failed. Please clear your browser cache and reload the page.');
541
- }
542
- } catch (error) {
543
- console.error('Error during logout:', error);
544
- alert('Error during logout. Please clear your browser cache and reload the page.');
545
- }
546
- }
547
-
548
- // Initialization on page load
549
- document.addEventListener('DOMContentLoaded', async () => {
550
- // Setup token auth form handler
551
- setupTokenAuthForm();
552
-
553
- // Initialize authentication (check if token is needed and valid)
554
- const authOk = await initializeAuth();
555
-
556
- if (authOk) {
557
- // Load auth status and form only if authenticated
558
- loadAuthStatus();
559
- initializeForm();
560
- }
561
- });
1
+ let keyValuePairCount = 0;
2
+
3
+ // ===========================
4
+ // Token Authentication Module
5
+ // ===========================
6
+
7
+ const AUTH_TOKEN_KEY = 'adminAuthToken';
8
+ let requiresBearerToken = false;
9
+
10
+ // Get stored auth token from sessionStorage
11
+ function getStoredToken () {
12
+ return sessionStorage.getItem(AUTH_TOKEN_KEY);
13
+ }
14
+
15
+ // Store auth token in sessionStorage
16
+ function storeToken (token) {
17
+ sessionStorage.setItem(AUTH_TOKEN_KEY, token);
18
+ }
19
+
20
+ // Clear stored auth token
21
+ function clearStoredToken () {
22
+ sessionStorage.removeItem(AUTH_TOKEN_KEY);
23
+ }
24
+
25
+ // Show token authentication modal
26
+ function showTokenModal (errorMessage = null) {
27
+ const modal = document.getElementById('tokenModal');
28
+ const errorDiv = document.getElementById('tokenAuthError');
29
+
30
+ if (errorMessage) {
31
+ errorDiv.innerHTML = `<strong>Error:</strong> ${errorMessage}`;
32
+ errorDiv.style.display = 'block';
33
+ } else {
34
+ errorDiv.style.display = 'none';
35
+ }
36
+
37
+ modal.style.display = 'flex';
38
+ }
39
+
40
+ // Hide token authentication modal
41
+ function hideTokenModal () {
42
+ const modal = document.getElementById('tokenModal');
43
+ modal.style.display = 'none';
44
+ }
45
+
46
+ // Authenticated fetch wrapper - adds Authorization header if token auth is required
47
+ async function authFetch (url, options = {}) {
48
+ const token = getStoredToken();
49
+
50
+ if (requiresBearerToken && token) {
51
+ options.headers = {
52
+ ...options.headers,
53
+ 'Authorization': `Bearer ${token}`,
54
+ };
55
+ }
56
+
57
+ const response = await fetch(url, options);
58
+
59
+ // Handle 401 Unauthorized - show token modal if available
60
+ if (response.status === 401 && requiresBearerToken) {
61
+ clearStoredToken();
62
+ const errorData = await response.json().catch(() => ({}));
63
+ const errorMessage = errorData.error || 'Authentication failed';
64
+
65
+ // Try to show modal, but if it's not available, throw with descriptive error
66
+ const modal = document.getElementById('tokenModal');
67
+ if (modal) {
68
+ showTokenModal(errorMessage + '. Please enter a valid token.');
69
+ }
70
+
71
+ // Throw error with status code for form error handling
72
+ const error = new Error(`401 Unauthorized: ${errorMessage}`);
73
+ error.status = 401;
74
+ throw error;
75
+ }
76
+
77
+ return response;
78
+ }
79
+
80
+ // Check auth config and initialize authentication if needed
81
+ async function initializeAuth () {
82
+ try {
83
+ // Get auth config from public endpoint (no auth required)
84
+ const response = await fetch('/admin/api/auth-config');
85
+ const config = await response.json();
86
+
87
+ if (config.success && config.requiresBearerToken) {
88
+ requiresBearerToken = true;
89
+
90
+ // Check if we have a stored token
91
+ const storedToken = getStoredToken();
92
+ if (!storedToken) {
93
+ showTokenModal();
94
+ return false;
95
+ }
96
+
97
+ // Verify token is still valid by making an authenticated request
98
+ try {
99
+ const verifyResponse = await authFetch('/admin/api/auth-status');
100
+ const verifyData = await verifyResponse.json();
101
+
102
+ if (!verifyData.success || !verifyData.isAuthenticated) {
103
+ clearStoredToken();
104
+ showTokenModal('Token is invalid or expired.');
105
+ return false;
106
+ }
107
+ } catch {
108
+ // authFetch already handles 401 and shows modal
109
+ return false;
110
+ }
111
+ }
112
+
113
+ return true;
114
+ } catch (error) {
115
+ console.error('Error checking auth config:', error);
116
+ return true; // Continue anyway if config check fails
117
+ }
118
+ }
119
+
120
+ // Handle token authentication form submission
121
+ function setupTokenAuthForm () {
122
+ const form = document.getElementById('tokenAuthForm');
123
+ if (!form) {return;}
124
+
125
+ form.addEventListener('submit', async (e) => {
126
+ e.preventDefault();
127
+
128
+ const tokenInput = document.getElementById('authTokenInput');
129
+ const token = tokenInput.value.trim();
130
+
131
+ if (!token) {
132
+ showTokenModal('Please enter a token.');
133
+ return;
134
+ }
135
+
136
+ // Store token and try to authenticate
137
+ storeToken(token);
138
+
139
+ try {
140
+ const response = await authFetch('/admin/api/auth-status');
141
+ const data = await response.json();
142
+
143
+ if (data.success && data.isAuthenticated) {
144
+ hideTokenModal();
145
+ tokenInput.value = '';
146
+ // Reload auth status and initialize form
147
+ loadAuthStatus();
148
+ initializeForm();
149
+ } else {
150
+ clearStoredToken();
151
+ showTokenModal(data.error || 'Invalid token.');
152
+ }
153
+ } catch (error) {
154
+ // Error already handled in authFetch
155
+ if (error.message !== 'Unauthorized') {
156
+ clearStoredToken();
157
+ showTokenModal('Authentication failed: ' + error.message);
158
+ }
159
+ }
160
+ });
161
+ }
162
+
163
+ // Set primary color CSS variable
164
+ function setPrimaryColor (color) {
165
+ if (color) {
166
+ document.documentElement.style.setProperty('--primary-color', color);
167
+ }
168
+ }
169
+
170
+ function switchTab (tabName) {
171
+ document.querySelectorAll('.tab-content').forEach(content => {
172
+ content.classList.remove('active');
173
+ });
174
+ document.querySelectorAll('.tab').forEach(tab => {
175
+ tab.classList.remove('active');
176
+ });
177
+ document.getElementById(tabName).classList.add('active');
178
+
179
+ // Activate the corresponding tab button
180
+ const tabs = document.querySelectorAll('.tab');
181
+ tabs.forEach(tab => {
182
+ const onclick = tab.getAttribute('onclick');
183
+ if (onclick && onclick.includes('switchTab(\'' + tabName + '\')')) {
184
+ tab.classList.add('active');
185
+ }
186
+ });
187
+ }
188
+
189
+ function addKeyValuePair (key = '', value = '', readonly = false, placeholder = 'Value') {
190
+ if (keyValuePairCount >= 15) {
191
+ alert('Maximum of 15 key-value pairs');
192
+ return;
193
+ }
194
+ const container = document.getElementById('keyValuePairs');
195
+ const pairDiv = document.createElement('div');
196
+ pairDiv.className = 'key-value-pair';
197
+
198
+ const keyInput = readonly ?
199
+ '<input type="text" placeholder="Key" name="keys" value="' + key + '" readonly style="background-color: #f8f9fa;">' :
200
+ '<input type="text" placeholder="Key" name="keys" value="' + key + '">';
201
+
202
+ const valueInput = '<input type="text" placeholder="' + placeholder + '" name="values" value="' + value + '">';
203
+
204
+ pairDiv.innerHTML = keyInput + valueInput +
205
+ '<button type="button" class="remove-btn" onclick="removeKeyValuePair(this)">×</button>';
206
+ container.appendChild(pairDiv);
207
+ keyValuePairCount++;
208
+ }
209
+
210
+ // eslint-disable-next-line unused-imports/no-unused-vars
211
+ function removeKeyValuePair (button) {
212
+ button.parentElement.remove();
213
+ keyValuePairCount--;
214
+ }
215
+
216
+ function addCopyButtonToTokenOutput (tokenOutput, token) {
217
+ if (!tokenOutput || tokenOutput.hasAttribute('data-copy-added')) {
218
+ return;
219
+ }
220
+
221
+ tokenOutput.setAttribute('data-copy-added', 'true');
222
+
223
+ const copyButton = document.createElement('button');
224
+ copyButton.className = 'copy-button';
225
+ copyButton.innerHTML = '📋';
226
+ copyButton.title = 'Copy to clipboard';
227
+ copyButton.setAttribute('aria-label', 'Copy to clipboard');
228
+
229
+ const validateButton = document.createElement('button');
230
+ validateButton.className = 'validate-token-button';
231
+ validateButton.innerHTML = '✓';
232
+ validateButton.title = 'Validate token';
233
+ validateButton.setAttribute('aria-label', 'Validate token');
234
+
235
+ const notification = document.createElement('div');
236
+ notification.className = 'copy-notification';
237
+ notification.textContent = 'Copied';
238
+
239
+ tokenOutput.appendChild(copyButton);
240
+ tokenOutput.appendChild(validateButton);
241
+ tokenOutput.appendChild(notification);
242
+
243
+ copyButton.addEventListener('click', async function () {
244
+ try {
245
+ await navigator.clipboard.writeText(token);
246
+
247
+ // Show notification
248
+ notification.classList.add('show');
249
+
250
+ // Hide notification after 1 second
251
+ setTimeout(() => {
252
+ notification.classList.remove('show');
253
+ }, 1000);
254
+
255
+ } catch {
256
+ // Fallback for browsers that don't support clipboard API
257
+ const textArea = document.createElement('textarea');
258
+ textArea.value = token;
259
+ textArea.style.position = 'fixed';
260
+ textArea.style.opacity = '0';
261
+ document.body.appendChild(textArea);
262
+ textArea.focus();
263
+ textArea.select();
264
+
265
+ try {
266
+ document.execCommand('copy');
267
+
268
+ // Show notification
269
+ notification.classList.add('show');
270
+
271
+ // Hide notification after 1 second
272
+ setTimeout(() => {
273
+ notification.classList.remove('show');
274
+ }, 1000);
275
+ } catch (fallbackErr) {
276
+ console.error('Failed to copy text:', fallbackErr);
277
+ }
278
+
279
+ document.body.removeChild(textArea);
280
+ }
281
+ });
282
+
283
+ validateButton.addEventListener('click', function () {
284
+ // Switch to validation tab
285
+ switchTab('validate');
286
+
287
+ // Set the token in the validation textarea
288
+ const tokenTextarea = document.getElementById('tokenInput');
289
+ if (tokenTextarea) {
290
+ tokenTextarea.value = token;
291
+
292
+ // Trigger validation form submit
293
+ const validateForm = document.getElementById('validateForm');
294
+ if (validateForm) {
295
+ validateForm.dispatchEvent(new Event('submit'));
296
+ }
297
+ }
298
+ });
299
+ }
300
+
301
+ function formatTime (ms) {
302
+ const seconds = Math.floor(ms / 1000);
303
+ const minutes = Math.floor(seconds / 60);
304
+ const hours = Math.floor(minutes / 60);
305
+ const days = Math.floor(hours / 24);
306
+
307
+ if (days > 0) {return days + ' d. ' + (hours % 24) + ' h.';}
308
+ if (hours > 0) {return hours + ' h. ' + (minutes % 60) + ' min.';}
309
+ if (minutes > 0) {return minutes + ' min.';}
310
+ return seconds + ' s.';
311
+ }
312
+
313
+ // Render authentication status in header
314
+ function renderAuthStatus (data) {
315
+ const container = document.getElementById('authStatusContainer');
316
+ if (!container) {return;}
317
+
318
+ const { authType, isAuthenticated, user, canLogout } = data;
319
+
320
+ // Don't show anything if no auth is configured or not authenticated
321
+ if (!authType || !isAuthenticated || !user) {
322
+ container.style.display = 'none';
323
+ return;
324
+ }
325
+
326
+ let html = '<div class="header-auth-info">';
327
+ html += '<img src="/svg/token-gen/user.svg" alt="" class="user-icon">';
328
+ html += '<span class="username">' + user + '</span>';
329
+ html += '</div>';
330
+
331
+ // Show logout button only for basic and ntlm auth types
332
+ if (canLogout) {
333
+ html += '<button class="header-logout-btn" onclick="logout()" title="Log out">';
334
+ html += '<img src="/svg/token-gen/logout.svg" alt="Log out">';
335
+ html += '</button>';
336
+ }
337
+
338
+ container.innerHTML = html;
339
+ container.style.display = 'flex';
340
+ }
341
+
342
+ // Load authentication status from API
343
+ async function loadAuthStatus () {
344
+ try {
345
+ const response = await authFetch('/admin/api/auth-status');
346
+ const data = await response.json();
347
+ if (data.success) {
348
+ renderAuthStatus(data);
349
+ }
350
+ } catch (error) {
351
+ console.error('Error loading auth status:', error);
352
+ }
353
+ }
354
+
355
+ // Processing the Generation Form
356
+ document.getElementById('generateForm').addEventListener('submit', async (e) => {
357
+ e.preventDefault();
358
+
359
+ const formData = new FormData(e.target);
360
+ const keys = formData.getAll('keys').filter(k => k.trim());
361
+ const values = formData.getAll('values').filter(v => v.trim());
362
+
363
+ const payload = {};
364
+ for (let i = 0; i < keys.length; i++) {
365
+ if (keys[i] && values[i]) {
366
+ payload[keys[i]] = values[i];
367
+ }
368
+ }
369
+
370
+ const ipValue = document.getElementById('tokenIp').value.trim();
371
+ if (ipValue) {
372
+ payload.ip = ipValue;
373
+ }
374
+
375
+ const requestData = {
376
+ user: formData.get('user'),
377
+ timeValue: parseInt(formData.get('timeValue')),
378
+ timeUnit: formData.get('timeUnit'),
379
+ payload: payload,
380
+ };
381
+
382
+ try {
383
+ const response = await authFetch('/admin/api/generate-token', {
384
+ method: 'POST',
385
+ headers: { 'Content-Type': 'application/json' },
386
+ body: JSON.stringify(requestData),
387
+ });
388
+
389
+ const result = await response.json();
390
+ const resultDiv = document.getElementById('generateResult');
391
+
392
+ if (result.success) {
393
+ resultDiv.innerHTML =
394
+ `<div class="result success">
395
+ <strong>The token has been successfully created!</strong><br>
396
+ <div class="token-output">${result.token}</div>
397
+ </div>`;
398
+
399
+ // Add floating copy button to the token output
400
+ const tokenOutput = resultDiv.querySelector('.token-output');
401
+ if (tokenOutput) {
402
+ addCopyButtonToTokenOutput(tokenOutput, result.token);
403
+ }
404
+
405
+ // Automatically populate the validation field with the generated token
406
+ const tokenTextarea = document.getElementById('tokenInput');
407
+ if (tokenTextarea) {
408
+ tokenTextarea.value = result.token;
409
+ }
410
+ } else {
411
+ resultDiv.innerHTML =
412
+ `<div class="result error">
413
+ <strong>Error:</strong> ${result.error}
414
+ </div>`;
415
+ }
416
+ } catch (error) {
417
+ document.getElementById('generateResult').innerHTML =
418
+ `<div class="result error">
419
+ <strong>Error:</strong> ${error.message}
420
+ </div>`;
421
+ }
422
+ });
423
+
424
+ // Processing the Verification Form
425
+ document.getElementById('validateForm').addEventListener('submit', async (e) => {
426
+ e.preventDefault();
427
+
428
+ const formData = new FormData(e.target);
429
+ const token = formData.get('token').trim();
430
+
431
+ try {
432
+ const response = await authFetch('/admin/api/validate-token', {
433
+ method: 'POST',
434
+ headers: { 'Content-Type': 'application/json' },
435
+ body: JSON.stringify({ token }),
436
+ });
437
+
438
+ const result = await response.json();
439
+ const resultDiv = document.getElementById('validateResult');
440
+
441
+ if (result.success) {
442
+ const remainingTime = result.payload.expire - Date.now();
443
+ const payloadKeys = Object.keys(result.payload).filter((k) => !/^(user|expire|iat|service|ip)$/.test(k));
444
+
445
+ let payloadHtml = '';
446
+ if (payloadKeys.length > 0) {
447
+ payloadHtml = '<h4>Additional data:</h4>';
448
+ payloadKeys.forEach(key => {
449
+ payloadHtml += '<p><strong>' + key + ':</strong> ' + result.payload[key] + '</p>';
450
+ });
451
+ }
452
+
453
+ // Format issued at time
454
+ const issuedAtTime = result.payload.iat ? new Date(result.payload.iat).toLocaleString('ru-RU') : 'N/A';
455
+
456
+ resultDiv.innerHTML =
457
+ `<div class="result success">
458
+ <strong>The token is valid!</strong>
459
+ <div class="token-info">
460
+ <h4>Token Information:</h4>
461
+ <p><strong>User:</strong> ${result.payload.user}</p>
462
+ ${result.payload.service ? `<p><strong>Service:</strong> ${result.payload.service}</p>` : ''}
463
+ ${result.payload.ip ? `<p><strong>Allowed IPs:</strong> ${result.payload.ip}</p>` : ''}
464
+ <p><strong>Issued at:</strong> ${issuedAtTime}</p>
465
+ <p><strong>Time remaining:</strong> ${formatTime(remainingTime)}</p>
466
+ <p><strong>Expires:</strong> ${new Date(result.payload.expire).toLocaleString('ru-RU')}</p>
467
+ ${payloadHtml}
468
+ </div>
469
+ </div>`;
470
+ } else {
471
+ resultDiv.innerHTML =
472
+ `<div class="result error">
473
+ <strong>Token invalid!</strong><br>
474
+ Reason: ${result.error}
475
+ </div>`;
476
+ }
477
+ } catch (error) {
478
+ document.getElementById('validateResult').innerHTML =
479
+ `<div class="result error">
480
+ <strong>Error:</strong> ${error.message}
481
+ </div>`;
482
+ }
483
+ });
484
+
485
+ // Function to initialize the form
486
+ async function initializeForm () {
487
+ try {
488
+ // Getting information about the service
489
+ const response = await authFetch('/admin/api/service-info');
490
+ const data = await response.json();
491
+ const { serviceName } = data;
492
+
493
+ // Set theme color
494
+ setPrimaryColor(data.primaryColor);
495
+
496
+ // Clear existing key-value pairs before re-initializing
497
+ const container = document.getElementById('keyValuePairs');
498
+ container.innerHTML = '';
499
+ keyValuePairCount = 0;
500
+
501
+ // Adding a pre-filled pair serviceName
502
+ addKeyValuePair('service', serviceName, true);
503
+ addKeyValuePair('issue', '', true, 'URL of request for the issuance of a token in JIRA');
504
+
505
+ } catch (error) {
506
+ console.error('Error loading service info:', error);
507
+ return;
508
+ }
509
+ // Add one empty pair for the user
510
+ addKeyValuePair();
511
+ }
512
+
513
+ // Logout function
514
+ // eslint-disable-next-line unused-imports/no-unused-vars
515
+ async function logout () {
516
+ try {
517
+ // For token-based auth, just clear the stored token
518
+ if (requiresBearerToken) {
519
+ clearStoredToken();
520
+ showTokenModal();
521
+ // Clear auth status display
522
+ const container = document.getElementById('authStatusContainer');
523
+ if (container) {
524
+ container.style.display = 'none';
525
+ }
526
+ return;
527
+ }
528
+
529
+ // For other auth types (NTLM, Basic), make logout request
530
+ const response = await fetch('/admin/logout', {
531
+ method: 'GET',
532
+ credentials: 'include',
533
+ });
534
+
535
+ if (response.status === 401) {
536
+ // Authentication cleared, reload page to trigger browser auth prompt
537
+ window.location.reload();
538
+ } else {
539
+ console.error('Logout failed');
540
+ alert('Logout failed. Please clear your browser cache and reload the page.');
541
+ }
542
+ } catch (error) {
543
+ console.error('Error during logout:', error);
544
+ alert('Error during logout. Please clear your browser cache and reload the page.');
545
+ }
546
+ }
547
+
548
+ // Initialization on page load
549
+ document.addEventListener('DOMContentLoaded', async () => {
550
+ // Setup token auth form handler
551
+ setupTokenAuthForm();
552
+
553
+ // Initialize authentication (check if token is needed and valid)
554
+ const authOk = await initializeAuth();
555
+
556
+ if (authOk) {
557
+ // Load auth status and form only if authenticated
558
+ loadAuthStatus();
559
+ initializeForm();
560
+ }
561
+ });