msteams-mcp 0.28.0 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -117,7 +117,7 @@ See [CLI Usage](#cli-usage) for commands.
117
117
  | `teams_search_email` | Search emails in your mailbox (same auth as Teams - no extra login) |
118
118
  | `teams_list_chats` | List recent conversations (1:1, group, meeting, channel) with a last-message preview |
119
119
  | `teams_get_message` | Get a single message by ID with full content (any age); includes reactions |
120
- | `teams_get_thread` | Get messages from a conversation/thread; includes reactions; `threadRootId` scopes to one channel thread; `fromUrl` accepts a Teams message deep link |
120
+ | `teams_get_thread` | Get messages from a conversation/thread; includes reactions; `threadRootId` scopes to one channel thread; `fromUrl` accepts a Teams message deep link; `cursor` pages back through older history |
121
121
  | `teams_find_channel` | Find channels by name (your teams + org-wide discovery) |
122
122
  | `teams_get_tags` | List a team's channel tags for tag @mentions |
123
123
  | `teams_get_activity` | Get activity feed (mentions, reactions, replies, notifications) |
@@ -43,6 +43,8 @@ export interface ThreadMessage {
43
43
  export interface GetThreadResult {
44
44
  conversationId: string;
45
45
  messages: ThreadMessage[];
46
+ /** Opaque cursor for the next-older page; absent when there is no older history. */
47
+ olderCursor?: string;
46
48
  }
47
49
  /** Result of editing a message. */
48
50
  export interface EditMessageResult {
@@ -151,13 +153,26 @@ export declare function getMessage(conversationId: string, messageId: string): P
151
153
  * @param options.startTime - Fetch messages from this timestamp onwards
152
154
  * @param options.order - Sort order: 'desc' (newest-first, default) or 'asc' (oldest-first)
153
155
  * @param options.replyToMessageId - For channels: scope to replies of a specific top-level post
156
+ * @param options.cursor - olderCursor from a previous call, to fetch the next-older page
154
157
  */
155
158
  export declare function getThreadMessages(conversationId: string, options?: {
156
159
  limit?: number;
157
160
  startTime?: number;
158
161
  order?: 'asc' | 'desc';
159
162
  replyToMessageId?: string;
163
+ cursor?: string;
160
164
  }): Promise<Result<GetThreadResult>>;
165
+ /**
166
+ * Builds the chatsvc messages URL. A cursor only contributes query parameters, so it can never
167
+ * redirect the authenticated request to another host or conversation.
168
+ */
169
+ export declare function buildThreadMessagesUrl(baseUrl: string, options: {
170
+ limit: number;
171
+ startTime?: number;
172
+ cursor?: string;
173
+ }): string;
174
+ /** Reduces chatsvc's _metadata.backwardLink (a full URL) to an opaque cursor of its query parameters. */
175
+ export declare function cursorFromBackwardLink(backwardLink: string | undefined): string | undefined;
161
176
  /**
162
177
  * Edits an existing message.
163
178
  *
@@ -243,6 +243,7 @@ export async function getMessage(conversationId, messageId) {
243
243
  * @param options.startTime - Fetch messages from this timestamp onwards
244
244
  * @param options.order - Sort order: 'desc' (newest-first, default) or 'asc' (oldest-first)
245
245
  * @param options.replyToMessageId - For channels: scope to replies of a specific top-level post
246
+ * @param options.cursor - olderCursor from a previous call, to fetch the next-older page
246
247
  */
247
248
  export async function getThreadMessages(conversationId, options = {}) {
248
249
  const authResult = requireMessageAuthWithConfig();
@@ -251,11 +252,7 @@ export async function getThreadMessages(conversationId, options = {}) {
251
252
  }
252
253
  const { auth, region, baseUrl } = authResult.value;
253
254
  const limit = options.limit ?? 50;
254
- let url = CHATSVC_API.messages(region, conversationId, options.replyToMessageId, baseUrl);
255
- url += `?view=msnp24Equivalent|supportsMessageProperties&pageSize=${limit}`;
256
- if (options.startTime) {
257
- url += `&startTime=${options.startTime}`;
258
- }
255
+ const url = buildThreadMessagesUrl(CHATSVC_API.messages(region, conversationId, options.replyToMessageId, baseUrl), { limit, startTime: options.startTime, cursor: options.cursor });
259
256
  const response = await httpRequest(url, {
260
257
  method: 'GET',
261
258
  headers: getSkypeAuthHeaders(auth.skypeToken, auth.authToken, baseUrl),
@@ -264,12 +261,13 @@ export async function getThreadMessages(conversationId, options = {}) {
264
261
  return response;
265
262
  }
266
263
  const rawMessages = response.value.data.messages;
267
- if (!Array.isArray(rawMessages)) {
264
+ if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
268
265
  return ok({
269
266
  conversationId,
270
267
  messages: [],
271
268
  });
272
269
  }
270
+ const olderCursor = cursorFromBackwardLink(response.value.data._metadata?.backwardLink);
273
271
  const messages = [];
274
272
  for (const raw of rawMessages) {
275
273
  const msg = raw;
@@ -350,8 +348,36 @@ export async function getThreadMessages(conversationId, options = {}) {
350
348
  return ok({
351
349
  conversationId,
352
350
  messages,
351
+ olderCursor,
353
352
  });
354
353
  }
354
+ /**
355
+ * Builds the chatsvc messages URL. A cursor only contributes query parameters, so it can never
356
+ * redirect the authenticated request to another host or conversation.
357
+ */
358
+ export function buildThreadMessagesUrl(baseUrl, options) {
359
+ const params = new URLSearchParams(options.cursor ?? '');
360
+ params.delete('view');
361
+ params.set('pageSize', String(options.limit));
362
+ if (!options.cursor && options.startTime) {
363
+ params.set('startTime', String(options.startTime));
364
+ }
365
+ return `${baseUrl}?view=msnp24Equivalent|supportsMessageProperties&${params}`;
366
+ }
367
+ /** Reduces chatsvc's _metadata.backwardLink (a full URL) to an opaque cursor of its query parameters. */
368
+ export function cursorFromBackwardLink(backwardLink) {
369
+ if (!backwardLink)
370
+ return undefined;
371
+ try {
372
+ const params = new URL(backwardLink).searchParams;
373
+ params.delete('view');
374
+ params.delete('pageSize');
375
+ return params.toString() || undefined;
376
+ }
377
+ catch {
378
+ return undefined;
379
+ }
380
+ }
355
381
  // ─────────────────────────────────────────────────────────────────────────────
356
382
  // Edit / Delete
357
383
  // ─────────────────────────────────────────────────────────────────────────────
@@ -5,7 +5,7 @@
5
5
  * and mentions property serialisation.
6
6
  */
7
7
  import { describe, it, expect } from 'vitest';
8
- import { buildMentionHtml, buildMentionsProperty, parseContentWithMentionsAndLinks, resolveMessageContent, parseScheduleTime, buildScheduledDraftBody, extractOneOnOneMemberIds, buildOneOnOneThreadBody, clampWaitParams, selectNewMessages, resolveWaitBaseline, generateClientMessageId, } from './chatsvc-messaging.js';
8
+ import { buildMentionHtml, buildMentionsProperty, parseContentWithMentionsAndLinks, resolveMessageContent, parseScheduleTime, buildScheduledDraftBody, extractOneOnOneMemberIds, buildOneOnOneThreadBody, clampWaitParams, selectNewMessages, resolveWaitBaseline, generateClientMessageId, buildThreadMessagesUrl, cursorFromBackwardLink, } from './chatsvc-messaging.js';
9
9
  import { MAX_WAIT_SECONDS } from '../constants.js';
10
10
  // ─────────────────────────────────────────────────────────────────────────────
11
11
  // buildMentionHtml
@@ -330,3 +330,26 @@ describe('resolveWaitBaseline', () => {
330
330
  expect(resolveWaitBaseline([])).toBe(0);
331
331
  });
332
332
  });
333
+ describe('thread history cursor', () => {
334
+ const base = 'https://teams.microsoft.com/api/chatsvc/amer/v1/users/ME/conversations/19%3Aabc%40thread.tacv2/messages';
335
+ it('builds the first page unchanged', () => {
336
+ expect(buildThreadMessagesUrl(base, { limit: 50, startTime: 123 }))
337
+ .toBe(`${base}?view=msnp24Equivalent|supportsMessageProperties&pageSize=50&startTime=123`);
338
+ });
339
+ it('round-trips a backwardLink into the next request', () => {
340
+ const cursor = cursorFromBackwardLink('https://amer.ng.msg.teams.microsoft.com/v1/users/ME/conversations/19:abc@thread.tacv2/messages?startTime=1&syncState=abc%3D&pageSize=200&view=msnp24Equivalent');
341
+ expect(cursor).toBe('startTime=1&syncState=abc%3D');
342
+ expect(buildThreadMessagesUrl(base, { limit: 200, startTime: 999, cursor }))
343
+ .toBe(`${base}?view=msnp24Equivalent|supportsMessageProperties&startTime=1&syncState=abc%3D&pageSize=200`);
344
+ });
345
+ it('keeps the request on the base host and path whatever the cursor holds', () => {
346
+ const url = buildThreadMessagesUrl(base, { limit: 10, cursor: 'x=1#@evil.example/path&view=other' });
347
+ expect(new URL(url).origin + new URL(url).pathname).toBe(base);
348
+ expect(new URL(url).searchParams.getAll('view')).toEqual(['msnp24Equivalent|supportsMessageProperties']);
349
+ });
350
+ it('has no cursor when there is no usable backwardLink', () => {
351
+ expect(cursorFromBackwardLink(undefined)).toBeUndefined();
352
+ expect(cursorFromBackwardLink('not a url')).toBeUndefined();
353
+ expect(cursorFromBackwardLink('https://x/messages?view=a&pageSize=5')).toBeUndefined();
354
+ });
355
+ });
@@ -37,12 +37,12 @@ export declare const SubmissionActionInputSchema: z.ZodObject<{
37
37
  assignmentId: z.ZodString;
38
38
  submissionId: z.ZodString;
39
39
  }, "strip", z.ZodTypeAny, {
40
- action: "submit" | "unsubmit" | "view";
40
+ action: "view" | "submit" | "unsubmit";
41
41
  classId: string;
42
42
  assignmentId: string;
43
43
  submissionId: string;
44
44
  }, {
45
- action: "submit" | "unsubmit" | "view";
45
+ action: "view" | "submit" | "unsubmit";
46
46
  classId: string;
47
47
  assignmentId: string;
48
48
  submissionId: string;
@@ -78,12 +78,12 @@ export declare const assignmentTools: (RegisteredTool<z.ZodObject<{
78
78
  assignmentId: z.ZodString;
79
79
  submissionId: z.ZodString;
80
80
  }, "strip", z.ZodTypeAny, {
81
- action: "submit" | "unsubmit" | "view";
81
+ action: "view" | "submit" | "unsubmit";
82
82
  classId: string;
83
83
  assignmentId: string;
84
84
  submissionId: string;
85
85
  }, {
86
- action: "submit" | "unsubmit" | "view";
86
+ action: "view" | "submit" | "unsubmit";
87
87
  classId: string;
88
88
  assignmentId: string;
89
89
  submissionId: string;
@@ -27,6 +27,7 @@ export declare const GetThreadInputSchema: z.ZodEffects<z.ZodObject<{
27
27
  markRead: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
28
28
  order: z.ZodDefault<z.ZodOptional<z.ZodEnum<["asc", "desc"]>>>;
29
29
  since: z.ZodOptional<z.ZodString>;
30
+ cursor: z.ZodOptional<z.ZodString>;
30
31
  }, "strip", z.ZodTypeAny, {
31
32
  limit: number;
32
33
  order: "asc" | "desc";
@@ -35,6 +36,7 @@ export declare const GetThreadInputSchema: z.ZodEffects<z.ZodObject<{
35
36
  fromUrl?: string | undefined;
36
37
  threadRootId?: string | undefined;
37
38
  since?: string | undefined;
39
+ cursor?: string | undefined;
38
40
  }, {
39
41
  conversationId?: string | undefined;
40
42
  limit?: number | undefined;
@@ -43,6 +45,7 @@ export declare const GetThreadInputSchema: z.ZodEffects<z.ZodObject<{
43
45
  threadRootId?: string | undefined;
44
46
  markRead?: boolean | undefined;
45
47
  since?: string | undefined;
48
+ cursor?: string | undefined;
46
49
  }>, {
47
50
  limit: number;
48
51
  order: "asc" | "desc";
@@ -51,6 +54,7 @@ export declare const GetThreadInputSchema: z.ZodEffects<z.ZodObject<{
51
54
  fromUrl?: string | undefined;
52
55
  threadRootId?: string | undefined;
53
56
  since?: string | undefined;
57
+ cursor?: string | undefined;
54
58
  }, {
55
59
  conversationId?: string | undefined;
56
60
  limit?: number | undefined;
@@ -59,6 +63,7 @@ export declare const GetThreadInputSchema: z.ZodEffects<z.ZodObject<{
59
63
  threadRootId?: string | undefined;
60
64
  markRead?: boolean | undefined;
61
65
  since?: string | undefined;
66
+ cursor?: string | undefined;
62
67
  }>;
63
68
  export declare const FindChannelInputSchema: z.ZodObject<{
64
69
  query: z.ZodString;
@@ -114,6 +119,7 @@ export declare const searchTools: (RegisteredTool<z.ZodObject<{
114
119
  markRead: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
115
120
  order: z.ZodDefault<z.ZodOptional<z.ZodEnum<["asc", "desc"]>>>;
116
121
  since: z.ZodOptional<z.ZodString>;
122
+ cursor: z.ZodOptional<z.ZodString>;
117
123
  }, "strip", z.ZodTypeAny, {
118
124
  limit: number;
119
125
  order: "asc" | "desc";
@@ -122,6 +128,7 @@ export declare const searchTools: (RegisteredTool<z.ZodObject<{
122
128
  fromUrl?: string | undefined;
123
129
  threadRootId?: string | undefined;
124
130
  since?: string | undefined;
131
+ cursor?: string | undefined;
125
132
  }, {
126
133
  conversationId?: string | undefined;
127
134
  limit?: number | undefined;
@@ -130,6 +137,7 @@ export declare const searchTools: (RegisteredTool<z.ZodObject<{
130
137
  threadRootId?: string | undefined;
131
138
  markRead?: boolean | undefined;
132
139
  since?: string | undefined;
140
+ cursor?: string | undefined;
133
141
  }>, {
134
142
  limit: number;
135
143
  order: "asc" | "desc";
@@ -138,6 +146,7 @@ export declare const searchTools: (RegisteredTool<z.ZodObject<{
138
146
  fromUrl?: string | undefined;
139
147
  threadRootId?: string | undefined;
140
148
  since?: string | undefined;
149
+ cursor?: string | undefined;
141
150
  }, {
142
151
  conversationId?: string | undefined;
143
152
  limit?: number | undefined;
@@ -146,6 +155,7 @@ export declare const searchTools: (RegisteredTool<z.ZodObject<{
146
155
  threadRootId?: string | undefined;
147
156
  markRead?: boolean | undefined;
148
157
  since?: string | undefined;
158
+ cursor?: string | undefined;
149
159
  }>> | RegisteredTool<z.ZodObject<{
150
160
  query: z.ZodString;
151
161
  limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
@@ -25,6 +25,7 @@ export const GetThreadInputSchema = z.object({
25
25
  markRead: z.boolean().optional().default(false),
26
26
  order: z.enum(['asc', 'desc']).optional().default('desc'),
27
27
  since: z.string().optional(),
28
+ cursor: z.string().min(1).optional(),
28
29
  }).refine(d => d.conversationId || d.fromUrl, {
29
30
  message: 'Provide either conversationId or fromUrl',
30
31
  });
@@ -69,7 +70,7 @@ const searchToolDefinition = {
69
70
  };
70
71
  const getThreadToolDefinition = {
71
72
  name: 'teams_get_thread',
72
- description: 'Get messages from a Teams conversation/thread. Default: newest-first (latest messages at top). For channels: messages include isThreadReply (true for replies) and threadRootId (ID of the post being replied to). Messages without threadRootId are top-level posts. Use threadRootId to group related messages. Messages include reactions (individual reactors, names resolved where possible) and reactionSummary (counts per emoji) when present. Each message includes a "when" field with the day of week (e.g., "Friday, January 30, 2026, 10:45 AM UTC"). Returns unread count and can optionally mark as read.',
73
+ description: 'Get messages from a Teams conversation/thread. Default: newest-first (latest messages at top). For channels: messages include isThreadReply (true for replies) and threadRootId (ID of the post being replied to). Messages without threadRootId are top-level posts. Use threadRootId to group related messages. Messages include reactions (individual reactors, names resolved where possible) and reactionSummary (counts per emoji) when present. Each message includes a "when" field with the day of week (e.g., "Friday, January 30, 2026, 10:45 AM UTC"). Returns unread count and can optionally mark as read. Pass olderCursor back as cursor to page through older history.',
73
74
  inputSchema: {
74
75
  type: 'object',
75
76
  properties: {
@@ -102,6 +103,10 @@ const getThreadToolDefinition = {
102
103
  type: 'string',
103
104
  description: 'ISO 8601 timestamp (e.g., "2026-02-26T00:00:00Z"). When provided, only returns messages after this time.',
104
105
  },
106
+ cursor: {
107
+ type: 'string',
108
+ description: 'The olderCursor from a previous response, to fetch the next-older page of history. Repeat until olderCursor is absent to walk a conversation back to its start. Overrides since.',
109
+ },
105
110
  },
106
111
  },
107
112
  };
@@ -219,6 +224,7 @@ async function handleGetThread(input) {
219
224
  order: input.order,
220
225
  startTime,
221
226
  replyToMessageId: threadRootId,
227
+ cursor: input.cursor,
222
228
  });
223
229
  if (!result.ok) {
224
230
  return { success: false, error: result.error };
@@ -269,6 +275,7 @@ async function handleGetThread(input) {
269
275
  lastReadMessageId,
270
276
  markedAsRead: input.markRead ? markedAsRead : undefined,
271
277
  messages: result.value.messages,
278
+ olderCursor: result.value.olderCursor,
272
279
  },
273
280
  };
274
281
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msteams-mcp",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
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 {};