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 +1 -1
- package/dist/api/chatsvc-messaging.d.ts +15 -0
- package/dist/api/chatsvc-messaging.js +32 -6
- package/dist/api/chatsvc-messaging.test.js +24 -1
- package/dist/tools/assignment-tools.d.ts +4 -4
- package/dist/tools/search-tools.d.ts +10 -0
- package/dist/tools/search-tools.js +8 -1
- 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
- package/dist/types/server.d.ts +0 -27
- package/dist/types/server.js +0 -7
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
|
-
|
|
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: "
|
|
40
|
+
action: "view" | "submit" | "unsubmit";
|
|
41
41
|
classId: string;
|
|
42
42
|
assignmentId: string;
|
|
43
43
|
submissionId: string;
|
|
44
44
|
}, {
|
|
45
|
-
action: "
|
|
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: "
|
|
81
|
+
action: "view" | "submit" | "unsubmit";
|
|
82
82
|
classId: string;
|
|
83
83
|
assignmentId: string;
|
|
84
84
|
submissionId: string;
|
|
85
85
|
}, {
|
|
86
|
-
action: "
|
|
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
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 {};
|