msteams-mcp 0.25.0 → 0.26.0

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.
@@ -1,294 +0,0 @@
1
- /**
2
- * Import Microsoft SSO cookies from the user's Chrome profile into a Playwright context.
3
- *
4
- * When the Teams MCP server needs to open a visible browser for login,
5
- * the Playwright profile is isolated from the user's real Chrome — so Microsoft
6
- * can't recognise the user via SSO. This module copies the relevant Microsoft
7
- * cookies from the user's actual Chrome work profile, enabling silent SSO in
8
- * the Playwright browser and eliminating the need to re-type credentials.
9
- *
10
- * macOS only (Chrome cookies are encrypted with a Keychain-backed key).
11
- * Fails gracefully on other platforms or when Chrome isn't available.
12
- *
13
- * Configuration:
14
- * TEAMS_MCP_CHROME_PROFILE env var — Chrome profile directory name
15
- * (e.g. "Profile 1"). If unset, auto-detects from Chrome's Local State.
16
- */
17
- import * as crypto from 'crypto';
18
- import * as fs from 'fs';
19
- import * as os from 'os';
20
- import * as path from 'path';
21
- import { execSync } from 'child_process';
22
- import * as log from '../utils/logger.js';
23
- // Microsoft domains whose cookies enable SSO
24
- const MICROSOFT_DOMAINS = [
25
- '%microsoftonline%',
26
- '%login.live.com%',
27
- '%login.microsoft.com%',
28
- '%microsoft.com%',
29
- '%office.com%',
30
- '%office365.com%',
31
- ];
32
- const CHROME_DATA_DIR = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome');
33
- // ─────────────────────────────────────────────────────────────────────────────
34
- // Chrome Profile Detection
35
- // ─────────────────────────────────────────────────────────────────────────────
36
- /**
37
- * Lists Chrome profiles from the Local State file.
38
- */
39
- function listChromeProfiles() {
40
- const localStatePath = path.join(CHROME_DATA_DIR, 'Local State');
41
- if (!fs.existsSync(localStatePath))
42
- return [];
43
- try {
44
- const localState = JSON.parse(fs.readFileSync(localStatePath, 'utf8'));
45
- const infoCache = localState?.profile?.info_cache;
46
- if (!infoCache || typeof infoCache !== 'object')
47
- return [];
48
- return Object.entries(infoCache).map(([dirName, info]) => {
49
- const i = info;
50
- return {
51
- dirName,
52
- name: String(i.name ?? ''),
53
- gaiaName: String(i.gaia_name ?? ''),
54
- };
55
- });
56
- }
57
- catch {
58
- return [];
59
- }
60
- }
61
- /**
62
- * Selects the Chrome profile to import cookies from.
63
- *
64
- * Priority:
65
- * 1. TEAMS_MCP_CHROME_PROFILE env var (exact dir name like "Profile 1")
66
- * 2. Auto-detect: first profile whose name looks like a work/corporate account
67
- * 3. null if no suitable profile found
68
- */
69
- function selectChromeProfile() {
70
- const profiles = listChromeProfiles();
71
- if (profiles.length === 0)
72
- return null;
73
- // Priority 1: explicit env var
74
- const envProfile = process.env.TEAMS_MCP_CHROME_PROFILE;
75
- if (envProfile) {
76
- const match = profiles.find(p => p.dirName === envProfile);
77
- if (match)
78
- return match;
79
- log.warn('cookie-import', `TEAMS_MCP_CHROME_PROFILE="${envProfile}" not found. Available: ${profiles.map(p => `${p.dirName} (${p.name})`).join(', ')}`);
80
- return null;
81
- }
82
- // Priority 2: auto-detect work profile (contains a domain-like name)
83
- const workProfile = profiles.find(p => /\.[a-z]{2,}$/i.test(p.name) || // name contains a domain (e.g. "corp.example.com")
84
- p.name.toLowerCase().includes('work') ||
85
- p.name.toLowerCase().includes('corp'));
86
- if (workProfile)
87
- return workProfile;
88
- // Skip auto-import if we can't identify a work profile
89
- return null;
90
- }
91
- // ─────────────────────────────────────────────────────────────────────────────
92
- // Cookie Decryption (macOS)
93
- // ─────────────────────────────────────────────────────────────────────────────
94
- /**
95
- * Cached AES key derived from the Chrome Safe Storage Keychain password.
96
- * Cached for the lifetime of the MCP server process so the Keychain
97
- * prompt only appears once (on first cookie import after server start).
98
- *
99
- * Tip: click "Always Allow" on the macOS Keychain dialog to permanently
100
- * authorize this process — then no prompt appears at all.
101
- */
102
- let cachedKey = null;
103
- /**
104
- * Gets (or retrieves from cache) the AES key for Chrome cookie decryption.
105
- * Accesses the macOS Keychain only on first call, caches for process lifetime.
106
- */
107
- function getDecryptionKey() {
108
- if (cachedKey)
109
- return cachedKey;
110
- let password;
111
- try {
112
- password = execSync('security find-generic-password -s "Chrome Safe Storage" -w', { encoding: 'utf8', timeout: 5000 }).trim();
113
- }
114
- catch {
115
- return null;
116
- }
117
- cachedKey = crypto.pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1');
118
- return cachedKey;
119
- }
120
- /**
121
- * Decrypts a Chrome cookie value.
122
- * Chrome macOS cookies are prefixed with 'v10' followed by AES-128-CBC ciphertext.
123
- */
124
- function decryptCookieValue(hexValue, key) {
125
- try {
126
- const encrypted = Buffer.from(hexValue, 'hex');
127
- // v10 prefix check (0x76 0x31 0x30)
128
- if (encrypted.length < 4 || encrypted[0] !== 0x76 || encrypted[1] !== 0x31 || encrypted[2] !== 0x30) {
129
- // Not encrypted or unknown format — try as plain text
130
- return encrypted.toString('utf8');
131
- }
132
- const ciphertext = encrypted.subarray(3);
133
- const iv = Buffer.alloc(16, 0x20); // 16 bytes of space (0x20)
134
- const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
135
- decipher.setAutoPadding(true);
136
- const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
137
- return decrypted.toString('utf8');
138
- }
139
- catch {
140
- return null;
141
- }
142
- }
143
- // ─────────────────────────────────────────────────────────────────────────────
144
- // Cookie Reading (via sqlite3 CLI)
145
- // ─────────────────────────────────────────────────────────────────────────────
146
- /**
147
- * Reads Microsoft-related cookies from a Chrome profile's Cookies database.
148
- * Copies the DB to a temp file to avoid locking conflicts with running Chrome.
149
- */
150
- function readChromeCookies(profileDir) {
151
- const cookiesDb = path.join(CHROME_DATA_DIR, profileDir, 'Cookies');
152
- if (!fs.existsSync(cookiesDb))
153
- return [];
154
- // Copy to temp to avoid lock conflicts with running Chrome
155
- const tmpDb = path.join(os.tmpdir(), `teams-mcp-cookies-${Date.now()}.db`);
156
- try {
157
- fs.copyFileSync(cookiesDb, tmpDb);
158
- // Also copy WAL/SHM if they exist (needed for consistent reads)
159
- for (const ext of ['-wal', '-shm']) {
160
- const src = cookiesDb + ext;
161
- if (fs.existsSync(src)) {
162
- fs.copyFileSync(src, tmpDb + ext);
163
- }
164
- }
165
- const whereClause = MICROSOFT_DOMAINS.map(d => `host_key LIKE '${d}'`).join(' OR ');
166
- const sql = `SELECT host_key, name, hex(encrypted_value) as ev, path, expires_utc, is_secure, is_httponly, samesite FROM cookies WHERE (${whereClause}) AND expires_utc > 0`;
167
- const output = execSync(`sqlite3 -separator '|||' "${tmpDb}" "${sql}"`, { encoding: 'utf8', timeout: 10000, maxBuffer: 1024 * 1024 });
168
- return output
169
- .trim()
170
- .split('\n')
171
- .filter(line => line.includes('|||'))
172
- .map(line => {
173
- const [host_key, name, encrypted_value_hex, cookiePath, expires_utc, is_secure, is_httponly, samesite] = line.split('|||');
174
- return {
175
- host_key,
176
- name,
177
- encrypted_value_hex,
178
- path: cookiePath,
179
- expires_utc: parseInt(expires_utc, 10),
180
- is_secure: parseInt(is_secure, 10),
181
- is_httponly: parseInt(is_httponly, 10),
182
- samesite: parseInt(samesite, 10),
183
- };
184
- });
185
- }
186
- catch (err) {
187
- log.warn('cookie-import', `Failed to read Chrome cookies: ${err instanceof Error ? err.message : String(err)}`);
188
- return [];
189
- }
190
- finally {
191
- // Clean up temp files
192
- for (const f of [tmpDb, tmpDb + '-wal', tmpDb + '-shm']) {
193
- try {
194
- fs.unlinkSync(f);
195
- }
196
- catch { /* ignore */ }
197
- }
198
- }
199
- }
200
- // ─────────────────────────────────────────────────────────────────────────────
201
- // Cookie Conversion
202
- // ─────────────────────────────────────────────────────────────────────────────
203
- /**
204
- * Converts Chrome epoch (microseconds since 1601-01-01) to Unix epoch (seconds since 1970-01-01).
205
- */
206
- function chromeEpochToUnix(chromeTimestamp) {
207
- // Chrome epoch starts 11644473600 seconds before Unix epoch
208
- return Math.floor(chromeTimestamp / 1_000_000) - 11644473600;
209
- }
210
- /**
211
- * Maps Chrome's samesite integer to Playwright's string value.
212
- */
213
- function chromeSameSiteToPlaywright(samesite) {
214
- switch (samesite) {
215
- case 2: return 'Strict';
216
- case 1: return 'Lax';
217
- default: return 'None';
218
- }
219
- }
220
- // ─────────────────────────────────────────────────────────────────────────────
221
- // Public API
222
- // ─────────────────────────────────────────────────────────────────────────────
223
- /**
224
- * Imports Microsoft SSO cookies from the user's Chrome profile into a Playwright context.
225
- *
226
- * This enables SSO in the Playwright browser so the user doesn't have to type
227
- * credentials when Microsoft redirects to the login page.
228
- *
229
- * Fails silently — cookie import is best-effort. If it doesn't work,
230
- * the user just has to log in manually as before.
231
- */
232
- export async function importMicrosoftCookies(context) {
233
- // Only supported on macOS
234
- if (process.platform !== 'darwin') {
235
- log.debug('cookie-import', 'Skipping cookie import (not macOS)');
236
- return;
237
- }
238
- // Check if Chrome is installed
239
- if (!fs.existsSync(CHROME_DATA_DIR)) {
240
- log.debug('cookie-import', 'Skipping cookie import (Chrome not found)');
241
- return;
242
- }
243
- const profile = selectChromeProfile();
244
- if (!profile) {
245
- log.debug('cookie-import', 'No Chrome work profile found. Set TEAMS_MCP_CHROME_PROFILE env var.');
246
- return;
247
- }
248
- log.info('cookie-import', `Importing Microsoft cookies from Chrome profile: ${profile.dirName} (${profile.name})`);
249
- // Get decryption key (cached after first access — only one Keychain prompt per server lifetime)
250
- const key = getDecryptionKey();
251
- if (!key) {
252
- log.warn('cookie-import', 'Could not get Chrome Safe Storage password from Keychain. ' +
253
- 'To fix, run: security set-generic-password-partition-list -S "apple-tool:,apple:" ' +
254
- '-a "Chrome" -s "Chrome Safe Storage" ~/Library/Keychains/login.keychain-db');
255
- return;
256
- }
257
- // Read and decrypt cookies
258
- const rawCookies = readChromeCookies(profile.dirName);
259
- if (rawCookies.length === 0) {
260
- log.info('cookie-import', 'No Microsoft cookies found in Chrome profile');
261
- return;
262
- }
263
- const playwrightCookies = [];
264
- for (const raw of rawCookies) {
265
- const value = decryptCookieValue(raw.encrypted_value_hex, key);
266
- if (!value)
267
- continue;
268
- const expires = chromeEpochToUnix(raw.expires_utc);
269
- // Skip expired cookies
270
- if (expires <= Math.floor(Date.now() / 1000))
271
- continue;
272
- playwrightCookies.push({
273
- name: raw.name,
274
- value,
275
- domain: raw.host_key,
276
- path: raw.path,
277
- expires,
278
- httpOnly: raw.is_httponly === 1,
279
- secure: raw.is_secure === 1,
280
- sameSite: chromeSameSiteToPlaywright(raw.samesite),
281
- });
282
- }
283
- if (playwrightCookies.length === 0) {
284
- log.info('cookie-import', 'No valid Microsoft cookies to import');
285
- return;
286
- }
287
- try {
288
- await context.addCookies(playwrightCookies);
289
- log.info('cookie-import', `Imported ${playwrightCookies.length} Microsoft cookies from Chrome "${profile.name}" profile`);
290
- }
291
- catch (err) {
292
- log.warn('cookie-import', `Failed to inject cookies: ${err instanceof Error ? err.message : String(err)}`);
293
- }
294
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,122 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import * as crypto from 'crypto';
3
- /**
4
- * Tests for the cookie import logic.
5
- *
6
- * The public importMicrosoftCookies() function depends on filesystem, Keychain,
7
- * and sqlite3 — so we test the pure cryptographic and conversion logic directly.
8
- */
9
- describe('chrome-cookie-import logic', () => {
10
- describe('cookie decryption (v10 AES-128-CBC)', () => {
11
- // Replicate Chrome macOS encryption: PBKDF2-SHA1, salt='saltysalt', 1003 iters
12
- const password = 'test-password';
13
- const key = crypto.pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1');
14
- const iv = Buffer.alloc(16, 0x20); // 16 spaces
15
- function encrypt(plaintext) {
16
- const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
17
- const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
18
- return Buffer.concat([Buffer.from('v10'), encrypted]).toString('hex');
19
- }
20
- function decrypt(hexValue) {
21
- const buf = Buffer.from(hexValue, 'hex');
22
- if (buf.length < 4 || buf[0] !== 0x76 || buf[1] !== 0x31 || buf[2] !== 0x30) {
23
- return buf.toString('utf8');
24
- }
25
- try {
26
- const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
27
- decipher.setAutoPadding(true);
28
- return Buffer.concat([decipher.update(buf.subarray(3)), decipher.final()]).toString('utf8');
29
- }
30
- catch {
31
- return null;
32
- }
33
- }
34
- it('round-trips a short cookie value', () => {
35
- const value = 'ESTSAUTHPERSISTENT_VALUE_HERE';
36
- expect(decrypt(encrypt(value))).toBe(value);
37
- });
38
- it('round-trips an empty string', () => {
39
- expect(decrypt(encrypt(''))).toBe('');
40
- });
41
- it('round-trips a long value (>1 AES block)', () => {
42
- const value = 'A'.repeat(200);
43
- expect(decrypt(encrypt(value))).toBe(value);
44
- });
45
- it('returns raw string for non-v10 prefix', () => {
46
- const plain = Buffer.from('hello');
47
- expect(decrypt(plain.toString('hex'))).toBe('hello');
48
- });
49
- it('returns null for corrupted ciphertext', () => {
50
- // Valid v10 prefix but garbage data
51
- const garbage = Buffer.concat([Buffer.from('v10'), Buffer.from('not-valid-ciphertext-at-all!!')]);
52
- expect(decrypt(garbage.toString('hex'))).toBeNull();
53
- });
54
- });
55
- describe('Chrome epoch conversion', () => {
56
- function chromeEpochToUnix(chromeTimestamp) {
57
- return Math.floor(chromeTimestamp / 1_000_000) - 11644473600;
58
- }
59
- it('converts a known Chrome timestamp to Unix', () => {
60
- // 2025-01-01T00:00:00Z in Unix = 1735689600
61
- // In Chrome epoch = (1735689600 + 11644473600) * 1_000_000 = 13380163200000000
62
- const chromeTs = 13380163200000000;
63
- expect(chromeEpochToUnix(chromeTs)).toBe(1735689600);
64
- });
65
- it('returns 0 for the Unix epoch in Chrome time', () => {
66
- const chromeTs = 11644473600 * 1_000_000;
67
- expect(chromeEpochToUnix(chromeTs)).toBe(0);
68
- });
69
- });
70
- describe('samesite mapping', () => {
71
- function chromeSameSiteToPlaywright(samesite) {
72
- switch (samesite) {
73
- case 2: return 'Strict';
74
- case 1: return 'Lax';
75
- default: return 'None';
76
- }
77
- }
78
- it('maps Chrome samesite values', () => {
79
- expect(chromeSameSiteToPlaywright(-1)).toBe('None');
80
- expect(chromeSameSiteToPlaywright(0)).toBe('None');
81
- expect(chromeSameSiteToPlaywright(1)).toBe('Lax');
82
- expect(chromeSameSiteToPlaywright(2)).toBe('Strict');
83
- });
84
- });
85
- describe('profile detection heuristics', () => {
86
- const profiles = [
87
- { dirName: 'Default', name: 'Person 1', gaiaName: 'Test' },
88
- { dirName: 'Profile 1', name: 'corp.example.com', gaiaName: 'Jane Smith' },
89
- { dirName: 'Profile 2', name: 'Jane', gaiaName: 'Jane Smith' },
90
- { dirName: 'Profile 4', name: 'Test', gaiaName: '' },
91
- ];
92
- function selectWorkProfile(profiles) {
93
- return profiles.find(p => /\.[a-z]{2,}$/i.test(p.name) ||
94
- p.name.toLowerCase().includes('work') ||
95
- p.name.toLowerCase().includes('corp')) ?? null;
96
- }
97
- it('selects profile with domain-like name', () => {
98
- expect(selectWorkProfile(profiles)?.dirName).toBe('Profile 1');
99
- });
100
- it('selects profile with "work" in name', () => {
101
- const custom = [
102
- { dirName: 'Default', name: 'Personal' },
103
- { dirName: 'Profile 1', name: 'Work Account' },
104
- ];
105
- expect(selectWorkProfile(custom)?.dirName).toBe('Profile 1');
106
- });
107
- it('selects profile with "corp" in name', () => {
108
- const custom = [
109
- { dirName: 'Default', name: 'Me' },
110
- { dirName: 'Profile 1', name: 'CorpNet' },
111
- ];
112
- expect(selectWorkProfile(custom)?.dirName).toBe('Profile 1');
113
- });
114
- it('returns null when no work profile found', () => {
115
- const custom = [
116
- { dirName: 'Default', name: 'Person 1' },
117
- { dirName: 'Profile 2', name: 'Gaming' },
118
- ];
119
- expect(selectWorkProfile(custom)).toBeNull();
120
- });
121
- });
122
- });
@@ -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 {};