@egain/egain-mcp-server 1.0.1 → 1.0.4

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.
@@ -42,6 +42,1138 @@ const getConfigPath = () => {
42
42
  const execAsync = promisify(exec);
43
43
  const CONFIG_SERVER_PORT = 3333;
44
44
  const CONFIG_SERVER_HOST = 'localhost';
45
+ // Embedded HTML/JS content for auth pages (included in bundle for npm package compatibility)
46
+ const CONFIG_PAGE_HTML = `<!DOCTYPE html>
47
+ <html lang="en">
48
+ <head>
49
+ <meta charset="UTF-8">
50
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
51
+ <title>eGain MCP - Sign In</title>
52
+ <style>
53
+ * { margin: 0; padding: 0; box-sizing: border-box; }
54
+ body {
55
+ font-family: "Open Sans", "Segoe UI", "SegoeUI", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
56
+ background: #fef1fd;
57
+ min-height: 100vh;
58
+ display: flex;
59
+ justify-content: center;
60
+ align-items: center;
61
+ padding: 20px;
62
+ }
63
+ .container {
64
+ background: white;
65
+ border-radius: 16px;
66
+ box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, 0.12);
67
+ max-width: 500px;
68
+ width: 100%;
69
+ padding: 32px;
70
+ }
71
+ h1 { color: #333; margin-bottom: 8px; font-size: 24px; }
72
+ .subtitle { color: #666; margin-bottom: 20px; font-size: 13px; }
73
+ .quick-signin {
74
+ display: none;
75
+ text-align: center;
76
+ position: relative;
77
+ }
78
+ .quick-signin .icon { font-size: 64px; }
79
+ .quick-signin h1 { text-align: center; }
80
+ .saved-config {
81
+ background: #f8f9fa;
82
+ border-radius: 8px;
83
+ padding: 20px;
84
+ margin: 30px 0;
85
+ text-align: left;
86
+ }
87
+ .saved-config-title {
88
+ font-size: 12px;
89
+ font-weight: 600;
90
+ color: #666;
91
+ text-transform: uppercase;
92
+ margin-bottom: 15px;
93
+ text-align: center;
94
+ }
95
+ .config-item {
96
+ display: flex;
97
+ justify-content: space-between;
98
+ padding: 8px 0;
99
+ border-bottom: 1px solid #e1e4e8;
100
+ font-size: 13px;
101
+ }
102
+ .config-item:last-child { border-bottom: none; }
103
+ .config-label { color: #666; font-weight: 500; }
104
+ .config-value {
105
+ color: #333;
106
+ font-family: 'Courier New', monospace;
107
+ max-width: 300px;
108
+ overflow: hidden;
109
+ text-overflow: ellipsis;
110
+ white-space: nowrap;
111
+ }
112
+ .config-value.masked { color: #999; }
113
+ .form-view { display: none; }
114
+ .form-group { margin-bottom: 16px; }
115
+ label {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 6px;
119
+ margin-bottom: 6px;
120
+ color: #333;
121
+ font-weight: 500;
122
+ font-size: 14px;
123
+ }
124
+ input {
125
+ width: 100%;
126
+ padding: 10px 12px;
127
+ border: 2px solid #e1e4e8;
128
+ border-radius: 8px;
129
+ font-size: 13px;
130
+ font-family: "Helvetica Neue LT Pro", "Open Sans", 'Courier New', monospace !important;
131
+ transition: border-color 0.2s;
132
+ }
133
+ input:focus {
134
+ outline: none;
135
+ border-color: #b91d8f;
136
+ }
137
+ .optional {
138
+ color: #999;
139
+ font-weight: normal;
140
+ font-size: 12px;
141
+ }
142
+ .tooltip {
143
+ position: relative;
144
+ display: inline-flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ width: 16px;
148
+ height: 16px;
149
+ background: #b91d8f;
150
+ color: white;
151
+ border-radius: 50%;
152
+ font-size: 11px;
153
+ font-weight: bold;
154
+ cursor: pointer;
155
+ flex-shrink: 0;
156
+ user-select: none;
157
+ }
158
+ .tooltip-content {
159
+ display: none !important;
160
+ position: fixed;
161
+ max-width: 380px;
162
+ width: max-content;
163
+ background: white;
164
+ border-radius: 8px;
165
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
166
+ z-index: 10000;
167
+ overflow: hidden;
168
+ border: 2px solid #b91d8f;
169
+ pointer-events: auto;
170
+ }
171
+ .tooltip-content.active {
172
+ display: block !important;
173
+ }
174
+ .tooltip-arrow {
175
+ position: absolute;
176
+ width: 0;
177
+ height: 0;
178
+ border: 8px solid transparent;
179
+ z-index: 1;
180
+ }
181
+ .tooltip-arrow.left {
182
+ left: -16px;
183
+ top: 50%;
184
+ transform: translateY(-50%);
185
+ border-right-color: #b91d8f;
186
+ }
187
+ .tooltip-arrow.right {
188
+ right: -16px;
189
+ top: 50%;
190
+ transform: translateY(-50%);
191
+ border-left-color: #b91d8f;
192
+ }
193
+ .tooltip-arrow.top {
194
+ top: -16px;
195
+ left: 50%;
196
+ transform: translateX(-50%);
197
+ border-bottom-color: #b91d8f;
198
+ }
199
+ .tooltip-arrow.bottom {
200
+ bottom: -16px;
201
+ left: 50%;
202
+ transform: translateX(-50%);
203
+ border-top-color: #b91d8f;
204
+ }
205
+ .tooltip-header {
206
+ background: #b91d8f;
207
+ color: white;
208
+ padding: 8px 12px;
209
+ font-weight: 600;
210
+ font-size: 13px;
211
+ }
212
+ .tooltip-body {
213
+ padding: 10px 12px;
214
+ }
215
+ .tooltip-image {
216
+ width: 100%;
217
+ max-height: 200px;
218
+ object-fit: contain;
219
+ border-radius: 4px;
220
+ margin-bottom: 6px;
221
+ border: 1px solid #e1e4e8;
222
+ background: #f8f9fa;
223
+ }
224
+ .tooltip-text {
225
+ color: #555;
226
+ font-size: 12px;
227
+ line-height: 1.4;
228
+ font-style: italic;
229
+ }
230
+ .button-group {
231
+ display: flex;
232
+ gap: 10px;
233
+ margin-top: 20px;
234
+ }
235
+ button {
236
+ flex: 1;
237
+ padding: 12px;
238
+ border: 2px solid transparent;
239
+ border-radius: 8px;
240
+ font-size: 15px;
241
+ font-weight: 600;
242
+ cursor: pointer;
243
+ transition: all 0.2s;
244
+ }
245
+ .btn-primary {
246
+ background: linear-gradient(135deg, #b91d8f 0%, #7a1460 100%);
247
+ color: white;
248
+ }
249
+ .btn-primary:hover {
250
+ transform: translateY(-2px);
251
+ box-shadow: 0 6px 20px rgba(185, 29, 143, 0.4);
252
+ }
253
+ .btn-secondary {
254
+ background: #f6f8fa;
255
+ color: #666;
256
+ }
257
+ .btn-secondary:hover {
258
+ background: #e1e4e8;
259
+ }
260
+ .btn-danger {
261
+ background: white;
262
+ color: #b91d8f;
263
+ border: 2px solid #b91d8f;
264
+ }
265
+ .btn-danger:hover {
266
+ background: #fef1fd;
267
+ transform: translateY(-1px);
268
+ }
269
+ .btn-link {
270
+ background: transparent;
271
+ color: #b91d8f;
272
+ padding: 8px;
273
+ font-size: 14px;
274
+ }
275
+ .btn-link:hover {
276
+ background: #f6f8fa;
277
+ }
278
+ .status {
279
+ margin-top: 20px;
280
+ padding: 12px;
281
+ border-radius: 8px;
282
+ font-size: 14px;
283
+ display: none;
284
+ }
285
+ .status.success {
286
+ background: #d4edda;
287
+ color: #155724;
288
+ border: 1px solid #c3e6cb;
289
+ }
290
+ .status.error {
291
+ background: #f8d7da;
292
+ color: #721c24;
293
+ border: 1px solid #f5c6cb;
294
+ }
295
+ .status.info {
296
+ background: #d1ecf1;
297
+ color: #0c5460;
298
+ border: 1px solid #bee5eb;
299
+ }
300
+ /* Loading overlay and spinner */
301
+ .loading-overlay {
302
+ display: none;
303
+ position: absolute;
304
+ top: -32px;
305
+ left: -32px;
306
+ right: -32px;
307
+ bottom: -32px;
308
+ background: rgba(255, 255, 255, 0.85);
309
+ backdrop-filter: blur(2px);
310
+ border-radius: 16px;
311
+ z-index: 1000;
312
+ justify-content: center;
313
+ align-items: center;
314
+ flex-direction: column;
315
+ gap: 16px;
316
+ }
317
+ .loading-overlay.active {
318
+ display: flex;
319
+ }
320
+ .spinner {
321
+ width: 48px;
322
+ height: 48px;
323
+ border: 4px solid #e1e4e8;
324
+ border-top-color: #b91d8f;
325
+ border-radius: 50%;
326
+ animation: spin 0.8s linear infinite;
327
+ }
328
+ @keyframes spin {
329
+ to { transform: rotate(360deg); }
330
+ }
331
+ .loading-message {
332
+ color: #666;
333
+ font-size: 14px;
334
+ font-weight: 500;
335
+ text-align: center;
336
+ max-width: 300px;
337
+ }
338
+ .form-view {
339
+ position: relative;
340
+ }
341
+ .modal-overlay {
342
+ display: none;
343
+ position: fixed;
344
+ top: 0;
345
+ left: 0;
346
+ right: 0;
347
+ bottom: 0;
348
+ background: rgba(0, 0, 0, 0.5);
349
+ z-index: 10001;
350
+ justify-content: center;
351
+ align-items: center;
352
+ }
353
+ .modal-overlay.active {
354
+ display: flex;
355
+ }
356
+ .modal-content {
357
+ background: white;
358
+ border-radius: 12px;
359
+ padding: 30px;
360
+ max-width: 400px;
361
+ width: 90%;
362
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
363
+ }
364
+ .modal-title {
365
+ font-size: 20px;
366
+ font-weight: 600;
367
+ color: #333;
368
+ margin-bottom: 12px;
369
+ }
370
+ .modal-message {
371
+ font-size: 14px;
372
+ color: #666;
373
+ margin-bottom: 24px;
374
+ line-height: 1.5;
375
+ }
376
+ .modal-buttons {
377
+ display: flex;
378
+ gap: 10px;
379
+ justify-content: flex-end;
380
+ }
381
+ .modal-buttons button {
382
+ padding: 10px 20px;
383
+ font-size: 14px;
384
+ flex: none;
385
+ }
386
+ </style>
387
+ </head>
388
+ <body>
389
+ <div class="container">
390
+ <div id="quickSigninView" class="quick-signin">
391
+ <div id="loadingOverlayQuickSignin" class="loading-overlay">
392
+ <div class="spinner"></div>
393
+ <div id="loadingMessageQuickSignin" class="loading-message">Redirecting to login...</div>
394
+ </div>
395
+ <div class="icon">🔐</div>
396
+ <h1>Welcome Back!</h1>
397
+ <p class="subtitle">Ready to sign in with your saved configuration</p>
398
+ <div class="saved-config">
399
+ <div class="saved-config-title">Saved Configuration</div>
400
+ <div id="savedConfigList"></div>
401
+ </div>
402
+ <div class="button-group">
403
+ <button type="button" class="btn-danger" onclick="clearConfigAndShowForm()">Clear All</button>
404
+ <button type="button" class="btn-primary" onclick="signInWithSavedConfig()">Sign In</button>
405
+ </div>
406
+ <button type="button" class="btn-link" onclick="showForm()" style="width: 100%; margin-top: 10px;">Edit Configuration</button>
407
+ </div>
408
+ <div id="formView" class="form-view">
409
+ <div id="loadingOverlay" class="loading-overlay">
410
+ <div class="spinner"></div>
411
+ <div id="loadingMessage" class="loading-message">Saving configuration...</div>
412
+ </div>
413
+ <h1>🔐 eGain MCP Configuration</h1>
414
+ <p class="subtitle">Enter details from your eGain <strong>Admin Console</strong></p>
415
+ <form id="configForm">
416
+ <div class="form-group">
417
+ <label for="egainUrl">
418
+ <span>eGain Environment URL</span>
419
+ <span class="tooltip" onmouseenter="showTooltip(event, 'egainUrl')" onmouseleave="hideTooltip(event, 'egainUrl')">?</span>
420
+ </label>
421
+ <input type="text" id="egainUrl" name="egainUrl" placeholder="https://your-environment.egain.cloud" required>
422
+ </div>
423
+ <div class="form-group">
424
+ <label for="authUrl">
425
+ <span>Authorization URL</span>
426
+ <span class="tooltip" onmouseenter="showTooltip(event, 'authUrl')" onmouseleave="hideTooltip(event, 'authUrl')">?</span>
427
+ </label>
428
+ <input type="text" id="authUrl" name="authUrl" placeholder="https://login.egain.cloud/.../oauth2/authorize" required>
429
+ </div>
430
+ <div class="form-group">
431
+ <label for="accessTokenUrl">
432
+ <span>Access Token URL</span>
433
+ <span class="tooltip" onmouseenter="showTooltip(event, 'accessTokenUrl')" onmouseleave="hideTooltip(event, 'accessTokenUrl')">?</span>
434
+ </label>
435
+ <input type="text" id="accessTokenUrl" name="accessTokenUrl" placeholder="https://login.egain.cloud/.../oauth2/token" required>
436
+ </div>
437
+ <div class="form-group">
438
+ <label for="clientId">
439
+ <span>Client ID</span>
440
+ <span class="tooltip" onmouseenter="showTooltip(event, 'clientId')" onmouseleave="hideTooltip(event, 'clientId')">?</span>
441
+ </label>
442
+ <input type="text" id="clientId" name="clientId" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required>
443
+ </div>
444
+ <div class="form-group">
445
+ <label for="redirectUrl">
446
+ <span>Redirect URL</span>
447
+ <span class="tooltip" onmouseenter="showTooltip(event, 'redirectUrl')" onmouseleave="hideTooltip(event, 'redirectUrl')">?</span>
448
+ </label>
449
+ <input type="text" id="redirectUrl" name="redirectUrl" placeholder="https://your-redirect-url.com/" required>
450
+ </div>
451
+
452
+ <!-- Advanced Settings Toggle -->
453
+ <div style="margin: 12px 0;">
454
+ <button type="button" onclick="toggleAdvancedSettings()" style="padding: 6px 0; font-size: 13px; display: flex; align-items: center; gap: 6px; background: none; border: none; color: #b91d8f; cursor: pointer; font-family: inherit;">
455
+ <span id="advancedToggleIcon">▶</span>
456
+ <span>Advanced Settings</span>
457
+ <span style="color: #999; font-size: 11px;">(optional)</span>
458
+ </button>
459
+ </div>
460
+
461
+ <!-- Advanced Settings Section (hidden by default) -->
462
+ <div id="advancedSettings" style="display: none;">
463
+ <div class="form-group">
464
+ <label for="clientSecret">
465
+ <span>Client Secret</span>
466
+ <span class="tooltip" onmouseenter="showTooltip(event, 'clientSecret')" onmouseleave="hideTooltip(event, 'clientSecret')">?</span>
467
+ <span class="optional">(optional)</span>
468
+ </label>
469
+ <input type="password" id="clientSecret" name="clientSecret" placeholder="Required for normal authentication flow/non-PKCE">
470
+ </div>
471
+ <div class="form-group">
472
+ <label for="scopePrefix">
473
+ <span>Scope Prefix</span>
474
+ <span class="tooltip" onmouseenter="showTooltip(event, 'scopePrefix')" onmouseleave="hideTooltip(event, 'scopePrefix')">?</span>
475
+ <span class="optional">(optional)</span>
476
+ </label>
477
+ <input type="text" id="scopePrefix" name="scopePrefix" placeholder="https://your.scope-prefix.cloud/auth/">
478
+ </div>
479
+ </div>
480
+ <div style="font-size: 11px; color: #999; margin: 12px 0 0 0; line-height: 1.4;">
481
+ 🔒 Your configuration will be securely saved to your <code style="background: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-size: 10px;">~/.egain-mcp/config.json</code>.
482
+ </div>
483
+ <div class="button-group">
484
+ <button type="button" class="btn-secondary" onclick="cancelForm()">Cancel</button>
485
+ <button type="submit" class="btn-primary">Save & Authenticate</button>
486
+ </div>
487
+ </form>
488
+ </div>
489
+ <div id="status" class="status"></div>
490
+ </div>
491
+
492
+ <!-- Confirmation Modal -->
493
+ <div id="confirmModal" class="modal-overlay" onclick="if(event.target === this) closeModal(false)">
494
+ <div class="modal-content">
495
+ <div class="modal-title" id="modalTitle">Confirm Action</div>
496
+ <div class="modal-message" id="modalMessage">Are you sure?</div>
497
+ <div class="modal-buttons">
498
+ <button type="button" class="btn-secondary" onclick="closeModal(false)">Cancel</button>
499
+ <button type="button" class="btn-danger" id="modalConfirmBtn" onclick="closeModal(true)">Confirm</button>
500
+ </div>
501
+ </div>
502
+ </div>
503
+
504
+ <!-- Tooltip Popups (outside container for proper fixed positioning) -->
505
+ <div id="tooltip-egainUrl" class="tooltip-content">
506
+ <div class="tooltip-header">eGain Environment URL</div>
507
+ <div class="tooltip-body">
508
+ <img src="/img/env-tooltip.png" class="tooltip-image" alt="eGain Environment URL" onerror="this.style.display='none'">
509
+ <div class="tooltip-text">Enter the domain URL displayed in your browser when accessing the eGain application.</div>
510
+ </div>
511
+ </div>
512
+
513
+ <div id="tooltip-authUrl" class="tooltip-content">
514
+ <div class="tooltip-header">Authorization URL</div>
515
+ <div class="tooltip-body">
516
+ <img src="/img/authurl-tooltip.png" class="tooltip-image" alt="Authorization URL" onerror="this.style.display='none'">
517
+ <div class="tooltip-text">In the Partition space, go to Integration → Client Application → Metadata, and copy the Authorization URL.</div>
518
+ </div>
519
+ </div>
520
+
521
+ <div id="tooltip-accessTokenUrl" class="tooltip-content">
522
+ <div class="tooltip-header">Access Token URL</div>
523
+ <div class="tooltip-body">
524
+ <img src="/img/accesstoken-tooltip.png" class="tooltip-image" alt="Access Token URL" onerror="this.style.display='none'">
525
+ <div class="tooltip-text">In the Partition space, go to Integration → Client Application → Metadata, and copy the Access Token URL.</div>
526
+ </div>
527
+ </div>
528
+
529
+ <div id="tooltip-clientId" class="tooltip-content">
530
+ <div class="tooltip-header">Client ID</div>
531
+ <div class="tooltip-body">
532
+ <img src="/img/clientid-tooltip.png" class="tooltip-image" alt="Client ID" onerror="this.style.display='none'">
533
+ <div class="tooltip-text">In the Partition space, go to Integration → Client Application, select your client app, and copy the Client ID.</div>
534
+ </div>
535
+ </div>
536
+
537
+ <div id="tooltip-redirectUrl" class="tooltip-content">
538
+ <div class="tooltip-header">Redirect URL</div>
539
+ <div class="tooltip-body">
540
+ <img src="/img/redirect-tooltip.png" class="tooltip-image" alt="Redirect URL" onerror="this.style.display='none'">
541
+ <div class="tooltip-text">In the Partition space, go to Integration → Client Application, select your client app, and copy the Redirect URL.</div>
542
+ </div>
543
+ </div>
544
+
545
+ <div id="tooltip-clientSecret" class="tooltip-content">
546
+ <div class="tooltip-header">Client Secret</div>
547
+ <div class="tooltip-body">
548
+ <img src="/img/clientsecret-tooltip.png" class="tooltip-image" alt="Client Secret" onerror="this.style.display='none'">
549
+ <div class="tooltip-text">In the Partition space, go to Integration → Client Application, select your client app, and copy the Client Secret under Secrets.</div>
550
+ </div>
551
+ </div>
552
+
553
+ <div id="tooltip-scopePrefix" class="tooltip-content">
554
+ <div class="tooltip-header">Scope Prefix</div>
555
+ <div class="tooltip-body">
556
+ <img src="/img/scopeprefix-tooltip.png" class="tooltip-image" alt="Scope Prefix" onerror="this.style.display='none'">
557
+ <div class="tooltip-text">In the Partition space, go to Integration → Client Application → Metadata, and copy the API Permission Prefix.</div>
558
+ </div>
559
+ </div>
560
+
561
+ <script src="/config-page.js"></script>
562
+ </body>
563
+ </html>`;
564
+ const CONFIG_PAGE_JS = `const FIELDS = ['egainUrl', 'authUrl', 'accessTokenUrl', 'clientId', 'redirectUrl', 'clientSecret', 'scopePrefix'];
565
+ const FIELD_LABELS = {
566
+ egainUrl: 'eGain URL', authUrl: 'Auth URL', accessTokenUrl: 'Token URL',
567
+ clientId: 'Client ID', redirectUrl: 'Redirect URL', clientSecret: 'Client Secret',
568
+ scopePrefix: 'Scope Prefix'
569
+ };
570
+
571
+ let savedConfigData = null; // Store config in memory only
572
+ let authenticationStarted = false; // Track if user started authentication process
573
+ let lastSubmittedConfig = null; // Track last submitted config to prevent duplicate submissions
574
+ let isSubmitting = false; // Track if form submission is in progress
575
+
576
+ // Fetch saved config from backend (secure file storage)
577
+ async function loadSavedConfig() {
578
+ try {
579
+ const response = await fetch('/get-config');
580
+ if (response.ok) {
581
+ const data = await response.json();
582
+ if (data.config && data.config.egainUrl && data.config.clientId) {
583
+ // Only set if we have valid required fields
584
+ savedConfigData = data.config;
585
+ return true;
586
+ }
587
+ }
588
+ } catch (error) {
589
+ console.error('Could not load saved config:', error);
590
+ }
591
+ savedConfigData = null;
592
+ return false;
593
+ }
594
+
595
+ function hasSavedConfig() {
596
+ return savedConfigData &&
597
+ savedConfigData.egainUrl &&
598
+ savedConfigData.clientId &&
599
+ savedConfigData.authUrl &&
600
+ savedConfigData.accessTokenUrl;
601
+ }
602
+
603
+ function displaySavedConfig() {
604
+ const listEl = document.getElementById('savedConfigList');
605
+ listEl.innerHTML = '';
606
+ if (!savedConfigData) return;
607
+
608
+ FIELDS.forEach(field => {
609
+ const value = savedConfigData[field];
610
+ if (value) {
611
+ const item = document.createElement('div');
612
+ item.className = 'config-item';
613
+ const label = document.createElement('span');
614
+ label.className = 'config-label';
615
+ label.textContent = FIELD_LABELS[field];
616
+ const valueSpan = document.createElement('span');
617
+ valueSpan.className = 'config-value';
618
+ if (field === 'clientSecret') {
619
+ valueSpan.classList.add('masked');
620
+ valueSpan.textContent = '••••••••';
621
+ } else if (field === 'clientId') {
622
+ valueSpan.textContent = value.substring(0, 8) + '...';
623
+ } else {
624
+ valueSpan.textContent = value.length > 40 ? value.substring(0, 40) + '...' : value;
625
+ }
626
+ item.appendChild(label);
627
+ item.appendChild(valueSpan);
628
+ listEl.appendChild(item);
629
+ }
630
+ });
631
+ }
632
+
633
+ function showQuickSignin() {
634
+ document.getElementById('quickSigninView').style.display = 'block';
635
+ document.getElementById('formView').style.display = 'none';
636
+ displaySavedConfig();
637
+ }
638
+
639
+ function showForm() {
640
+ document.getElementById('quickSigninView').style.display = 'none';
641
+ document.getElementById('formView').style.display = 'block';
642
+ loadFormValues();
643
+ }
644
+
645
+ function loadFormValues() {
646
+ if (!savedConfigData) return;
647
+
648
+ // Load all field values
649
+ FIELDS.forEach(field => {
650
+ const value = savedConfigData[field];
651
+ if (value) document.getElementById(field).value = value;
652
+ });
653
+
654
+ // Show advanced settings if clientSecret or scopePrefix exist
655
+ if (savedConfigData.clientSecret || savedConfigData.scopePrefix) {
656
+ toggleAdvancedSettings();
657
+ }
658
+ }
659
+
660
+ async function clearConfigAndShowForm() {
661
+ showModal(
662
+ 'Clear All Configuration?',
663
+ 'This will delete all saved OAuth settings from your home directory. You will need to re-enter them next time.',
664
+ async (confirmed) => {
665
+ if (confirmed) {
666
+ try {
667
+ const response = await fetch('/clear-config', { method: 'POST' });
668
+ if (response.ok) {
669
+ savedConfigData = null;
670
+ FIELDS.forEach(field => {
671
+ document.getElementById(field).value = '';
672
+ });
673
+ showStatus('Configuration cleared successfully', 'success');
674
+ showForm();
675
+ } else {
676
+ showStatus('Failed to clear configuration', 'error');
677
+ }
678
+ } catch (error) {
679
+ showStatus('Error clearing configuration: ' + error.message, 'error');
680
+ }
681
+ }
682
+ },
683
+ 'Clear All'
684
+ );
685
+ }
686
+
687
+ function cancelForm() {
688
+ if (hasSavedConfig()) {
689
+ showQuickSignin();
690
+ } else {
691
+ showModal(
692
+ 'Cancel Authentication?',
693
+ "You haven't saved any configuration yet. This will cancel the authentication process.",
694
+ async (confirmed) => {
695
+ if (confirmed) {
696
+ // Notify server that user cancelled
697
+ try {
698
+ await fetch('/cancel', { method: 'POST' });
699
+ } catch (error) {
700
+ console.error('Could not notify server of cancellation:', error);
701
+ }
702
+ window.close();
703
+ }
704
+ },
705
+ 'Cancel'
706
+ );
707
+ }
708
+ }
709
+
710
+ async function signInWithSavedConfig() {
711
+ authenticationStarted = true; // Mark that auth has started
712
+
713
+ // Show loading overlay
714
+ showLoadingOverlay('Redirecting to login...');
715
+
716
+ try {
717
+ const response = await fetch('/get-oauth-url', { method: 'POST' });
718
+ const result = await response.json();
719
+
720
+ if (response.ok && result.oauthUrl) {
721
+ console.log('🔗 OAuth URL:', result.oauthUrl);
722
+ // Redirect after a brief delay to show loading state
723
+ setTimeout(() => {
724
+ window.location.href = result.oauthUrl;
725
+ }, 500);
726
+ } else {
727
+ hideLoadingOverlay();
728
+ showStatus('❌ ' + (result.error || 'Failed to get OAuth URL'), 'error');
729
+ authenticationStarted = false; // Reset on error
730
+ }
731
+ } catch (error) {
732
+ hideLoadingOverlay();
733
+ showStatus('❌ Error: ' + error.message, 'error');
734
+ authenticationStarted = false; // Reset on error
735
+ }
736
+ }
737
+
738
+ function showStatus(message, type) {
739
+ const statusEl = document.getElementById('status');
740
+ statusEl.textContent = message;
741
+ statusEl.className = 'status ' + type;
742
+ statusEl.style.display = 'block';
743
+ // Auto-hide info and success messages after a delay
744
+ if (type === 'info') {
745
+ setTimeout(() => {
746
+ // Only hide if it's still an info message (not changed to success/error)
747
+ if (statusEl.className === 'status info') {
748
+ statusEl.style.display = 'none';
749
+ }
750
+ }, 3000);
751
+ } else if (type === 'success') {
752
+ setTimeout(() => {
753
+ // Only hide if it's still a success message (not changed to error)
754
+ if (statusEl.className === 'status success') {
755
+ statusEl.style.display = 'none';
756
+ }
757
+ }, 4000);
758
+ }
759
+ }
760
+
761
+ function configValuesEqual(config1, config2) {
762
+ // Compare all fields that matter
763
+ const fieldsToCompare = ['egainUrl', 'authUrl', 'accessTokenUrl', 'clientId', 'redirectUrl', 'clientSecret', 'scopePrefix'];
764
+ for (const field of fieldsToCompare) {
765
+ const val1 = (config1[field] || '').trim();
766
+ const val2 = (config2[field] || '').trim();
767
+ if (val1 !== val2) {
768
+ return false;
769
+ }
770
+ }
771
+ return true;
772
+ }
773
+
774
+ function showLoadingOverlay(message) {
775
+ // Show overlay for form view
776
+ const overlay = document.getElementById('loadingOverlay');
777
+ const loadingMessage = document.getElementById('loadingMessage');
778
+ if (overlay) {
779
+ overlay.classList.add('active');
780
+ if (loadingMessage) {
781
+ loadingMessage.textContent = message || 'Saving configuration...';
782
+ }
783
+ }
784
+
785
+ // Show overlay for quick signin view
786
+ const overlayQuickSignin = document.getElementById('loadingOverlayQuickSignin');
787
+ const loadingMessageQuickSignin = document.getElementById('loadingMessageQuickSignin');
788
+ if (overlayQuickSignin) {
789
+ overlayQuickSignin.classList.add('active');
790
+ if (loadingMessageQuickSignin) {
791
+ loadingMessageQuickSignin.textContent = message || 'Redirecting to login...';
792
+ }
793
+ }
794
+ }
795
+
796
+ function hideLoadingOverlay() {
797
+ // Hide overlay for form view
798
+ const overlay = document.getElementById('loadingOverlay');
799
+ if (overlay) {
800
+ overlay.classList.remove('active');
801
+ }
802
+
803
+ // Hide overlay for quick signin view
804
+ const overlayQuickSignin = document.getElementById('loadingOverlayQuickSignin');
805
+ if (overlayQuickSignin) {
806
+ overlayQuickSignin.classList.remove('active');
807
+ }
808
+ }
809
+
810
+ async function authenticateWithConfig(config) {
811
+ // Prevent duplicate submissions
812
+ if (isSubmitting) {
813
+ return; // Already submitting, ignore
814
+ }
815
+
816
+ // Check if values have changed since last submission
817
+ if (lastSubmittedConfig && configValuesEqual(config, lastSubmittedConfig)) {
818
+ showStatus('⚠️ Configuration unchanged. Already saved.', 'info');
819
+ return; // Values haven't changed, don't submit again
820
+ }
821
+
822
+ try {
823
+ isSubmitting = true; // Mark as submitting
824
+ const submitButton = document.querySelector('button[type="submit"]');
825
+ if (submitButton) {
826
+ submitButton.disabled = true;
827
+ submitButton.textContent = 'Saving...';
828
+ }
829
+
830
+ // Show loading overlay with spinner
831
+ showLoadingOverlay('Saving configuration...');
832
+ authenticationStarted = true; // Mark that auth has started
833
+
834
+ const response = await fetch('/authenticate', {
835
+ method: 'POST',
836
+ headers: { 'Content-Type': 'application/json' },
837
+ body: JSON.stringify(config)
838
+ });
839
+ const result = await response.json();
840
+
841
+ if (response.ok && result.oauthUrl) {
842
+ // Config saved! Store this config as last submitted
843
+ lastSubmittedConfig = { ...config };
844
+
845
+ console.log('🔗 OAuth URL:', result.oauthUrl);
846
+
847
+ // Update loading message (overlay already shows the status, no need for status message)
848
+ showLoadingOverlay('Configuration saved! Redirecting to login...');
849
+
850
+ setTimeout(() => {
851
+ window.location.href = result.oauthUrl;
852
+ }, 500);
853
+ } else if (response.ok && result.success) {
854
+ lastSubmittedConfig = { ...config };
855
+ hideLoadingOverlay();
856
+ showStatus('✅ ' + result.message, 'success');
857
+ setTimeout(() => { window.close(); }, 2000);
858
+ } else {
859
+ hideLoadingOverlay();
860
+ showStatus('❌ ' + (result.error || 'Authentication failed'), 'error');
861
+ authenticationStarted = false; // Reset on error
862
+ isSubmitting = false; // Reset submitting flag
863
+ if (submitButton) {
864
+ submitButton.disabled = false;
865
+ submitButton.textContent = 'Save & Authenticate';
866
+ }
867
+ }
868
+ } catch (error) {
869
+ hideLoadingOverlay();
870
+ showStatus('❌ Error: ' + error.message, 'error');
871
+ authenticationStarted = false; // Reset on error
872
+ isSubmitting = false; // Reset submitting flag
873
+ const submitButton = document.querySelector('button[type="submit"]');
874
+ if (submitButton) {
875
+ submitButton.disabled = false;
876
+ submitButton.textContent = 'Save & Authenticate';
877
+ }
878
+ }
879
+ }
880
+
881
+ document.getElementById('configForm').addEventListener('submit', async (e) => {
882
+ e.preventDefault();
883
+
884
+ // Prevent duplicate submissions
885
+ if (isSubmitting) {
886
+ return;
887
+ }
888
+
889
+ const formData = new FormData(e.target);
890
+ const config = {};
891
+ for (let [key, value] of formData.entries()) {
892
+ config[key] = value;
893
+ }
894
+
895
+ // Config is now sent to backend for secure file storage (not cookies)
896
+ await authenticateWithConfig(config);
897
+ });
898
+
899
+ // Initialize: Load saved config from backend
900
+ (async () => {
901
+ const hasConfig = await loadSavedConfig();
902
+ if (hasConfig) {
903
+ showQuickSignin();
904
+ } else {
905
+ showForm();
906
+ }
907
+ })();
908
+
909
+ // Custom modal functions
910
+ let modalCallback = null;
911
+
912
+ function showModal(title, message, callback, confirmText = 'Confirm') {
913
+ document.getElementById('modalTitle').textContent = title;
914
+ document.getElementById('modalMessage').textContent = message;
915
+ document.getElementById('modalConfirmBtn').textContent = confirmText;
916
+ modalCallback = callback;
917
+ document.getElementById('confirmModal').classList.add('active');
918
+ }
919
+
920
+ function closeModal(confirmed) {
921
+ document.getElementById('confirmModal').classList.remove('active');
922
+ if (modalCallback) {
923
+ modalCallback(confirmed);
924
+ modalCallback = null;
925
+ }
926
+ }
927
+
928
+ // Advanced settings toggle
929
+ function toggleAdvancedSettings() {
930
+ const advancedSection = document.getElementById('advancedSettings');
931
+ const toggleIcon = document.getElementById('advancedToggleIcon');
932
+
933
+ if (advancedSection.style.display === 'none') {
934
+ advancedSection.style.display = 'block';
935
+ toggleIcon.textContent = '▼';
936
+ } else {
937
+ advancedSection.style.display = 'none';
938
+ toggleIcon.textContent = '▶';
939
+ }
940
+ }
941
+
942
+ // Tooltip functions
943
+ function showTooltip(event, fieldName) {
944
+ event.preventDefault(); // Prevent label from focusing input
945
+ event.stopPropagation(); // Prevent event from bubbling
946
+
947
+ const tooltipId = 'tooltip-' + fieldName;
948
+ let tooltipElement = document.getElementById(tooltipId);
949
+ const button = event.currentTarget;
950
+
951
+ if (tooltipElement) {
952
+ // Show the tooltip first to get its dimensions
953
+ tooltipElement.classList.add('active');
954
+
955
+ // Get actual dimensions after showing
956
+ const buttonRect = button.getBoundingClientRect();
957
+ const tooltipRect = tooltipElement.getBoundingClientRect();
958
+ const tooltipWidth = tooltipRect.width || 380;
959
+ const tooltipHeight = tooltipRect.height || 300;
960
+
961
+ const spacing = 16; // Space between button and tooltip
962
+ const viewportPadding = 10; // Padding from viewport edges
963
+ const horizontalComfortZone = 50; // Extra space needed to avoid cramped horizontal positioning
964
+
965
+ // Calculate available space on all sides
966
+ const spaceOnRight = window.innerWidth - buttonRect.right;
967
+ const spaceOnLeft = buttonRect.left;
968
+ const spaceBelow = window.innerHeight - buttonRect.bottom;
969
+ const spaceAbove = buttonRect.top;
970
+
971
+ let left, top;
972
+ let arrowSide = 'left'; // Default: tooltip on right, arrow on left
973
+
974
+ // Check if horizontal positioning would be too cramped (tooltip might cover the icon)
975
+ const horizontalSpaceTight = (spaceOnRight < tooltipWidth + horizontalComfortZone) &&
976
+ (spaceOnLeft < tooltipWidth + horizontalComfortZone);
977
+
978
+ if (horizontalSpaceTight) {
979
+ // Use vertical positioning to avoid covering the icon
980
+ // Determine if we're in the top or bottom half of the viewport
981
+ const inTopHalf = buttonRect.top < window.innerHeight / 2;
982
+
983
+ if (inTopHalf && spaceBelow >= tooltipHeight + spacing + viewportPadding) {
984
+ // Position below
985
+ left = buttonRect.left + (buttonRect.width / 2) - (tooltipWidth / 2);
986
+ top = buttonRect.bottom + spacing;
987
+ arrowSide = 'top';
988
+ } else if (!inTopHalf && spaceAbove >= tooltipHeight + spacing + viewportPadding) {
989
+ // Position above
990
+ left = buttonRect.left + (buttonRect.width / 2) - (tooltipWidth / 2);
991
+ top = buttonRect.top - tooltipHeight - spacing;
992
+ arrowSide = 'bottom';
993
+ } else if (spaceBelow > spaceAbove) {
994
+ // Not enough vertical space either, prefer below
995
+ left = buttonRect.left + (buttonRect.width / 2) - (tooltipWidth / 2);
996
+ top = buttonRect.bottom + spacing;
997
+ arrowSide = 'top';
998
+ } else {
999
+ // Prefer above
1000
+ left = buttonRect.left + (buttonRect.width / 2) - (tooltipWidth / 2);
1001
+ top = buttonRect.top - tooltipHeight - spacing;
1002
+ arrowSide = 'bottom';
1003
+ }
1004
+
1005
+ // Keep horizontally centered within viewport
1006
+ if (left < viewportPadding) {
1007
+ left = viewportPadding;
1008
+ }
1009
+ if (left + tooltipWidth > window.innerWidth - viewportPadding) {
1010
+ left = window.innerWidth - tooltipWidth - viewportPadding;
1011
+ }
1012
+
1013
+ } else {
1014
+ // Use horizontal positioning (original logic)
1015
+ // Prefer right side if there's enough space
1016
+ if (spaceOnRight >= tooltipWidth + spacing + viewportPadding) {
1017
+ // Position to the right
1018
+ left = buttonRect.right + spacing;
1019
+ arrowSide = 'left';
1020
+ } else if (spaceOnLeft >= tooltipWidth + spacing + viewportPadding) {
1021
+ // Position to the left
1022
+ left = buttonRect.left - tooltipWidth - spacing;
1023
+ arrowSide = 'right';
1024
+ } else {
1025
+ // Not enough space on either side, use the side with more space
1026
+ if (spaceOnRight > spaceOnLeft) {
1027
+ left = buttonRect.right + spacing;
1028
+ arrowSide = 'left';
1029
+ // Allow tooltip to go to edge of screen
1030
+ if (left + tooltipWidth > window.innerWidth - viewportPadding) {
1031
+ left = window.innerWidth - tooltipWidth - viewportPadding;
1032
+ }
1033
+ } else {
1034
+ left = buttonRect.left - tooltipWidth - spacing;
1035
+ arrowSide = 'right';
1036
+ // Allow tooltip to go to edge of screen
1037
+ if (left < viewportPadding) {
1038
+ left = viewportPadding;
1039
+ }
1040
+ }
1041
+ }
1042
+
1043
+ // Center vertically relative to button
1044
+ top = buttonRect.top + (buttonRect.height / 2) - (tooltipHeight / 2);
1045
+
1046
+ // Keep tooltip within viewport vertically
1047
+ if (top < viewportPadding) {
1048
+ top = viewportPadding;
1049
+ }
1050
+ if (top + tooltipHeight > window.innerHeight - viewportPadding) {
1051
+ top = window.innerHeight - tooltipHeight - viewportPadding;
1052
+ }
1053
+ }
1054
+
1055
+ // Apply positioning
1056
+ tooltipElement.style.left = left + 'px';
1057
+ tooltipElement.style.top = top + 'px';
1058
+
1059
+ // Update arrow direction
1060
+ const arrow = tooltipElement.querySelector('.tooltip-arrow');
1061
+ if (arrow) {
1062
+ arrow.className = 'tooltip-arrow ' + arrowSide;
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ function hideTooltip(event, fieldName) {
1068
+ const tooltipId = 'tooltip-' + fieldName;
1069
+ let tooltipElement = document.getElementById(tooltipId);
1070
+
1071
+ if (tooltipElement) {
1072
+ tooltipElement.classList.remove('active');
1073
+ }
1074
+ }
1075
+
1076
+ // Close tooltip on ESC key
1077
+ document.addEventListener('keydown', function(e) {
1078
+ if (e.key === 'Escape') {
1079
+ document.querySelectorAll('.tooltip-content').forEach(tip => {
1080
+ tip.classList.remove('active');
1081
+ });
1082
+ }
1083
+ });
1084
+
1085
+ // Notify server if window is closed without starting authentication
1086
+ window.addEventListener('beforeunload', function(e) {
1087
+ // Only send cancel if user never started the authentication process
1088
+ // (If they started auth, they either completed it or clicked cancel explicitly)
1089
+ if (!authenticationStarted) {
1090
+ // Use sendBeacon for reliable delivery even as page unloads
1091
+ navigator.sendBeacon('/cancel', '');
1092
+ }
1093
+ });`;
1094
+ const SAFARI_WARNING_HTML = `<!DOCTYPE html>
1095
+ <html lang="en">
1096
+ <head>
1097
+ <meta charset="UTF-8">
1098
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1099
+ <title>Safari Not Supported - eGain MCP</title>
1100
+ <style>
1101
+ * {
1102
+ margin: 0;
1103
+ padding: 0;
1104
+ box-sizing: border-box;
1105
+ }
1106
+
1107
+ body {
1108
+ font-family: "Open Sans", "Segoe UI", "SegoeUI", "Helvetica Neue", Helvetica, Arial, sans-serif !important;
1109
+ background: #fef1fd;
1110
+ min-height: 100vh;
1111
+ display: flex;
1112
+ justify-content: center;
1113
+ align-items: center;
1114
+ padding: 20px;
1115
+ }
1116
+
1117
+ .container {
1118
+ background: white;
1119
+ border-radius: 16px;
1120
+ box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, 0.12);
1121
+ max-width: 500px;
1122
+ width: 100%;
1123
+ padding: 32px;
1124
+ text-align: center;
1125
+ }
1126
+
1127
+ .warning-icon {
1128
+ font-size: 64px;
1129
+ margin-bottom: 20px;
1130
+ }
1131
+
1132
+ h1 {
1133
+ color: #e74c3c;
1134
+ font-size: 28px;
1135
+ margin-bottom: 30px;
1136
+ font-weight: 600;
1137
+ }
1138
+
1139
+ .reason {
1140
+ background: #fff0f6;
1141
+ border-left: 4px solid #d946a6;
1142
+ padding: 20px;
1143
+ text-align: left;
1144
+ border-radius: 4px;
1145
+ }
1146
+
1147
+ .reason strong {
1148
+ color: #a21361;
1149
+ display: block;
1150
+ margin-bottom: 12px;
1151
+ font-size: 16px;
1152
+ }
1153
+
1154
+ .reason p {
1155
+ color: #a21361;
1156
+ font-size: 14px;
1157
+ line-height: 1.6;
1158
+ }
1159
+ </style>
1160
+ </head>
1161
+ <body>
1162
+ <div class="container">
1163
+ <div class="warning-icon">⚠️</div>
1164
+ <h1>Safari Not Supported</h1>
1165
+
1166
+ <div class="reason">
1167
+ <strong>Why?</strong>
1168
+ <p>
1169
+ Safari doesn't support private browsing mode via command line, which is required
1170
+ to protect your OAuth credentials from being cached or leaked. We prioritize your
1171
+ security over convenience.
1172
+ </p>
1173
+ </div>
1174
+ </div>
1175
+ </body>
1176
+ </html>`;
45
1177
  export class AuthenticationHook {
46
1178
  token = null;
47
1179
  authConfig;
@@ -413,6 +1545,11 @@ export class AuthenticationHook {
413
1545
  * Generate Safari warning page (Safari doesn't support private browsing via CLI)
414
1546
  */
415
1547
  getSafariWarningPage() {
1548
+ // Use embedded content first (works in npm package)
1549
+ if (SAFARI_WARNING_HTML) {
1550
+ return SAFARI_WARNING_HTML;
1551
+ }
1552
+ // Fallback to file reading for development
416
1553
  try {
417
1554
  const projectRoot = getProjectRoot();
418
1555
  const htmlPath = path.join(projectRoot, 'src', 'hooks', 'auth-pages', 'safari-warning.html');
@@ -428,6 +1565,11 @@ export class AuthenticationHook {
428
1565
  * Load HTML page for browser-based configuration
429
1566
  */
430
1567
  getConfigPage() {
1568
+ // Use embedded content first (works in npm package)
1569
+ if (CONFIG_PAGE_HTML) {
1570
+ return CONFIG_PAGE_HTML;
1571
+ }
1572
+ // Fallback to file reading for development
431
1573
  try {
432
1574
  const projectRoot = getProjectRoot();
433
1575
  const htmlPath = path.join(projectRoot, 'src', 'hooks', 'auth-pages', 'config-page.html');
@@ -443,6 +1585,11 @@ export class AuthenticationHook {
443
1585
  * Serve JavaScript for config page
444
1586
  */
445
1587
  getConfigPageJS() {
1588
+ // Use embedded content first (works in npm package)
1589
+ if (CONFIG_PAGE_JS) {
1590
+ return CONFIG_PAGE_JS;
1591
+ }
1592
+ // Fallback to file reading for development
446
1593
  try {
447
1594
  const projectRoot = getProjectRoot();
448
1595
  const jsPath = path.join(projectRoot, 'src', 'hooks', 'auth-pages', 'config-page.js');