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.
- package/dist/auth/session-store.d.ts +0 -5
- package/dist/auth/session-store.js +0 -7
- package/dist/browser/auth.js +4 -6
- package/dist/browser/auth.test.js +1 -20
- package/dist/browser/context.test.d.ts +8 -0
- package/dist/browser/context.test.js +55 -0
- package/dist/cli.js +0 -0
- package/dist/index.js +0 -0
- package/dist/tools/auth-tools.js +27 -26
- package/dist/tools/auth-tools.test.js +4 -18
- package/package.json +1 -1
- package/dist/api/index.d.ts +0 -10
- package/dist/api/index.js +0 -10
- package/dist/auth/index.d.ts +0 -7
- package/dist/auth/index.js +0 -7
- package/dist/research/auth-research.d.ts +0 -10
- package/dist/research/auth-research.js +0 -175
- package/dist/research/explore.d.ts +0 -11
- package/dist/research/explore.js +0 -287
- package/dist/research/search-research.d.ts +0 -17
- package/dist/research/search-research.js +0 -317
- package/dist/test/debug-search.d.ts +0 -10
- package/dist/test/debug-search.js +0 -147
- package/dist/test/mcp-harness.d.ts +0 -17
- package/dist/test/mcp-harness.js +0 -476
|
@@ -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
|
*/
|
package/dist/browser/auth.js
CHANGED
|
@@ -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('
|
|
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
|
-
//
|
|
411
|
-
//
|
|
412
|
-
|
|
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 {
|
|
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
|
package/dist/tools/auth-tools.js
CHANGED
|
@@ -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,
|
|
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
|
|
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: '
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
clearAllLocalData();
|
|
82
|
-
clearRegionCache();
|
|
83
|
-
return loginInVisibleBrowser();
|
|
79
|
+
clearSessionState();
|
|
80
|
+
clearTokenCache();
|
|
84
81
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
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
package/dist/api/index.d.ts
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';
|
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';
|
package/dist/auth/index.d.ts
DELETED
package/dist/auth/index.js
DELETED
|
@@ -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 {};
|