msteams-mcp 0.2.1

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.

Potentially problematic release.


This version of msteams-mcp might be problematic. Click here for more details.

Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -0
  3. package/dist/__fixtures__/api-responses.d.ts +254 -0
  4. package/dist/__fixtures__/api-responses.js +245 -0
  5. package/dist/api/calendar-api.d.ts +66 -0
  6. package/dist/api/calendar-api.js +179 -0
  7. package/dist/api/chatsvc-api.d.ts +352 -0
  8. package/dist/api/chatsvc-api.js +1100 -0
  9. package/dist/api/csa-api.d.ts +64 -0
  10. package/dist/api/csa-api.js +200 -0
  11. package/dist/api/index.d.ts +7 -0
  12. package/dist/api/index.js +7 -0
  13. package/dist/api/substrate-api.d.ts +50 -0
  14. package/dist/api/substrate-api.js +305 -0
  15. package/dist/auth/crypto.d.ts +32 -0
  16. package/dist/auth/crypto.js +66 -0
  17. package/dist/auth/index.d.ts +7 -0
  18. package/dist/auth/index.js +7 -0
  19. package/dist/auth/session-store.d.ts +87 -0
  20. package/dist/auth/session-store.js +230 -0
  21. package/dist/auth/token-extractor.d.ts +185 -0
  22. package/dist/auth/token-extractor.js +674 -0
  23. package/dist/auth/token-refresh.d.ts +25 -0
  24. package/dist/auth/token-refresh.js +85 -0
  25. package/dist/browser/auth.d.ts +53 -0
  26. package/dist/browser/auth.js +603 -0
  27. package/dist/browser/context.d.ts +40 -0
  28. package/dist/browser/context.js +122 -0
  29. package/dist/constants.d.ts +104 -0
  30. package/dist/constants.js +195 -0
  31. package/dist/index.d.ts +8 -0
  32. package/dist/index.js +12 -0
  33. package/dist/research/auth-research.d.ts +10 -0
  34. package/dist/research/auth-research.js +175 -0
  35. package/dist/research/explore.d.ts +11 -0
  36. package/dist/research/explore.js +270 -0
  37. package/dist/research/search-research.d.ts +17 -0
  38. package/dist/research/search-research.js +317 -0
  39. package/dist/server.d.ts +66 -0
  40. package/dist/server.js +295 -0
  41. package/dist/test/debug-search.d.ts +10 -0
  42. package/dist/test/debug-search.js +147 -0
  43. package/dist/test/mcp-harness.d.ts +17 -0
  44. package/dist/test/mcp-harness.js +474 -0
  45. package/dist/tools/auth-tools.d.ts +26 -0
  46. package/dist/tools/auth-tools.js +191 -0
  47. package/dist/tools/index.d.ts +56 -0
  48. package/dist/tools/index.js +34 -0
  49. package/dist/tools/meeting-tools.d.ts +33 -0
  50. package/dist/tools/meeting-tools.js +64 -0
  51. package/dist/tools/message-tools.d.ts +269 -0
  52. package/dist/tools/message-tools.js +856 -0
  53. package/dist/tools/people-tools.d.ts +46 -0
  54. package/dist/tools/people-tools.js +112 -0
  55. package/dist/tools/registry.d.ts +23 -0
  56. package/dist/tools/registry.js +63 -0
  57. package/dist/tools/search-tools.d.ts +91 -0
  58. package/dist/tools/search-tools.js +222 -0
  59. package/dist/types/errors.d.ts +58 -0
  60. package/dist/types/errors.js +132 -0
  61. package/dist/types/result.d.ts +43 -0
  62. package/dist/types/result.js +51 -0
  63. package/dist/types/server.d.ts +27 -0
  64. package/dist/types/server.js +7 -0
  65. package/dist/types/teams.d.ts +85 -0
  66. package/dist/types/teams.js +4 -0
  67. package/dist/utils/api-config.d.ts +103 -0
  68. package/dist/utils/api-config.js +158 -0
  69. package/dist/utils/auth-guards.d.ts +67 -0
  70. package/dist/utils/auth-guards.js +147 -0
  71. package/dist/utils/http.d.ts +29 -0
  72. package/dist/utils/http.js +112 -0
  73. package/dist/utils/parsers.d.ts +247 -0
  74. package/dist/utils/parsers.js +731 -0
  75. package/dist/utils/parsers.test.d.ts +7 -0
  76. package/dist/utils/parsers.test.js +511 -0
  77. package/package.json +62 -0
@@ -0,0 +1,603 @@
1
+ /**
2
+ * Authentication handling for Microsoft Teams.
3
+ * Manages login detection and manual authentication flows.
4
+ */
5
+ import { saveSessionState } from './context.js';
6
+ import { OVERLAY_STEP_PAUSE_MS, OVERLAY_COMPLETE_PAUSE_MS, } from '../constants.js';
7
+ /**
8
+ * Default Teams URL for initial login.
9
+ *
10
+ * For commercial tenants, this is teams.microsoft.com.
11
+ * For GCC/GCC-High/DoD tenants, Microsoft's login flow will redirect users
12
+ * to the appropriate URL (teams.microsoft.us, etc.) after authentication.
13
+ * We then extract the correct base URL from DISCOVER-REGION-GTM for all API calls.
14
+ */
15
+ const TEAMS_URL = 'https://teams.microsoft.com';
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // Progress Overlay UI
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+ const PROGRESS_OVERLAY_ID = 'mcp-login-progress-overlay';
20
+ /** Content for each overlay phase. */
21
+ const OVERLAY_CONTENT = {
22
+ 'signed-in': {
23
+ message: "You're signed in!",
24
+ detail: 'Setting up your connection to Teams...',
25
+ },
26
+ 'acquiring': {
27
+ message: 'Acquiring permissions...',
28
+ detail: 'Getting access to search and messages...',
29
+ },
30
+ 'saving': {
31
+ message: 'Saving your session...',
32
+ detail: "So you won't need to log in again.",
33
+ },
34
+ 'complete': {
35
+ message: 'All done!',
36
+ detail: 'This window will close automatically.',
37
+ },
38
+ 'refreshing': {
39
+ message: 'Refreshing your session...',
40
+ detail: 'Updating your access tokens...',
41
+ },
42
+ 'error': {
43
+ message: 'Something went wrong',
44
+ detail: 'Please try again or check the console for details.',
45
+ },
46
+ };
47
+ /** Detail messages that cycle during the acquiring/refreshing phases. */
48
+ const ACQUIRING_DETAILS = [
49
+ 'Preparing Teams connection...',
50
+ 'Navigating to search...',
51
+ 'Waiting for API response...',
52
+ 'Acquiring search permissions...',
53
+ 'Convincing Microsoft we mean well...',
54
+ 'Negotiating with the UI...',
55
+ 'Gathering auth tokens...',
56
+ 'Good things come to those who wait...',
57
+ 'Almost there...',
58
+ ];
59
+ /** Interval for cycling detail messages (ms). */
60
+ const DETAIL_CYCLE_INTERVAL_MS = 3000;
61
+ /** ID for the detail text element (for cycling updates). */
62
+ const DETAIL_ELEMENT_ID = 'mcp-login-detail';
63
+ /**
64
+ * Shows a progress overlay for a specific phase.
65
+ * Handles injection, content, and optional pause.
66
+ * Failures are silently ignored - the overlay is purely cosmetic.
67
+ */
68
+ async function showLoginProgress(page, phase, options = {}) {
69
+ const content = OVERLAY_CONTENT[phase];
70
+ const isComplete = phase === 'complete';
71
+ const isError = phase === 'error';
72
+ const isAnimated = phase === 'acquiring' || phase === 'refreshing';
73
+ try {
74
+ await page.evaluate(({ id, detailId, message, detail, complete, error, animated, cycleDetails, cycleInterval }) => {
75
+ // Remove existing overlay if present, clearing any running timer
76
+ const existing = document.getElementById(id);
77
+ if (existing) {
78
+ const existingWithTimer = existing;
79
+ if (existingWithTimer._cycleTimer) {
80
+ clearInterval(existingWithTimer._cycleTimer);
81
+ }
82
+ existing.remove();
83
+ }
84
+ // Remove any existing style element
85
+ const existingStyle = document.getElementById(`${id}-style`);
86
+ if (existingStyle) {
87
+ existingStyle.remove();
88
+ }
89
+ // Add keyframe animations for spinner
90
+ const style = document.createElement('style');
91
+ style.id = `${id}-style`;
92
+ style.textContent = `
93
+ @keyframes mcp-spin {
94
+ 0% { transform: rotate(0deg); }
95
+ 100% { transform: rotate(360deg); }
96
+ }
97
+ @keyframes mcp-pulse {
98
+ 0%, 100% { opacity: 1; }
99
+ 50% { opacity: 0.6; }
100
+ }
101
+ @keyframes mcp-fade {
102
+ 0% { opacity: 0; transform: translateY(4px); }
103
+ 15% { opacity: 1; transform: translateY(0); }
104
+ 85% { opacity: 1; transform: translateY(0); }
105
+ 100% { opacity: 0; transform: translateY(-4px); }
106
+ }
107
+ `;
108
+ document.head.appendChild(style);
109
+ // Create overlay container
110
+ const overlay = document.createElement('div');
111
+ overlay.id = id;
112
+ Object.assign(overlay.style, {
113
+ position: 'fixed',
114
+ top: '0',
115
+ left: '0',
116
+ right: '0',
117
+ bottom: '0',
118
+ background: 'rgba(0, 0, 0, 0.7)',
119
+ display: 'flex',
120
+ alignItems: 'center',
121
+ justifyContent: 'center',
122
+ zIndex: '999999',
123
+ fontFamily: "'Segoe UI', system-ui, sans-serif",
124
+ });
125
+ // Create modal card
126
+ const modal = document.createElement('div');
127
+ Object.assign(modal.style, {
128
+ background: 'white',
129
+ borderRadius: '12px',
130
+ padding: '40px 48px',
131
+ maxWidth: '420px',
132
+ textAlign: 'center',
133
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
134
+ });
135
+ // Create icon container (for animation)
136
+ const iconContainer = document.createElement('div');
137
+ Object.assign(iconContainer.style, {
138
+ width: '64px',
139
+ height: '64px',
140
+ margin: '0 auto 24px',
141
+ position: 'relative',
142
+ });
143
+ // Create icon
144
+ const icon = document.createElement('div');
145
+ const iconBg = error ? '#c42b1c' : complete ? '#107c10' : '#5b5fc7';
146
+ Object.assign(icon.style, {
147
+ width: '64px',
148
+ height: '64px',
149
+ borderRadius: '50%',
150
+ display: 'flex',
151
+ alignItems: 'center',
152
+ justifyContent: 'center',
153
+ fontSize: '32px',
154
+ background: iconBg,
155
+ color: 'white',
156
+ });
157
+ if (animated) {
158
+ // Spinner ring for animated states
159
+ const spinner = document.createElement('div');
160
+ Object.assign(spinner.style, {
161
+ position: 'absolute',
162
+ top: '-4px',
163
+ left: '-4px',
164
+ width: '72px',
165
+ height: '72px',
166
+ borderRadius: '50%',
167
+ border: '3px solid transparent',
168
+ borderTopColor: iconBg,
169
+ borderRightColor: iconBg,
170
+ animation: 'mcp-spin 1.2s linear infinite',
171
+ });
172
+ iconContainer.appendChild(spinner);
173
+ icon.textContent = '⋯';
174
+ icon.style.animation = 'mcp-pulse 2s ease-in-out infinite';
175
+ }
176
+ else {
177
+ icon.textContent = error ? '✕' : complete ? '✓' : '⋯';
178
+ }
179
+ iconContainer.appendChild(icon);
180
+ // Create title
181
+ const title = document.createElement('h2');
182
+ Object.assign(title.style, {
183
+ margin: '0 0 12px',
184
+ fontSize: '20px',
185
+ fontWeight: '600',
186
+ color: '#242424',
187
+ });
188
+ title.textContent = message;
189
+ // Create detail text
190
+ const detailEl = document.createElement('p');
191
+ detailEl.id = detailId;
192
+ Object.assign(detailEl.style, {
193
+ margin: '0',
194
+ fontSize: '14px',
195
+ color: '#616161',
196
+ lineHeight: '1.5',
197
+ minHeight: '21px', // Prevent layout shift
198
+ });
199
+ if (animated) {
200
+ detailEl.style.animation = `mcp-fade ${cycleInterval}ms ease-in-out infinite`;
201
+ }
202
+ detailEl.textContent = detail;
203
+ // Assemble and append
204
+ modal.appendChild(iconContainer);
205
+ modal.appendChild(title);
206
+ modal.appendChild(detailEl);
207
+ overlay.appendChild(modal);
208
+ document.body.appendChild(overlay);
209
+ // Set up detail cycling for animated states
210
+ if (animated && cycleDetails && cycleDetails.length > 0) {
211
+ let detailIndex = 0;
212
+ const cycleTimer = setInterval(() => {
213
+ const el = document.getElementById(detailId);
214
+ if (el) {
215
+ el.textContent = cycleDetails[detailIndex];
216
+ detailIndex = (detailIndex + 1) % cycleDetails.length;
217
+ }
218
+ else {
219
+ clearInterval(cycleTimer);
220
+ }
221
+ }, cycleInterval);
222
+ // Store timer ID on overlay for potential future cleanup
223
+ overlay._cycleTimer = cycleTimer;
224
+ }
225
+ }, {
226
+ id: PROGRESS_OVERLAY_ID,
227
+ detailId: DETAIL_ELEMENT_ID,
228
+ message: content.message,
229
+ detail: content.detail,
230
+ complete: isComplete,
231
+ error: isError,
232
+ animated: isAnimated,
233
+ cycleDetails: isAnimated ? ACQUIRING_DETAILS : [],
234
+ cycleInterval: DETAIL_CYCLE_INTERVAL_MS,
235
+ });
236
+ // Pause if requested (for steps that need user to see the message)
237
+ if (options.pause) {
238
+ const pauseMs = isComplete ? OVERLAY_COMPLETE_PAUSE_MS : OVERLAY_STEP_PAUSE_MS;
239
+ await page.waitForTimeout(pauseMs);
240
+ }
241
+ }
242
+ catch {
243
+ // Overlay is cosmetic - don't fail login if it can't be shown
244
+ // To debug: change to `catch (e)` and add `console.debug('[overlay]', e);`
245
+ }
246
+ }
247
+ // ─────────────────────────────────────────────────────────────────────────────
248
+ // Authentication Detection
249
+ // ─────────────────────────────────────────────────────────────────────────────
250
+ // URLs that indicate we're in a login flow
251
+ const LOGIN_URL_PATTERNS = [
252
+ 'login.microsoftonline.com',
253
+ 'login.live.com',
254
+ 'login.microsoft.com',
255
+ ];
256
+ // Selectors that indicate successful authentication
257
+ const AUTH_SUCCESS_SELECTORS = [
258
+ '[data-tid="app-bar"]',
259
+ '[data-tid="search-box"]',
260
+ 'input[placeholder*="Search"]',
261
+ '[data-tid="chat-list"]',
262
+ '[data-tid="team-list"]',
263
+ ];
264
+ /**
265
+ * Checks if the current page URL indicates a login flow.
266
+ */
267
+ function isLoginUrl(url) {
268
+ return LOGIN_URL_PATTERNS.some(pattern => url.includes(pattern));
269
+ }
270
+ /**
271
+ * Checks if the page shows authenticated Teams content.
272
+ */
273
+ async function hasAuthenticatedContent(page) {
274
+ for (const selector of AUTH_SUCCESS_SELECTORS) {
275
+ try {
276
+ const count = await page.locator(selector).count();
277
+ if (count > 0) {
278
+ return true;
279
+ }
280
+ }
281
+ catch {
282
+ // Selector not found, continue checking others
283
+ }
284
+ }
285
+ return false;
286
+ }
287
+ /**
288
+ * Triggers MSAL to acquire the Substrate token.
289
+ *
290
+ * MSAL only acquires tokens for specific scopes when the app makes API calls
291
+ * requiring those scopes. The Substrate API is only used for search, so we
292
+ * perform a minimal search ("is:Messages") to trigger token acquisition.
293
+ */
294
+ async function triggerTokenAcquisition(page, log) {
295
+ log('Triggering token acquisition...');
296
+ try {
297
+ // Wait for the app to be ready
298
+ await page.waitForTimeout(5000);
299
+ // Try multiple methods to trigger search
300
+ let searchTriggered = false;
301
+ // Method 1: Navigate to search results URL (triggers Substrate API call directly)
302
+ log('Navigating to search results...');
303
+ try {
304
+ await page.goto('https://teams.microsoft.com/v2/#/search?query=test', {
305
+ waitUntil: 'domcontentloaded',
306
+ timeout: 30000,
307
+ });
308
+ await page.waitForTimeout(5000);
309
+ searchTriggered = true;
310
+ log('Search results page loaded.');
311
+ }
312
+ catch (e) {
313
+ log(`Search navigation failed: ${e instanceof Error ? e.message : String(e)}`);
314
+ }
315
+ // Method 2: Fallback - focus and type
316
+ if (!searchTriggered) {
317
+ log('Trying focus+type fallback...');
318
+ try {
319
+ const focused = await page.evaluate(() => {
320
+ const selectors = [
321
+ '#ms-searchux-input',
322
+ '[data-tid="searchInputField"]',
323
+ 'input[placeholder*="Search"]',
324
+ ];
325
+ for (const sel of selectors) {
326
+ const el = document.querySelector(sel);
327
+ if (el) {
328
+ el.focus();
329
+ el.click();
330
+ return true;
331
+ }
332
+ }
333
+ return false;
334
+ });
335
+ if (focused) {
336
+ await page.waitForTimeout(500);
337
+ await page.keyboard.type('test', { delay: 30 });
338
+ await page.keyboard.press('Enter');
339
+ searchTriggered = true;
340
+ log('Search submitted via typing.');
341
+ }
342
+ }
343
+ catch {
344
+ // Continue
345
+ }
346
+ }
347
+ // Method 3: Keyboard shortcut fallback
348
+ if (!searchTriggered) {
349
+ log('Trying keyboard shortcut...');
350
+ const isMac = process.platform === 'darwin';
351
+ await page.keyboard.press(isMac ? 'Meta+e' : 'Control+e');
352
+ await page.waitForTimeout(1000);
353
+ await page.keyboard.type('is:Messages', { delay: 30 });
354
+ await page.keyboard.press('Enter');
355
+ searchTriggered = true;
356
+ }
357
+ // Wait for the Substrate search API call to complete
358
+ log('Waiting for search API...');
359
+ try {
360
+ // Wait for the actual API request to substrate.office.com
361
+ await Promise.race([
362
+ page.waitForResponse(resp => resp.url().includes('substrate.office.com') && resp.status() === 200, { timeout: 15000 }),
363
+ page.waitForTimeout(15000),
364
+ ]);
365
+ log('Substrate API call detected.');
366
+ }
367
+ catch {
368
+ log('No Substrate API call detected, continuing...');
369
+ }
370
+ await page.waitForTimeout(2000);
371
+ // Close search and reset
372
+ await page.keyboard.press('Escape');
373
+ await page.waitForTimeout(1000);
374
+ log('Token acquisition complete.');
375
+ }
376
+ catch (error) {
377
+ log(`Token acquisition warning: ${error instanceof Error ? error.message : String(error)}`);
378
+ await page.waitForTimeout(3000);
379
+ }
380
+ }
381
+ /**
382
+ * Gets the current authentication status.
383
+ */
384
+ export async function getAuthStatus(page) {
385
+ const currentUrl = page.url();
386
+ const onLoginPage = isLoginUrl(currentUrl);
387
+ // If on login page, definitely not authenticated
388
+ if (onLoginPage) {
389
+ return {
390
+ isAuthenticated: false,
391
+ isOnLoginPage: true,
392
+ currentUrl,
393
+ };
394
+ }
395
+ // If on Teams domain, check for authenticated content
396
+ if (currentUrl.includes('teams.microsoft.com')) {
397
+ const hasContent = await hasAuthenticatedContent(page);
398
+ return {
399
+ isAuthenticated: hasContent,
400
+ isOnLoginPage: false,
401
+ currentUrl,
402
+ };
403
+ }
404
+ // Unknown state
405
+ return {
406
+ isAuthenticated: false,
407
+ isOnLoginPage: false,
408
+ currentUrl,
409
+ };
410
+ }
411
+ /** Timeout for detecting login redirect (ms). */
412
+ const LOGIN_REDIRECT_TIMEOUT_MS = 5000;
413
+ /** URL patterns that indicate we're on a Teams page (not redirected elsewhere). */
414
+ const TEAMS_URL_PATTERNS = [
415
+ 'teams.microsoft.com',
416
+ 'teams.microsoft.us', // GCC-High
417
+ 'dod.teams.microsoft.us', // DoD
418
+ 'teams.cloud.microsoft', // New Teams URL
419
+ ];
420
+ /**
421
+ * Checks if a URL is a Teams domain.
422
+ */
423
+ function isTeamsUrl(url) {
424
+ return TEAMS_URL_PATTERNS.some(pattern => url.includes(pattern));
425
+ }
426
+ /**
427
+ * Navigates to Teams and checks authentication status.
428
+ *
429
+ * Uses a fast redirect-based detection: if we're not redirected to a login
430
+ * page within a few seconds, the session is valid. This is much faster than
431
+ * waiting for the full Teams SPA to render (which can take 30+ seconds).
432
+ *
433
+ * Returns isAuthenticated: false if we can't confirm we're on Teams, to avoid
434
+ * silently failing with an invisible browser stuck on an unexpected page.
435
+ */
436
+ export async function navigateToTeams(page) {
437
+ // Set up a promise that resolves when we detect a login redirect
438
+ let redirectDetected = false;
439
+ // Handler for detecting login redirects
440
+ const handleFrameNavigated = (frame) => {
441
+ if (frame === page.mainFrame() && isLoginUrl(frame.url())) {
442
+ redirectDetected = true;
443
+ }
444
+ };
445
+ // Listen for navigation events
446
+ page.on('framenavigated', handleFrameNavigated);
447
+ try {
448
+ // Navigate to Teams
449
+ await page.goto(TEAMS_URL, { waitUntil: 'domcontentloaded' });
450
+ // Wait for either:
451
+ // 1. A redirect to login page (detected via framenavigated)
452
+ // 2. Timeout expires (no redirect = session valid)
453
+ //
454
+ // Research shows login redirect happens ~3-4 seconds after navigation
455
+ // when session is invalid (MSAL tries silent auth first, then redirects).
456
+ // 5 seconds gives enough buffer while still being fast.
457
+ const startTime = Date.now();
458
+ while (Date.now() - startTime < LOGIN_REDIRECT_TIMEOUT_MS) {
459
+ if (redirectDetected)
460
+ break;
461
+ await page.waitForTimeout(100); // Check every 100ms
462
+ }
463
+ }
464
+ finally {
465
+ // Clean up listener to avoid memory leaks
466
+ page.off('framenavigated', handleFrameNavigated);
467
+ }
468
+ // Check final state
469
+ const currentUrl = page.url();
470
+ // Definitely on login page
471
+ if (redirectDetected || isLoginUrl(currentUrl)) {
472
+ return {
473
+ isAuthenticated: false,
474
+ isOnLoginPage: true,
475
+ currentUrl,
476
+ };
477
+ }
478
+ // Verify we're actually on a Teams page (not some unexpected redirect)
479
+ // If we ended up somewhere unexpected, treat as unauthenticated to avoid
480
+ // silently failing with a headless browser stuck on the wrong page
481
+ if (!isTeamsUrl(currentUrl)) {
482
+ return {
483
+ isAuthenticated: false,
484
+ isOnLoginPage: false, // Not on login, but also not on Teams
485
+ currentUrl,
486
+ };
487
+ }
488
+ // On a Teams URL and no redirect to login = session is valid
489
+ return {
490
+ isAuthenticated: true,
491
+ isOnLoginPage: false,
492
+ currentUrl,
493
+ };
494
+ }
495
+ /**
496
+ * Waits for the user to complete manual authentication.
497
+ * Returns when authenticated or throws after timeout.
498
+ *
499
+ * @param page - The page to monitor
500
+ * @param context - Browser context for saving session
501
+ * @param timeoutMs - Maximum time to wait (default: 5 minutes)
502
+ * @param onProgress - Callback for progress updates
503
+ * @param showOverlay - Whether to show progress overlay (default: true for visible browsers)
504
+ */
505
+ export async function waitForManualLogin(page, context, timeoutMs = 5 * 60 * 1000, onProgress, showOverlay = true) {
506
+ const startTime = Date.now();
507
+ const log = onProgress ?? console.log;
508
+ log('Waiting for manual login...');
509
+ while (Date.now() - startTime < timeoutMs) {
510
+ const status = await getAuthStatus(page);
511
+ if (status.isAuthenticated) {
512
+ log('Authentication successful!');
513
+ // Show progress through login steps (only if overlay enabled)
514
+ if (showOverlay) {
515
+ await showLoginProgress(page, 'signed-in', { pause: true });
516
+ await showLoginProgress(page, 'acquiring');
517
+ }
518
+ // Trigger a search to cause MSAL to acquire the Substrate token
519
+ await triggerTokenAcquisition(page, log);
520
+ if (showOverlay) {
521
+ await showLoginProgress(page, 'saving');
522
+ }
523
+ // Save the session state with fresh tokens
524
+ await saveSessionState(context);
525
+ log('Session state saved.');
526
+ if (showOverlay) {
527
+ await showLoginProgress(page, 'complete', { pause: true });
528
+ }
529
+ return;
530
+ }
531
+ // Check every 2 seconds
532
+ await page.waitForTimeout(2000);
533
+ }
534
+ // Show error overlay before throwing (only if overlay enabled)
535
+ if (showOverlay) {
536
+ await showLoginProgress(page, 'error', { pause: true });
537
+ }
538
+ throw new Error('Authentication timeout: user did not complete login within the allowed time');
539
+ }
540
+ /**
541
+ * Performs a full authentication flow:
542
+ * 1. Navigate to Teams
543
+ * 2. Check if already authenticated
544
+ * 3. If not, wait for manual login (or throw if headless)
545
+ *
546
+ * @param page - The page to use
547
+ * @param context - Browser context for session management
548
+ * @param onProgress - Callback for progress updates
549
+ * @param showOverlay - Whether to show progress overlay (default: true for visible browsers)
550
+ * @param headless - If true, throw immediately if user interaction is required (default: false)
551
+ */
552
+ export async function ensureAuthenticated(page, context, onProgress, showOverlay = true, headless = false) {
553
+ const log = onProgress ?? console.log;
554
+ log('Navigating to Teams...');
555
+ const status = await navigateToTeams(page);
556
+ if (status.isAuthenticated) {
557
+ log('Already authenticated.');
558
+ if (showOverlay) {
559
+ await showLoginProgress(page, 'refreshing');
560
+ }
561
+ // Trigger a search to cause MSAL to acquire/refresh the Substrate token
562
+ await triggerTokenAcquisition(page, log);
563
+ if (showOverlay) {
564
+ await showLoginProgress(page, 'saving');
565
+ }
566
+ // Save the session state with fresh tokens
567
+ await saveSessionState(context);
568
+ if (showOverlay) {
569
+ await showLoginProgress(page, 'complete', { pause: true });
570
+ }
571
+ return;
572
+ }
573
+ // User interaction required - fail fast if headless
574
+ if (headless) {
575
+ const reason = status.isOnLoginPage
576
+ ? 'Login page detected - user credentials required'
577
+ : `Unexpected page state: ${status.currentUrl}`;
578
+ throw new Error(`Headless SSO failed: ${reason}`);
579
+ }
580
+ if (status.isOnLoginPage) {
581
+ log('Login required. Please complete authentication in the browser window.');
582
+ await waitForManualLogin(page, context, undefined, onProgress, showOverlay);
583
+ // Navigate back to Teams after login (in case we're on a callback URL)
584
+ await navigateToTeams(page);
585
+ }
586
+ else {
587
+ // Unexpected state - might need manual intervention
588
+ log('Unexpected page state. Waiting for authentication...');
589
+ await waitForManualLogin(page, context, undefined, onProgress, showOverlay);
590
+ }
591
+ }
592
+ /**
593
+ * Forces a new login by clearing session and navigating to Teams.
594
+ */
595
+ export async function forceNewLogin(page, context, onProgress) {
596
+ const log = onProgress ?? console.log;
597
+ log('Starting fresh login...');
598
+ // Clear cookies to force re-authentication
599
+ await context.clearCookies();
600
+ // Navigate and wait for login
601
+ await navigateToTeams(page);
602
+ await waitForManualLogin(page, context, undefined, onProgress);
603
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Playwright browser context management.
3
+ * Creates and manages browser contexts with session persistence.
4
+ *
5
+ * Uses the system's installed Chrome or Edge browser rather than downloading
6
+ * Playwright's bundled Chromium. This significantly reduces install size.
7
+ */
8
+ import { type Browser, type BrowserContext, type Page } from 'playwright';
9
+ export interface BrowserManager {
10
+ browser: Browser;
11
+ context: BrowserContext;
12
+ page: Page;
13
+ isNewSession: boolean;
14
+ }
15
+ export interface CreateBrowserOptions {
16
+ headless?: boolean;
17
+ viewport?: {
18
+ width: number;
19
+ height: number;
20
+ };
21
+ }
22
+ /**
23
+ * Creates a browser context with optional session state restoration.
24
+ *
25
+ * Uses the system's installed Chrome or Edge browser rather than downloading
26
+ * Playwright's bundled Chromium (~180MB savings).
27
+ *
28
+ * @param options - Browser configuration options
29
+ * @returns Browser manager with browser, context, and page
30
+ * @throws Error if system browser is not found (with helpful suggestions)
31
+ */
32
+ export declare function createBrowserContext(options?: CreateBrowserOptions): Promise<BrowserManager>;
33
+ /**
34
+ * Saves the current browser context's session state.
35
+ */
36
+ export declare function saveSessionState(context: BrowserContext): Promise<void>;
37
+ /**
38
+ * Closes the browser and optionally saves session state.
39
+ */
40
+ export declare function closeBrowser(manager: BrowserManager, saveSession?: boolean): Promise<void>;