msteams-mcp 0.30.2 → 0.30.3

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.
@@ -63,11 +63,6 @@ export declare function writeSessionState(state: SessionState): void;
63
63
  * Deletes the session state file.
64
64
  */
65
65
  export declare function clearSessionState(): void;
66
- /**
67
- * Deletes everything stored locally: browser profile (cookies, MSAL caches,
68
- * every account ever signed in), session state and token cache.
69
- */
70
- export declare function clearAllLocalData(): void;
71
66
  /**
72
67
  * Gets the age of the session state in hours.
73
68
  */
@@ -86,13 +86,6 @@ export function clearSessionState() {
86
86
  fs.unlinkSync(SESSION_STATE_PATH);
87
87
  }
88
88
  }
89
- /**
90
- * Deletes everything stored locally: browser profile (cookies, MSAL caches,
91
- * every account ever signed in), session state and token cache.
92
- */
93
- export function clearAllLocalData() {
94
- fs.rmSync(CONFIG_DIR, { recursive: true, force: true });
95
- }
96
89
  /**
97
90
  * Gets the age of the session state in hours.
98
91
  */
@@ -386,7 +386,7 @@ export async function ensureAuthenticated(page, context, onProgress, showOverlay
386
386
  log('Navigating to Teams...');
387
387
  const status = await navigateToTeams(page);
388
388
  if (status.isAuthenticated) {
389
- log('Teams loaded — saving session state.');
389
+ log('Already authenticated — saving session state.');
390
390
  await saveSessionState(context);
391
391
  // Check if the saved tokens are actually valid
392
392
  // Browser session cookies can outlive MSAL tokens (cookies last days, tokens ~1 hour)
@@ -407,11 +407,9 @@ export async function ensureAuthenticated(page, context, onProgress, showOverlay
407
407
  // Still no valid tokens after waiting
408
408
  throw new Error('Headless SSO failed: MSAL token refresh timed out');
409
409
  }
410
- // No redirect to login does not mean signed in: Teams can render with the
411
- // Microsoft SSO session gone and only a dead SPA refresh token in its cache.
412
- // Keep the window open until a live token appears (silently or by sign-in).
413
- log('Tokens expired. Sign in in the browser window if prompted.');
414
- await waitForManualLogin(page, context, onProgress, showOverlay);
410
+ // In visible mode, the tokens might refresh while user interacts, or they can
411
+ // manually complete any prompts. We've saved what we have.
412
+ log('Session state saved (tokens may need refresh).');
415
413
  return;
416
414
  }
417
415
  // User interaction required - fail fast if headless
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  vi.mock('./context.js', () => ({ saveSessionState: vi.fn() }));
3
3
  vi.mock('../auth/token-extractor.js', () => ({ extractSubstrateToken: vi.fn() }));
4
- import { ensureAuthenticated, getAuthStatus, isTeamsUrl } from './auth.js';
4
+ import { getAuthStatus, isTeamsUrl } from './auth.js';
5
5
  /** A page on `url` with no Teams DOM markers and `tokenMinutes` left on MSAL. */
6
6
  function fakePage(url, tokenMinutes) {
7
7
  return {
@@ -43,22 +43,3 @@ describe('getAuthStatus', () => {
43
43
  expect(status).toMatchObject({ isAuthenticated: false, isOnLoginPage: true });
44
44
  });
45
45
  });
46
- describe('ensureAuthenticated', () => {
47
- it('keeps a visible browser open for sign-in when Teams loads with expired tokens', async () => {
48
- // Teams can render without redirecting to login while the Microsoft SSO
49
- // session is gone; only a fresh MSAL token proves the user has signed in.
50
- const tokenMinutes = [-1, -1, 55];
51
- const page = {
52
- goto: async () => null,
53
- waitForURL: async () => { throw new Error('timeout'); },
54
- url: () => 'https://teams.cloud.microsoft/',
55
- evaluate: async () => tokenMinutes.length > 1 ? tokenMinutes.shift() : tokenMinutes[0],
56
- waitForTimeout: async () => { },
57
- };
58
- const messages = [];
59
- await ensureAuthenticated(page, {}, msg => messages.push(msg), false, false);
60
- expect(messages).toContain('Waiting for manual login...');
61
- expect(messages).toContain('Authentication successful!');
62
- expect(tokenMinutes).toEqual([55]);
63
- });
64
- });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Tests for browser channel selection.
3
+ *
4
+ * The server launches into its own profile dir, so the user's default browser
5
+ * is irrelevant - what matters is that every platform has an ordered list of
6
+ * channels to try and ends with a fallback that needs no system browser.
7
+ */
8
+ export {};
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tests for browser channel selection.
3
+ *
4
+ * The server launches into its own profile dir, so the user's default browser
5
+ * is irrelevant - what matters is that every platform has an ordered list of
6
+ * channels to try and ends with a fallback that needs no system browser.
7
+ */
8
+ import { describe, it, expect, afterEach, vi } from 'vitest';
9
+ import { browserChannels } from './context.js';
10
+ /** Runs fn with process.platform stubbed, then restores the real value. */
11
+ function withPlatform(platform, fn) {
12
+ const original = Object.getOwnPropertyDescriptor(process, 'platform');
13
+ Object.defineProperty(process, 'platform', { value: platform, configurable: true });
14
+ try {
15
+ return fn();
16
+ }
17
+ finally {
18
+ Object.defineProperty(process, 'platform', original);
19
+ }
20
+ }
21
+ describe('browserChannels', () => {
22
+ afterEach(() => {
23
+ vi.unstubAllEnvs();
24
+ });
25
+ it('prefers Edge on Windows, Chrome elsewhere', () => {
26
+ vi.stubEnv('TEAMS_BROWSER_CHANNEL', '');
27
+ expect(withPlatform('win32', browserChannels)[0]).toBe('msedge');
28
+ expect(withPlatform('darwin', browserChannels)[0]).toBe('chrome');
29
+ expect(withPlatform('linux', browserChannels)[0]).toBe('chrome');
30
+ });
31
+ it('offers both system channels on every platform', () => {
32
+ vi.stubEnv('TEAMS_BROWSER_CHANNEL', '');
33
+ for (const platform of ['win32', 'darwin', 'linux']) {
34
+ const channels = withPlatform(platform, browserChannels);
35
+ expect(channels).toContain('chrome');
36
+ expect(channels).toContain('msedge');
37
+ }
38
+ });
39
+ it('ends with bundled Chromium so a machine with no system browser can recover', () => {
40
+ vi.stubEnv('TEAMS_BROWSER_CHANNEL', '');
41
+ for (const platform of ['win32', 'darwin', 'linux']) {
42
+ expect(withPlatform(platform, browserChannels).at(-1)).toBeUndefined();
43
+ }
44
+ });
45
+ it('honours TEAMS_BROWSER_CHANNEL and disables the fallback', () => {
46
+ vi.stubEnv('TEAMS_BROWSER_CHANNEL', 'msedge');
47
+ expect(withPlatform('darwin', browserChannels)).toEqual(['msedge']);
48
+ vi.stubEnv('TEAMS_BROWSER_CHANNEL', 'chrome');
49
+ expect(withPlatform('win32', browserChannels)).toEqual(['chrome']);
50
+ });
51
+ it('ignores an unrecognised TEAMS_BROWSER_CHANNEL', () => {
52
+ vi.stubEnv('TEAMS_BROWSER_CHANNEL', 'safari');
53
+ expect(withPlatform('darwin', browserChannels)).toEqual(['chrome', 'msedge', undefined]);
54
+ });
55
+ });
package/dist/cli.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
@@ -5,12 +5,12 @@ import { createRequire } from 'module';
5
5
  import { z } from 'zod';
6
6
  const require = createRequire(import.meta.url);
7
7
  const pkg = require('../../package.json');
8
- import { hasSessionState, isSessionLikelyExpired, clearAllLocalData, } from '../auth/session-store.js';
8
+ import { hasSessionState, isSessionLikelyExpired, clearSessionState, clearTokenCache, } from '../auth/session-store.js';
9
9
  import { getSubstrateTokenStatus, getMessageAuthStatus, extractMessageAuth, extractCsaToken, extractSubstrateToken, extractSkypeSpacesToken, } from '../auth/token-extractor.js';
10
10
  import { refreshTokensViaHttp } from '../auth/token-refresh-http.js';
11
11
  import { createBrowserContext, closeBrowser } from '../browser/context.js';
12
12
  import * as log from '../utils/logger.js';
13
- import { resetAssignmentsAvailability, clearRegionCache } from '../utils/auth-guards.js';
13
+ import { resetAssignmentsAvailability } from '../utils/auth-guards.js';
14
14
  import { ensureAuthenticated } from '../browser/auth.js';
15
15
  // ─────────────────────────────────────────────────────────────────────────────
16
16
  // Schemas
@@ -29,7 +29,7 @@ const loginToolDefinition = {
29
29
  properties: {
30
30
  forceNew: {
31
31
  type: 'boolean',
32
- description: 'Delete all previous login data (browser profile, cookies, saved session, every previously signed-in account) and sign in from scratch in a visible browser. Use to switch accounts or recover a broken session (default: false)',
32
+ description: 'Force a new login even if a session exists (default: false)',
33
33
  },
34
34
  },
35
35
  },
@@ -76,32 +76,36 @@ async function handleLogin(input) {
76
76
  // A fresh login may have changed what the account can access (e.g. new consent)
77
77
  resetAssignmentsAvailability();
78
78
  if (input.forceNew) {
79
- // Start from nothing: no profile, cookies, MSAL cache or session from any
80
- // earlier account can survive to be mixed into the new login.
81
- clearAllLocalData();
82
- clearRegionCache();
83
- return loginInVisibleBrowser();
79
+ clearSessionState();
80
+ clearTokenCache();
84
81
  }
85
- // Fast path: if tokens are still valid, skip browser entirely
86
- const tokenStatus = getSubstrateTokenStatus();
87
- if (tokenStatus.hasToken && tokenStatus.minutesRemaining >= TOKEN_VALID_THRESHOLD_MINUTES) {
88
- await fillCoreCredentialGaps();
89
- return {
90
- success: true,
91
- data: {
92
- message: `Already authenticated. Token valid for ${tokenStatus.minutesRemaining} more minutes.`,
93
- tokenStatus: {
94
- expiresAt: tokenStatus.expiresAt,
95
- minutesRemaining: tokenStatus.minutesRemaining,
82
+ else {
83
+ // Fast path: if tokens are still valid, skip browser entirely
84
+ const tokenStatus = getSubstrateTokenStatus();
85
+ if (tokenStatus.hasToken && tokenStatus.minutesRemaining >= TOKEN_VALID_THRESHOLD_MINUTES) {
86
+ await fillCoreCredentialGaps();
87
+ return {
88
+ success: true,
89
+ data: {
90
+ message: `Already authenticated. Token valid for ${tokenStatus.minutesRemaining} more minutes.`,
91
+ tokenStatus: {
92
+ expiresAt: tokenStatus.expiresAt,
93
+ minutesRemaining: tokenStatus.minutesRemaining,
94
+ },
96
95
  },
97
- },
98
- };
96
+ };
97
+ }
99
98
  }
100
99
  // Headless-first strategy:
101
100
  // The persistent browser profile retains Microsoft's long-lived session cookies,
102
- // so headless SSO can succeed even without a session-state file.
101
+ // so headless SSO can succeed even without a session-state file. Always try
102
+ // headless first — even for forceNew. Most recovery scenarios complete silently.
103
103
  const headless = await createBrowserContext({ headless: true });
104
104
  try {
105
+ if (input.forceNew) {
106
+ // Clear persistent profile cookies to force fresh authentication
107
+ await headless.context.clearCookies();
108
+ }
105
109
  await ensureAuthenticated(headless.page, headless.context, (msg) => log.info('login:headless', msg), false, // No overlay in headless
106
110
  true // Headless mode - throw immediately if user interaction required
107
111
  );
@@ -118,10 +122,7 @@ async function handleLogin(input) {
118
122
  log.warn('login:headless', `Headless SSO failed, falling back to visible browser: ${error instanceof Error ? error.message : String(error)}`);
119
123
  await closeBrowser(headless, false).catch(() => { });
120
124
  }
121
- return loginInVisibleBrowser();
122
- }
123
- /** Opens a visible browser for the user to sign in, then saves the session. */
124
- async function loginInVisibleBrowser() {
125
+ // Open visible browser for user interaction
125
126
  const visible = await createBrowserContext({ headless: false });
126
127
  try {
127
128
  await ensureAuthenticated(visible.page, visible.context, (msg) => log.info('login', msg));
@@ -2,7 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  vi.mock('../auth/session-store.js', () => ({
3
3
  hasSessionState: vi.fn(() => true),
4
4
  isSessionLikelyExpired: vi.fn(() => false),
5
- clearAllLocalData: vi.fn(),
5
+ clearSessionState: vi.fn(),
6
+ clearTokenCache: vi.fn(),
6
7
  }));
7
8
  vi.mock('../auth/token-extractor.js', () => ({
8
9
  getSubstrateTokenStatus: vi.fn(),
@@ -15,9 +16,8 @@ vi.mock('../auth/token-extractor.js', () => ({
15
16
  vi.mock('../auth/token-refresh-http.js', () => ({ refreshTokensViaHttp: vi.fn() }));
16
17
  vi.mock('../browser/context.js', () => ({ createBrowserContext: vi.fn(), closeBrowser: vi.fn() }));
17
18
  vi.mock('../browser/auth.js', () => ({ ensureAuthenticated: vi.fn() }));
18
- vi.mock('../utils/auth-guards.js', () => ({ resetAssignmentsAvailability: vi.fn(), clearRegionCache: vi.fn() }));
19
+ vi.mock('../utils/auth-guards.js', () => ({ resetAssignmentsAvailability: vi.fn() }));
19
20
  import { loginTool } from './auth-tools.js';
20
- import { clearAllLocalData } from '../auth/session-store.js';
21
21
  import { getSubstrateTokenStatus, extractMessageAuth, extractCsaToken, extractSubstrateToken, extractSkypeSpacesToken, } from '../auth/token-extractor.js';
22
22
  import { refreshTokensViaHttp } from '../auth/token-refresh-http.js';
23
23
  import { createBrowserContext, closeBrowser } from '../browser/context.js';
@@ -49,26 +49,12 @@ describe('teams_login core credential top-up', () => {
49
49
  });
50
50
  it('tops up after a forced browser login leaves the CSA token behind', async () => {
51
51
  sessionMissingCsaToken();
52
- vi.mocked(createBrowserContext).mockResolvedValue({ page: {}, context: {} });
52
+ vi.mocked(createBrowserContext).mockResolvedValue({ page: {}, context: { clearCookies: vi.fn() } });
53
53
  vi.mocked(closeBrowser).mockResolvedValue(undefined);
54
54
  const result = await loginTool.handler({ forceNew: true });
55
55
  expect(result.success).toBe(true);
56
56
  expect(refreshTokensViaHttp).toHaveBeenCalledOnce();
57
57
  });
58
- it('forceNew wipes all previous login data and signs in from scratch in a visible browser', async () => {
59
- sessionMissingCsaToken();
60
- vi.mocked(extractCsaToken).mockReturnValue('csa');
61
- vi.mocked(createBrowserContext).mockResolvedValue({ page: {}, context: {} });
62
- vi.mocked(closeBrowser).mockResolvedValue(undefined);
63
- await loginTool.handler({ forceNew: true });
64
- expect(clearAllLocalData).toHaveBeenCalledOnce();
65
- expect(vi.mocked(clearAllLocalData).mock.invocationCallOrder[0])
66
- .toBeLessThan(vi.mocked(createBrowserContext).mock.invocationCallOrder[0]);
67
- // Valid-looking tokens from the old session must not short-circuit the login,
68
- // and a wiped profile has nothing for headless SSO to use.
69
- expect(createBrowserContext).toHaveBeenCalledOnce();
70
- expect(createBrowserContext).toHaveBeenCalledWith({ headless: false });
71
- });
72
58
  it('still reports success when the top-up fails', async () => {
73
59
  sessionMissingCsaToken();
74
60
  vi.mocked(refreshTokensViaHttp).mockResolvedValue({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msteams-mcp",
3
- "version": "0.30.2",
3
+ "version": "0.30.3",
4
4
  "description": "MCP server for Microsoft Teams - search messages, send replies, manage favourites",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,10 +0,0 @@
1
- /**
2
- * API module exports.
3
- */
4
- export * from './substrate-api.js';
5
- export * from './chatsvc-api.js';
6
- export * from './csa-api.js';
7
- export * from './calendar-api.js';
8
- export * from './transcript-api.js';
9
- export * from './files-api.js';
10
- export * from './tags-api.js';
package/dist/api/index.js DELETED
@@ -1,10 +0,0 @@
1
- /**
2
- * API module exports.
3
- */
4
- export * from './substrate-api.js';
5
- export * from './chatsvc-api.js';
6
- export * from './csa-api.js';
7
- export * from './calendar-api.js';
8
- export * from './transcript-api.js';
9
- export * from './files-api.js';
10
- export * from './tags-api.js';
@@ -1,7 +0,0 @@
1
- /**
2
- * Auth module exports.
3
- */
4
- export * from './session-store.js';
5
- export * from './token-extractor.js';
6
- export * from './token-refresh.js';
7
- export { encrypt, decrypt, isEncrypted, type EncryptedData } from './crypto.js';
@@ -1,7 +0,0 @@
1
- /**
2
- * Auth module exports.
3
- */
4
- export * from './session-store.js';
5
- export * from './token-extractor.js';
6
- export * from './token-refresh.js';
7
- export { encrypt, decrypt, isEncrypted } from './crypto.js';
@@ -1,10 +0,0 @@
1
- /**
2
- * Research script to understand Teams authentication redirect behaviour.
3
- *
4
- * This script observes:
5
- * 1. What happens when navigating to Teams WITH a valid session
6
- * 2. What happens when navigating to Teams WITHOUT a session
7
- *
8
- * Goal: Understand the fastest way to detect if we're authenticated.
9
- */
10
- export {};
@@ -1,175 +0,0 @@
1
- /**
2
- * Research script to understand Teams authentication redirect behaviour.
3
- *
4
- * This script observes:
5
- * 1. What happens when navigating to Teams WITH a valid session
6
- * 2. What happens when navigating to Teams WITHOUT a session
7
- *
8
- * Goal: Understand the fastest way to detect if we're authenticated.
9
- */
10
- import { chromium } from 'playwright';
11
- import { hasSessionState } from '../auth/session-store.js';
12
- const TEAMS_URL = 'https://teams.microsoft.com';
13
- async function observeNavigation(page, scenario, startTime) {
14
- const events = [];
15
- const log = (type, url, extra) => {
16
- const now = Date.now();
17
- const event = {
18
- timestamp: now,
19
- elapsed: now - startTime,
20
- type,
21
- url,
22
- ...extra,
23
- };
24
- events.push(event);
25
- console.log(` [${event.elapsed}ms] ${type}: ${url}${extra?.status ? ` (${extra.status})` : ''}${extra?.note ? ` - ${extra.note}` : ''}`);
26
- };
27
- // Track all frame navigations
28
- page.on('framenavigated', (frame) => {
29
- if (frame === page.mainFrame()) {
30
- const url = frame.url();
31
- const isLogin = url.includes('login.microsoftonline.com') ||
32
- url.includes('login.live.com') ||
33
- url.includes('login.microsoft.com');
34
- const isTeams = url.includes('teams.microsoft.com') ||
35
- url.includes('teams.microsoft.us');
36
- log('navigation', url, {
37
- note: isLogin ? 'LOGIN PAGE' : isTeams ? 'TEAMS' : undefined
38
- });
39
- }
40
- });
41
- // Track responses (especially redirects)
42
- page.on('response', (response) => {
43
- const url = response.url();
44
- const status = response.status();
45
- // Only log main document responses and redirects
46
- if (response.request().resourceType() === 'document' ||
47
- (status >= 300 && status < 400)) {
48
- log('response', url, {
49
- status,
50
- note: status >= 300 && status < 400 ? 'REDIRECT' : undefined
51
- });
52
- }
53
- });
54
- return events;
55
- }
56
- async function runScenario(browser, scenario, useSession) {
57
- console.log(`\n${'='.repeat(60)}`);
58
- console.log(`SCENARIO: ${scenario}`);
59
- console.log(`Session: ${useSession ? 'WITH existing session' : 'NO session (cleared)'}`);
60
- console.log('='.repeat(60));
61
- let context;
62
- if (useSession && hasSessionState()) {
63
- console.log('Loading existing session state...');
64
- // Handle encrypted session
65
- try {
66
- const { readSessionState } = await import('../auth/session-store.js');
67
- const state = readSessionState();
68
- if (state) {
69
- context = await browser.newContext({
70
- storageState: state,
71
- viewport: { width: 1280, height: 800 },
72
- });
73
- }
74
- else {
75
- throw new Error('Could not read session');
76
- }
77
- }
78
- catch {
79
- console.log('Could not load session, using fresh context');
80
- context = await browser.newContext({
81
- viewport: { width: 1280, height: 800 },
82
- });
83
- }
84
- }
85
- else {
86
- console.log('Using fresh context (no session)...');
87
- context = await browser.newContext({
88
- viewport: { width: 1280, height: 800 },
89
- });
90
- }
91
- const page = await context.newPage();
92
- const startTime = Date.now();
93
- console.log(`\nNavigating to ${TEAMS_URL}...`);
94
- console.log('Watching for navigations and redirects:\n');
95
- const events = await observeNavigation(page, scenario, startTime);
96
- try {
97
- // Navigate and wait for initial load
98
- await page.goto(TEAMS_URL, {
99
- waitUntil: 'domcontentloaded',
100
- timeout: 30000,
101
- });
102
- const afterGoto = Date.now();
103
- console.log(`\n [${afterGoto - startTime}ms] goto() completed (domcontentloaded)`);
104
- // Wait a bit more to see if any late redirects happen
105
- console.log('\n Waiting 10 seconds to observe any delayed redirects...\n');
106
- for (let i = 1; i <= 10; i++) {
107
- await page.waitForTimeout(1000);
108
- const currentUrl = page.url();
109
- const isLogin = currentUrl.includes('login.microsoftonline.com') ||
110
- currentUrl.includes('login.live.com') ||
111
- currentUrl.includes('login.microsoft.com');
112
- const isTeams = currentUrl.includes('teams.microsoft.com') ||
113
- currentUrl.includes('teams.microsoft.us');
114
- console.log(` [${Date.now() - startTime}ms] Check ${i}: ${isLogin ? 'ON LOGIN' : isTeams ? 'ON TEAMS' : 'OTHER'} - ${currentUrl.substring(0, 80)}...`);
115
- // If we're on login page, we know we're not authenticated
116
- if (isLogin) {
117
- console.log('\n ✓ Detected redirect to login page - NOT authenticated');
118
- break;
119
- }
120
- }
121
- const finalUrl = page.url();
122
- const duration = Date.now() - startTime;
123
- console.log(`\n${'─'.repeat(60)}`);
124
- console.log(`RESULT for "${scenario}":`);
125
- console.log(` Final URL: ${finalUrl}`);
126
- console.log(` Total duration: ${duration}ms`);
127
- console.log(` Navigation events: ${events.length}`);
128
- const isOnLogin = finalUrl.includes('login.microsoftonline.com') ||
129
- finalUrl.includes('login.live.com') ||
130
- finalUrl.includes('login.microsoft.com');
131
- const isOnTeams = finalUrl.includes('teams.microsoft.com') ||
132
- finalUrl.includes('teams.microsoft.us');
133
- console.log(` Authenticated: ${isOnTeams && !isOnLogin ? 'YES (on Teams)' : 'NO (on login page)'}`);
134
- return { events, finalUrl, duration };
135
- }
136
- finally {
137
- await context.close();
138
- }
139
- }
140
- async function main() {
141
- console.log('🔬 Teams Authentication Research');
142
- console.log('================================\n');
143
- console.log('This script observes redirect behaviour during Teams navigation.\n');
144
- const browser = await chromium.launch({
145
- headless: false, // Visible so we can see what's happening
146
- });
147
- try {
148
- // Scenario 1: With existing session (if available)
149
- if (hasSessionState()) {
150
- await runScenario(browser, 'WITH SESSION', true);
151
- }
152
- else {
153
- console.log('\n⚠️ No existing session found. Run `npm run cli -- login` first to create one.');
154
- console.log(' Skipping "WITH SESSION" scenario.\n');
155
- }
156
- // Scenario 2: Without session (cleared context)
157
- await runScenario(browser, 'WITHOUT SESSION', false);
158
- console.log('\n\n' + '='.repeat(60));
159
- console.log('SUMMARY');
160
- console.log('='.repeat(60));
161
- console.log(`
162
- Key observations to look for:
163
- 1. How quickly does the redirect to login.microsoftonline.com happen?
164
- 2. Does the redirect happen BEFORE or AFTER domcontentloaded?
165
- 3. Are there any intermediate URLs?
166
- 4. What HTTP status codes are used for redirects?
167
-
168
- Based on this, we can tune LOGIN_REDIRECT_TIMEOUT_MS in auth.ts
169
- `);
170
- }
171
- finally {
172
- await browser.close();
173
- }
174
- }
175
- main().catch(console.error);
@@ -1,11 +0,0 @@
1
- /**
2
- * Research script to explore Teams web app behaviour.
3
- *
4
- * This script:
5
- * 1. Launches a browser with persistent context
6
- * 2. Navigates to Teams and handles authentication
7
- * 3. Monitors network requests to discover API endpoints
8
- * 4. Allows manual interaction to trigger searches
9
- * 5. Logs discovered endpoints and data structures
10
- */
11
- export {};