msteams-mcp 0.30.0 → 0.30.2
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/api/index.d.ts +10 -0
- package/dist/api/index.js +10 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/session-store.d.ts +5 -0
- package/dist/auth/session-store.js +7 -0
- package/dist/browser/auth.js +6 -4
- package/dist/browser/auth.test.js +20 -1
- package/dist/cli.js +0 -0
- package/dist/index.js +0 -0
- package/dist/research/auth-research.d.ts +10 -0
- package/dist/research/auth-research.js +175 -0
- package/dist/research/explore.d.ts +11 -0
- package/dist/research/explore.js +287 -0
- package/dist/research/search-research.d.ts +17 -0
- package/dist/research/search-research.js +317 -0
- package/dist/test/debug-search.d.ts +10 -0
- package/dist/test/debug-search.js +147 -0
- package/dist/test/mcp-harness.d.ts +17 -0
- package/dist/test/mcp-harness.js +476 -0
- package/dist/tools/auth-tools.js +26 -27
- package/dist/tools/auth-tools.test.js +18 -4
- package/package.json +1 -1
- package/dist/browser/context.test.d.ts +0 -8
- package/dist/browser/context.test.js +0 -55
|
@@ -0,0 +1,10 @@
|
|
|
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';
|
|
@@ -0,0 +1,10 @@
|
|
|
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';
|
|
@@ -63,6 +63,11 @@ 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;
|
|
66
71
|
/**
|
|
67
72
|
* Gets the age of the session state in hours.
|
|
68
73
|
*/
|
|
@@ -86,6 +86,13 @@ 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
|
+
}
|
|
89
96
|
/**
|
|
90
97
|
* Gets the age of the session state in hours.
|
|
91
98
|
*/
|
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('Teams loaded — 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,9 +407,11 @@ 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
|
-
|
|
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);
|
|
413
415
|
return;
|
|
414
416
|
}
|
|
415
417
|
// 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 { getAuthStatus, isTeamsUrl } from './auth.js';
|
|
4
|
+
import { ensureAuthenticated, 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,3 +43,22 @@ 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
|
+
});
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,175 @@
|
|
|
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);
|
|
@@ -0,0 +1,11 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
import { chromium } from 'playwright';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { PROJECT_ROOT, USER_DATA_DIR, readSessionState, writeSessionState, } from '../auth/session-store.js';
|
|
15
|
+
const FINDINGS_PATH = path.join(PROJECT_ROOT, 'research-findings.json');
|
|
16
|
+
// Track interesting API calls
|
|
17
|
+
const capturedNetworkData = {
|
|
18
|
+
requests: [],
|
|
19
|
+
responses: [],
|
|
20
|
+
};
|
|
21
|
+
// Patterns that indicate interesting API calls
|
|
22
|
+
const SEARCH_PATTERNS = [
|
|
23
|
+
/search/i,
|
|
24
|
+
/query/i,
|
|
25
|
+
/find/i,
|
|
26
|
+
/substrate/i,
|
|
27
|
+
/graph\.microsoft/i,
|
|
28
|
+
/teams.*api/i,
|
|
29
|
+
/chatservice/i,
|
|
30
|
+
/emea\.ng\.msg/i,
|
|
31
|
+
/transcript/i,
|
|
32
|
+
/recording/i,
|
|
33
|
+
/onlineMeeting/i,
|
|
34
|
+
/callRecord/i,
|
|
35
|
+
/\.vtt/i,
|
|
36
|
+
/asyncgw/i,
|
|
37
|
+
/mediaservice/i,
|
|
38
|
+
];
|
|
39
|
+
function isInterestingUrl(url) {
|
|
40
|
+
// Skip static assets, telemetry, and auth-related noise
|
|
41
|
+
const skipPatterns = [
|
|
42
|
+
/\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|ico)(\?|$)/i,
|
|
43
|
+
/telemetry/i,
|
|
44
|
+
/analytics/i,
|
|
45
|
+
/logging/i,
|
|
46
|
+
/beacon/i,
|
|
47
|
+
/\.clarity\./i,
|
|
48
|
+
/fonts\./i,
|
|
49
|
+
/static/i,
|
|
50
|
+
];
|
|
51
|
+
if (skipPatterns.some(pattern => pattern.test(url))) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
// Include if it matches search patterns or is a potential API call
|
|
55
|
+
return SEARCH_PATTERNS.some(pattern => pattern.test(url)) ||
|
|
56
|
+
url.includes('/api/') ||
|
|
57
|
+
url.includes('/v1/') ||
|
|
58
|
+
url.includes('/v2/');
|
|
59
|
+
}
|
|
60
|
+
async function captureRequest(request) {
|
|
61
|
+
const url = request.url();
|
|
62
|
+
if (!isInterestingUrl(url)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const captured = {
|
|
66
|
+
url,
|
|
67
|
+
method: request.method(),
|
|
68
|
+
headers: request.headers(),
|
|
69
|
+
postData: request.postData() ?? undefined,
|
|
70
|
+
timestamp: new Date().toISOString(),
|
|
71
|
+
resourceType: request.resourceType(),
|
|
72
|
+
};
|
|
73
|
+
capturedNetworkData.requests.push(captured);
|
|
74
|
+
console.log(`\n📤 REQUEST: ${request.method()} ${url}`);
|
|
75
|
+
if (captured.postData) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(captured.postData);
|
|
78
|
+
// Show full body for message sends (to see mention format)
|
|
79
|
+
const isMessageSend = url.includes('/messages') && request.method() === 'POST';
|
|
80
|
+
const limit = isMessageSend ? 10000 : 500;
|
|
81
|
+
console.log(' Body:', JSON.stringify(parsed, null, 2).substring(0, limit));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
console.log(' Body:', captured.postData.substring(0, 200));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function captureResponse(response) {
|
|
89
|
+
const url = response.url();
|
|
90
|
+
if (!isInterestingUrl(url)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let body;
|
|
94
|
+
try {
|
|
95
|
+
const contentType = response.headers()['content-type'] || '';
|
|
96
|
+
if (contentType.includes('application/json') || contentType.includes('text/vtt') || contentType.includes('text/plain')) {
|
|
97
|
+
body = await response.text();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Response body may not be available
|
|
102
|
+
}
|
|
103
|
+
const captured = {
|
|
104
|
+
url,
|
|
105
|
+
status: response.status(),
|
|
106
|
+
statusText: response.statusText(),
|
|
107
|
+
headers: response.headers(),
|
|
108
|
+
body,
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
capturedNetworkData.responses.push(captured);
|
|
112
|
+
console.log(`\n📥 RESPONSE: ${response.status()} ${url}`);
|
|
113
|
+
if (body) {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(body);
|
|
116
|
+
console.log(' Body preview:', JSON.stringify(parsed, null, 2).substring(0, 500));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
console.log(' Body preview:', body.substring(0, 200));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function saveFindings() {
|
|
124
|
+
// Deduplicate and summarise findings
|
|
125
|
+
const uniqueEndpoints = new Map();
|
|
126
|
+
for (const req of capturedNetworkData.requests) {
|
|
127
|
+
const key = `${req.method}:${new URL(req.url).pathname}`;
|
|
128
|
+
if (!uniqueEndpoints.has(key)) {
|
|
129
|
+
uniqueEndpoints.set(key, req);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const findings = {
|
|
133
|
+
discoveredAt: new Date().toISOString(),
|
|
134
|
+
uniqueEndpoints: Array.from(uniqueEndpoints.values()).map(req => ({
|
|
135
|
+
method: req.method,
|
|
136
|
+
url: req.url,
|
|
137
|
+
hasPostData: !!req.postData,
|
|
138
|
+
postDataSample: req.postData?.substring(0, 500),
|
|
139
|
+
})),
|
|
140
|
+
allRequests: capturedNetworkData.requests,
|
|
141
|
+
allResponses: capturedNetworkData.responses,
|
|
142
|
+
};
|
|
143
|
+
fs.writeFileSync(FINDINGS_PATH, JSON.stringify(findings, null, 2));
|
|
144
|
+
console.log(`\n💾 Findings saved to ${FINDINGS_PATH}`);
|
|
145
|
+
}
|
|
146
|
+
async function checkAuthentication(page) {
|
|
147
|
+
const url = page.url();
|
|
148
|
+
// Check if we're on a login page
|
|
149
|
+
if (url.includes('login.microsoftonline.com') ||
|
|
150
|
+
url.includes('login.live.com') ||
|
|
151
|
+
url.includes('login.microsoft.com')) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
// Check if we're on the Teams app
|
|
155
|
+
if (url.includes('teams.microsoft.com')) {
|
|
156
|
+
// Look for indicators that we're logged in
|
|
157
|
+
try {
|
|
158
|
+
// Wait briefly for the app to load
|
|
159
|
+
await page.waitForTimeout(2000);
|
|
160
|
+
// Check for common authenticated elements
|
|
161
|
+
const hasAppBar = await page.locator('[data-tid="app-bar"]').count() > 0;
|
|
162
|
+
const hasSearchBox = await page.locator('[data-tid="search-box"]').count() > 0 ||
|
|
163
|
+
await page.locator('input[placeholder*="Search"]').count() > 0;
|
|
164
|
+
return hasAppBar || hasSearchBox;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
async function waitForAuthentication(page) {
|
|
173
|
+
console.log('\n🔐 Please log in to Microsoft Teams in the browser window...');
|
|
174
|
+
console.log(' The script will continue once you are authenticated.\n');
|
|
175
|
+
// Poll for authentication
|
|
176
|
+
while (true) {
|
|
177
|
+
if (await checkAuthentication(page)) {
|
|
178
|
+
console.log('✅ Authentication detected!');
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
await page.waitForTimeout(2000);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function main() {
|
|
185
|
+
console.log('🔍 Teams Research Script');
|
|
186
|
+
console.log('========================\n');
|
|
187
|
+
console.log('This script will help discover how Teams web app works.\n');
|
|
188
|
+
// Ensure user data directory exists
|
|
189
|
+
if (!fs.existsSync(USER_DATA_DIR)) {
|
|
190
|
+
fs.mkdirSync(USER_DATA_DIR, { recursive: true });
|
|
191
|
+
}
|
|
192
|
+
let browser;
|
|
193
|
+
let context;
|
|
194
|
+
try {
|
|
195
|
+
// Launch browser with persistent context
|
|
196
|
+
const channel = process.platform === 'win32' ? 'msedge' : 'chrome';
|
|
197
|
+
browser = await chromium.launch({
|
|
198
|
+
headless: false, // Must be visible for manual login
|
|
199
|
+
channel,
|
|
200
|
+
});
|
|
201
|
+
// Check if we have saved session state (uses encrypted store)
|
|
202
|
+
const existingState = readSessionState();
|
|
203
|
+
if (existingState) {
|
|
204
|
+
console.log('📂 Found existing session state, attempting to restore...');
|
|
205
|
+
context = await browser.newContext({
|
|
206
|
+
storageState: existingState,
|
|
207
|
+
viewport: { width: 1280, height: 800 },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
console.log('📂 No session state found, starting fresh...');
|
|
212
|
+
context = await browser.newContext({
|
|
213
|
+
viewport: { width: 1280, height: 800 },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const page = await context.newPage();
|
|
217
|
+
// Set up network interception
|
|
218
|
+
page.on('request', captureRequest);
|
|
219
|
+
page.on('response', captureResponse);
|
|
220
|
+
// Navigate to Teams
|
|
221
|
+
console.log('🌐 Navigating to Teams...');
|
|
222
|
+
await page.goto('https://teams.microsoft.com', { waitUntil: 'domcontentloaded' });
|
|
223
|
+
// Check if authentication is needed
|
|
224
|
+
const isAuthenticated = await checkAuthentication(page);
|
|
225
|
+
if (!isAuthenticated) {
|
|
226
|
+
await waitForAuthentication(page);
|
|
227
|
+
// Save session state after successful authentication
|
|
228
|
+
console.log('💾 Saving session state...');
|
|
229
|
+
const state = await context.storageState();
|
|
230
|
+
writeSessionState(state);
|
|
231
|
+
console.log('✅ Session state saved!');
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
console.log('✅ Already authenticated!');
|
|
235
|
+
}
|
|
236
|
+
// Instructions for the user
|
|
237
|
+
console.log('\n' + '='.repeat(60));
|
|
238
|
+
console.log('🔬 RESEARCH MODE ACTIVE');
|
|
239
|
+
console.log('='.repeat(60));
|
|
240
|
+
console.log('\nThe browser is now monitoring network requests.');
|
|
241
|
+
console.log('Try the following to discover API endpoints:\n');
|
|
242
|
+
console.log('1. Use the search bar (Cmd/Ctrl+E or click the search icon)');
|
|
243
|
+
console.log('2. Search for some text');
|
|
244
|
+
console.log('3. Click on search results');
|
|
245
|
+
console.log('4. Navigate between chats and channels');
|
|
246
|
+
console.log('\nPress Ctrl+C in this terminal when done to save findings.');
|
|
247
|
+
console.log('='.repeat(60) + '\n');
|
|
248
|
+
// Keep the browser open until interrupted
|
|
249
|
+
await new Promise((resolve) => {
|
|
250
|
+
process.on('SIGINT', () => {
|
|
251
|
+
console.log('\n\n⏹️ Stopping research...');
|
|
252
|
+
resolve();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
// Save research findings FIRST (before browser might close)
|
|
256
|
+
await saveFindings();
|
|
257
|
+
// Try to save session state (may fail if browser already closed)
|
|
258
|
+
try {
|
|
259
|
+
console.log('💾 Saving final session state...');
|
|
260
|
+
const finalState = await context.storageState();
|
|
261
|
+
writeSessionState(finalState);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
console.log('⚠️ Could not save session state (browser already closed)');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
// Still try to save findings on error
|
|
269
|
+
await saveFindings();
|
|
270
|
+
console.error('❌ Error:', error);
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
try {
|
|
274
|
+
if (context)
|
|
275
|
+
await context.close();
|
|
276
|
+
}
|
|
277
|
+
catch { /* already closed */ }
|
|
278
|
+
try {
|
|
279
|
+
if (browser)
|
|
280
|
+
await browser.close();
|
|
281
|
+
}
|
|
282
|
+
catch { /* already closed */ }
|
|
283
|
+
}
|
|
284
|
+
console.log('\n✅ Research session complete!');
|
|
285
|
+
console.log(` Review findings in: ${FINDINGS_PATH}`);
|
|
286
|
+
}
|
|
287
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focused research script for Teams search API.
|
|
3
|
+
*
|
|
4
|
+
* Captures full request/response bodies for Substrate search endpoints
|
|
5
|
+
* to understand pagination, query format, and response structure.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npm run research:search
|
|
9
|
+
*
|
|
10
|
+
* Instructions:
|
|
11
|
+
* 1. Run this script
|
|
12
|
+
* 2. Perform a search in Teams
|
|
13
|
+
* 3. Click "Messages" tab or "See more results"
|
|
14
|
+
* 4. Scroll through results to trigger pagination
|
|
15
|
+
* 5. Press Ctrl+C to stop and save findings
|
|
16
|
+
*/
|
|
17
|
+
export {};
|