msteams-mcp 0.2.0 ā 0.3.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 +5 -0
- package/dist/__fixtures__/api-responses.d.ts +6 -6
- package/dist/__fixtures__/api-responses.js +4 -4
- package/dist/api/chatsvc-api.d.ts +85 -21
- package/dist/api/chatsvc-api.js +231 -25
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/token-extractor.js +46 -60
- package/dist/auth/token-refresh.d.ts +47 -0
- package/dist/auth/token-refresh.js +121 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +19 -0
- package/dist/index.js +0 -0
- package/dist/test/cli.js +124 -12
- package/dist/test/mcp-harness.js +31 -0
- package/dist/tools/message-tools.d.ts +39 -0
- package/dist/tools/message-tools.js +177 -2
- package/dist/tools/people-tools.d.ts +2 -2
- package/dist/tools/search-tools.d.ts +8 -2
- package/dist/tools/search-tools.js +49 -2
- package/dist/utils/api-config.d.ts +6 -0
- package/dist/utils/api-config.js +7 -0
- package/dist/utils/auth-guards.d.ts +7 -0
- package/dist/utils/auth-guards.js +42 -1
- package/dist/utils/parsers.d.ts +15 -0
- package/dist/utils/parsers.js +31 -0
- package/dist/utils/parsers.test.js +55 -1
- package/package.json +1 -1
|
@@ -5,6 +5,30 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { readSessionState, readTokenCache, writeTokenCache, clearTokenCache, getTeamsOrigin, } from './session-store.js';
|
|
7
7
|
import { parseJwtProfile } from '../utils/parsers.js';
|
|
8
|
+
/**
|
|
9
|
+
* Decodes a JWT token's payload without verifying the signature.
|
|
10
|
+
* Returns null if the token is invalid.
|
|
11
|
+
*/
|
|
12
|
+
function decodeJwtPayload(token) {
|
|
13
|
+
try {
|
|
14
|
+
const parts = token.split('.');
|
|
15
|
+
if (parts.length < 2)
|
|
16
|
+
return null;
|
|
17
|
+
return JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Gets the expiry date from a JWT token.
|
|
25
|
+
*/
|
|
26
|
+
function getJwtExpiry(token) {
|
|
27
|
+
const payload = decodeJwtPayload(token);
|
|
28
|
+
if (!payload?.exp || typeof payload.exp !== 'number')
|
|
29
|
+
return null;
|
|
30
|
+
return new Date(payload.exp * 1000);
|
|
31
|
+
}
|
|
8
32
|
/**
|
|
9
33
|
* Extracts the Substrate search token from session state.
|
|
10
34
|
*/
|
|
@@ -22,10 +46,8 @@ export function extractSubstrateToken(state) {
|
|
|
22
46
|
const token = val.secret;
|
|
23
47
|
if (!token || typeof token !== 'string')
|
|
24
48
|
continue;
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
28
|
-
const expiry = new Date(payload.exp * 1000);
|
|
49
|
+
const expiry = getJwtExpiry(token);
|
|
50
|
+
if (expiry) {
|
|
29
51
|
return { token, expiry };
|
|
30
52
|
}
|
|
31
53
|
}
|
|
@@ -107,13 +129,12 @@ export function extractTeamsToken(state) {
|
|
|
107
129
|
const secret = val.secret;
|
|
108
130
|
if (typeof secret !== 'string' || !secret.startsWith('ey'))
|
|
109
131
|
continue;
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
132
|
+
const payload = decodeJwtPayload(secret);
|
|
133
|
+
if (!payload?.exp || typeof payload.exp !== 'number')
|
|
112
134
|
continue;
|
|
113
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
114
135
|
const tokenExpiry = new Date(payload.exp * 1000);
|
|
115
136
|
// Extract user MRI from any token
|
|
116
|
-
if (payload.oid && !userMri) {
|
|
137
|
+
if (typeof payload.oid === 'string' && !userMri) {
|
|
117
138
|
userMri = `8:orgid:${payload.oid}`;
|
|
118
139
|
}
|
|
119
140
|
// Prefer chatsvcagg.teams.microsoft.com token
|
|
@@ -139,17 +160,9 @@ export function extractTeamsToken(state) {
|
|
|
139
160
|
if (!userMri) {
|
|
140
161
|
const substrateInfo = extractSubstrateToken(sessionState);
|
|
141
162
|
if (substrateInfo) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
146
|
-
if (payload.oid) {
|
|
147
|
-
userMri = `8:orgid:${payload.oid}`;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
// Ignore
|
|
163
|
+
const payload = decodeJwtPayload(substrateInfo.token);
|
|
164
|
+
if (typeof payload?.oid === 'string') {
|
|
165
|
+
userMri = `8:orgid:${payload.oid}`;
|
|
153
166
|
}
|
|
154
167
|
}
|
|
155
168
|
}
|
|
@@ -185,32 +198,16 @@ export function extractMessageAuth(state) {
|
|
|
185
198
|
}
|
|
186
199
|
// Get userMri from skypeToken payload
|
|
187
200
|
if (skypeToken) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
192
|
-
if (payload.skypeid) {
|
|
193
|
-
userMri = payload.skypeid;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
// Not a JWT format, that's fine
|
|
201
|
+
const payload = decodeJwtPayload(skypeToken);
|
|
202
|
+
if (typeof payload?.skypeid === 'string') {
|
|
203
|
+
userMri = payload.skypeid;
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
206
|
// Fallback to extracting userMri from authToken
|
|
202
207
|
if (!userMri && authToken) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
207
|
-
if (payload.oid) {
|
|
208
|
-
userMri = `8:orgid:${payload.oid}`;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
// Ignore
|
|
208
|
+
const payload = decodeJwtPayload(authToken);
|
|
209
|
+
if (typeof payload?.oid === 'string') {
|
|
210
|
+
userMri = `8:orgid:${payload.oid}`;
|
|
214
211
|
}
|
|
215
212
|
}
|
|
216
213
|
if (skypeToken && authToken && userMri) {
|
|
@@ -252,7 +249,6 @@ export function getUserProfile(state) {
|
|
|
252
249
|
const teamsOrigin = getTeamsOrigin(sessionState);
|
|
253
250
|
if (!teamsOrigin)
|
|
254
251
|
return null;
|
|
255
|
-
// Look through localStorage for any JWT with user info
|
|
256
252
|
for (const item of teamsOrigin.localStorage) {
|
|
257
253
|
try {
|
|
258
254
|
const val = JSON.parse(item.value);
|
|
@@ -260,13 +256,11 @@ export function getUserProfile(state) {
|
|
|
260
256
|
continue;
|
|
261
257
|
if (!val.secret.startsWith('ey'))
|
|
262
258
|
continue;
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (profile) {
|
|
269
|
-
return profile;
|
|
259
|
+
const payload = decodeJwtPayload(val.secret);
|
|
260
|
+
if (payload) {
|
|
261
|
+
const profile = parseJwtProfile(payload);
|
|
262
|
+
if (profile)
|
|
263
|
+
return profile;
|
|
270
264
|
}
|
|
271
265
|
}
|
|
272
266
|
catch {
|
|
@@ -302,17 +296,9 @@ export function getUserDisplayName(state) {
|
|
|
302
296
|
// Try to get from token
|
|
303
297
|
const teamsToken = extractTeamsToken(sessionState);
|
|
304
298
|
if (teamsToken) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
309
|
-
if (payload.name)
|
|
310
|
-
return payload.name;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
catch {
|
|
314
|
-
// Ignore
|
|
315
|
-
}
|
|
299
|
+
const payload = decodeJwtPayload(teamsToken.token);
|
|
300
|
+
if (typeof payload?.name === 'string')
|
|
301
|
+
return payload.name;
|
|
316
302
|
}
|
|
317
303
|
return null;
|
|
318
304
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token refresh via headless browser.
|
|
3
|
+
*
|
|
4
|
+
* Teams uses SPA OAuth2 which restricts refresh tokens to browser-based CORS
|
|
5
|
+
* requests. We open a headless browser with saved session state, let MSAL
|
|
6
|
+
* silently refresh tokens, then save the updated state. Seamless to the user.
|
|
7
|
+
*/
|
|
8
|
+
import { type Result } from '../types/result.js';
|
|
9
|
+
/** Result of a successful token refresh. */
|
|
10
|
+
export interface TokenRefreshResult {
|
|
11
|
+
/** New token expiry time. */
|
|
12
|
+
newExpiry: Date;
|
|
13
|
+
/** Previous expiry time (for comparison). */
|
|
14
|
+
previousExpiry: Date;
|
|
15
|
+
/** Minutes gained by refresh. */
|
|
16
|
+
minutesGained: number;
|
|
17
|
+
/** Whether a refresh was actually needed (token was close to expiry). */
|
|
18
|
+
refreshNeeded: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Refreshes tokens by opening a headless browser with saved session state.
|
|
22
|
+
* MSAL refreshes tokens automatically when Teams loads; we just need to
|
|
23
|
+
* trigger that and save the updated state.
|
|
24
|
+
*/
|
|
25
|
+
export declare function refreshTokensViaBrowser(): Promise<Result<TokenRefreshResult>>;
|
|
26
|
+
/**
|
|
27
|
+
* Checks if tokens need refreshing (less than threshold time remaining).
|
|
28
|
+
*
|
|
29
|
+
* @returns true if tokens should be refreshed proactively
|
|
30
|
+
*/
|
|
31
|
+
export declare function shouldRefreshToken(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Checks if tokens are completely expired.
|
|
34
|
+
*
|
|
35
|
+
* @returns true if tokens have expired and cannot be used
|
|
36
|
+
*/
|
|
37
|
+
export declare function isTokenExpired(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Attempts to proactively refresh tokens if they're approaching expiry.
|
|
40
|
+
*
|
|
41
|
+
* This is a convenience function that checks if refresh is needed and
|
|
42
|
+
* performs it if necessary. Safe to call on any operation - it only
|
|
43
|
+
* opens a browser when actually needed.
|
|
44
|
+
*
|
|
45
|
+
* @returns The refresh result if refresh was attempted, null if not needed
|
|
46
|
+
*/
|
|
47
|
+
export declare function refreshIfNeeded(): Promise<Result<TokenRefreshResult> | null>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token refresh via headless browser.
|
|
3
|
+
*
|
|
4
|
+
* Teams uses SPA OAuth2 which restricts refresh tokens to browser-based CORS
|
|
5
|
+
* requests. We open a headless browser with saved session state, let MSAL
|
|
6
|
+
* silently refresh tokens, then save the updated state. Seamless to the user.
|
|
7
|
+
*/
|
|
8
|
+
import { TOKEN_REFRESH_THRESHOLD_MS, MSAL_TOKEN_DELAY_MS } from '../constants.js';
|
|
9
|
+
import { ErrorCode, createError } from '../types/errors.js';
|
|
10
|
+
import { ok, err } from '../types/result.js';
|
|
11
|
+
import { extractSubstrateToken, clearTokenCache, } from './token-extractor.js';
|
|
12
|
+
import { hasSessionState, isSessionLikelyExpired, } from './session-store.js';
|
|
13
|
+
/**
|
|
14
|
+
* Refreshes tokens by opening a headless browser with saved session state.
|
|
15
|
+
* MSAL refreshes tokens automatically when Teams loads; we just need to
|
|
16
|
+
* trigger that and save the updated state.
|
|
17
|
+
*/
|
|
18
|
+
export async function refreshTokensViaBrowser() {
|
|
19
|
+
// Check we have a session to work with
|
|
20
|
+
if (!hasSessionState()) {
|
|
21
|
+
return err(createError(ErrorCode.AUTH_REQUIRED, 'No session state available. Please run teams_login to authenticate.', { suggestions: ['Call teams_login to authenticate'] }));
|
|
22
|
+
}
|
|
23
|
+
if (isSessionLikelyExpired()) {
|
|
24
|
+
return err(createError(ErrorCode.AUTH_EXPIRED, 'Session is too old and likely expired. Please re-authenticate.', { suggestions: ['Call teams_login to re-authenticate'] }));
|
|
25
|
+
}
|
|
26
|
+
// Get current token expiry for comparison
|
|
27
|
+
const beforeToken = extractSubstrateToken();
|
|
28
|
+
if (!beforeToken) {
|
|
29
|
+
return err(createError(ErrorCode.AUTH_REQUIRED, 'No token found in session. Please run teams_login to authenticate.', { suggestions: ['Call teams_login to authenticate'] }));
|
|
30
|
+
}
|
|
31
|
+
const previousExpiry = beforeToken.expiry;
|
|
32
|
+
// Import browser functions dynamically to avoid circular dependencies
|
|
33
|
+
const { createBrowserContext, closeBrowser } = await import('../browser/context.js');
|
|
34
|
+
let manager = null;
|
|
35
|
+
try {
|
|
36
|
+
// Open headless browser with saved session
|
|
37
|
+
manager = await createBrowserContext({ headless: true });
|
|
38
|
+
// Navigate to Teams - this triggers MSAL to check/refresh tokens
|
|
39
|
+
await manager.page.goto('https://teams.microsoft.com', {
|
|
40
|
+
waitUntil: 'domcontentloaded',
|
|
41
|
+
});
|
|
42
|
+
// Wait for MSAL to refresh tokens in the background
|
|
43
|
+
// The token storage happens asynchronously after page load
|
|
44
|
+
await manager.page.waitForTimeout(MSAL_TOKEN_DELAY_MS);
|
|
45
|
+
// Close browser and save session state (this captures the refreshed tokens)
|
|
46
|
+
await closeBrowser(manager, true);
|
|
47
|
+
manager = null;
|
|
48
|
+
// Clear our token cache to force re-extraction from the new session
|
|
49
|
+
clearTokenCache();
|
|
50
|
+
// Extract the new token to verify we still have valid tokens
|
|
51
|
+
const afterToken = extractSubstrateToken();
|
|
52
|
+
if (!afterToken) {
|
|
53
|
+
return err(createError(ErrorCode.AUTH_EXPIRED, 'Token refresh failed - no token found after refresh attempt.', { suggestions: ['Call teams_login to re-authenticate'] }));
|
|
54
|
+
}
|
|
55
|
+
const newExpiry = afterToken.expiry;
|
|
56
|
+
const minutesGained = Math.round((newExpiry.getTime() - previousExpiry.getTime()) / 1000 / 60);
|
|
57
|
+
// Check if the token was close to expiry and needed refresh
|
|
58
|
+
const wasCloseToExpiry = previousExpiry.getTime() - Date.now() < TOKEN_REFRESH_THRESHOLD_MS;
|
|
59
|
+
// If we needed a refresh but didn't get one, that's an error
|
|
60
|
+
if (wasCloseToExpiry && newExpiry.getTime() <= previousExpiry.getTime()) {
|
|
61
|
+
return err(createError(ErrorCode.AUTH_EXPIRED, 'Token was not refreshed despite being close to expiry. Session may need re-authentication.', { suggestions: ['Call teams_login to re-authenticate'] }));
|
|
62
|
+
}
|
|
63
|
+
return ok({
|
|
64
|
+
newExpiry,
|
|
65
|
+
previousExpiry,
|
|
66
|
+
minutesGained,
|
|
67
|
+
refreshNeeded: wasCloseToExpiry,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
// Clean up browser if still open
|
|
72
|
+
if (manager) {
|
|
73
|
+
try {
|
|
74
|
+
await closeBrowser(manager, false);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Ignore cleanup errors
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
81
|
+
return err(createError(ErrorCode.UNKNOWN, `Token refresh via browser failed: ${message}`, { suggestions: ['Call teams_login to re-authenticate'] }));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Checks if tokens need refreshing (less than threshold time remaining).
|
|
86
|
+
*
|
|
87
|
+
* @returns true if tokens should be refreshed proactively
|
|
88
|
+
*/
|
|
89
|
+
export function shouldRefreshToken() {
|
|
90
|
+
const substrate = extractSubstrateToken();
|
|
91
|
+
if (!substrate)
|
|
92
|
+
return false;
|
|
93
|
+
const timeRemaining = substrate.expiry.getTime() - Date.now();
|
|
94
|
+
return timeRemaining > 0 && timeRemaining < TOKEN_REFRESH_THRESHOLD_MS;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Checks if tokens are completely expired.
|
|
98
|
+
*
|
|
99
|
+
* @returns true if tokens have expired and cannot be used
|
|
100
|
+
*/
|
|
101
|
+
export function isTokenExpired() {
|
|
102
|
+
const substrate = extractSubstrateToken();
|
|
103
|
+
if (!substrate)
|
|
104
|
+
return true;
|
|
105
|
+
return substrate.expiry.getTime() <= Date.now();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Attempts to proactively refresh tokens if they're approaching expiry.
|
|
109
|
+
*
|
|
110
|
+
* This is a convenience function that checks if refresh is needed and
|
|
111
|
+
* performs it if necessary. Safe to call on any operation - it only
|
|
112
|
+
* opens a browser when actually needed.
|
|
113
|
+
*
|
|
114
|
+
* @returns The refresh result if refresh was attempted, null if not needed
|
|
115
|
+
*/
|
|
116
|
+
export async function refreshIfNeeded() {
|
|
117
|
+
if (!shouldRefreshToken()) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return refreshTokensViaBrowser();
|
|
121
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -52,3 +52,13 @@ export declare const RETRY_BASE_DELAY_MS = 1000;
|
|
|
52
52
|
export declare const RETRY_MAX_DELAY_MS = 10000;
|
|
53
53
|
/** Self-chat (notes) conversation ID. */
|
|
54
54
|
export declare const SELF_CHAT_ID = "48:notes";
|
|
55
|
+
/** Activity feed (notifications) conversation ID. */
|
|
56
|
+
export declare const NOTIFICATIONS_ID = "48:notifications";
|
|
57
|
+
/** Default limit for activity feed items. */
|
|
58
|
+
export declare const DEFAULT_ACTIVITY_LIMIT = 50;
|
|
59
|
+
/** Maximum limit for activity feed items. */
|
|
60
|
+
export declare const MAX_ACTIVITY_LIMIT = 200;
|
|
61
|
+
/** Maximum conversations to check when aggregating unread status. */
|
|
62
|
+
export declare const MAX_UNREAD_AGGREGATE_CHECK = 20;
|
|
63
|
+
/** Threshold for proactive token refresh (10 minutes before expiry). */
|
|
64
|
+
export declare const TOKEN_REFRESH_THRESHOLD_MS: number;
|
package/dist/constants.js
CHANGED
|
@@ -70,3 +70,22 @@ export const RETRY_MAX_DELAY_MS = 10000;
|
|
|
70
70
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
71
71
|
/** Self-chat (notes) conversation ID. */
|
|
72
72
|
export const SELF_CHAT_ID = '48:notes';
|
|
73
|
+
/** Activity feed (notifications) conversation ID. */
|
|
74
|
+
export const NOTIFICATIONS_ID = '48:notifications';
|
|
75
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
76
|
+
// Activity Feed
|
|
77
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
78
|
+
/** Default limit for activity feed items. */
|
|
79
|
+
export const DEFAULT_ACTIVITY_LIMIT = 50;
|
|
80
|
+
/** Maximum limit for activity feed items. */
|
|
81
|
+
export const MAX_ACTIVITY_LIMIT = 200;
|
|
82
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
83
|
+
// Unread Status
|
|
84
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
85
|
+
/** Maximum conversations to check when aggregating unread status. */
|
|
86
|
+
export const MAX_UNREAD_AGGREGATE_CHECK = 20;
|
|
87
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
88
|
+
// Token Refresh
|
|
89
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
90
|
+
/** Threshold for proactive token refresh (10 minutes before expiry). */
|
|
91
|
+
export const TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/test/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createBrowserContext, closeBrowser } from '../browser/context.js';
|
|
|
13
13
|
import { ensureAuthenticated, forceNewLogin } from '../browser/auth.js';
|
|
14
14
|
import { hasSessionState, getSessionAge, clearSessionState, } from '../auth/session-store.js';
|
|
15
15
|
import { hasValidSubstrateToken, getSubstrateTokenStatus, extractMessageAuth, getUserProfile, clearTokenCache, } from '../auth/token-extractor.js';
|
|
16
|
+
import { refreshTokensViaBrowser } from '../auth/token-refresh.js';
|
|
16
17
|
import { searchMessages } from '../api/substrate-api.js';
|
|
17
18
|
import { sendMessage, sendNoteToSelf } from '../api/chatsvc-api.js';
|
|
18
19
|
function parseArgs() {
|
|
@@ -64,6 +65,7 @@ Commands:
|
|
|
64
65
|
me Get current user profile (email, name, Teams ID)
|
|
65
66
|
login Log in to Teams (opens browser)
|
|
66
67
|
login --force Force new login (clears existing session)
|
|
68
|
+
refresh Test OAuth token refresh (shows before/after status)
|
|
67
69
|
help Show this help message
|
|
68
70
|
|
|
69
71
|
Options:
|
|
@@ -84,17 +86,27 @@ Examples:
|
|
|
84
86
|
npm run cli -- search "query" --from 25
|
|
85
87
|
npm run cli -- send "Test message to myself"
|
|
86
88
|
npm run cli -- login --force
|
|
89
|
+
npm run cli -- refresh
|
|
87
90
|
`);
|
|
88
91
|
}
|
|
89
92
|
async function commandStatus(flags) {
|
|
90
93
|
const hasSession = hasSessionState();
|
|
91
94
|
const sessionAge = getSessionAge();
|
|
92
|
-
const
|
|
95
|
+
const substrateStatus = getSubstrateTokenStatus();
|
|
96
|
+
const messageAuth = extractMessageAuth();
|
|
97
|
+
// Check if refresh will trigger soon (within 10 minutes)
|
|
98
|
+
const REFRESH_THRESHOLD_MINS = 10;
|
|
99
|
+
const willRefreshSoon = substrateStatus.minutesRemaining !== undefined &&
|
|
100
|
+
substrateStatus.minutesRemaining <= REFRESH_THRESHOLD_MINS;
|
|
93
101
|
const status = {
|
|
94
|
-
|
|
95
|
-
available:
|
|
96
|
-
expiresAt:
|
|
97
|
-
minutesRemaining:
|
|
102
|
+
search: {
|
|
103
|
+
available: substrateStatus.hasToken,
|
|
104
|
+
expiresAt: substrateStatus.expiresAt,
|
|
105
|
+
minutesRemaining: substrateStatus.minutesRemaining,
|
|
106
|
+
willAutoRefresh: willRefreshSoon,
|
|
107
|
+
},
|
|
108
|
+
messaging: {
|
|
109
|
+
available: !!messageAuth,
|
|
98
110
|
},
|
|
99
111
|
session: {
|
|
100
112
|
exists: hasSession,
|
|
@@ -106,20 +118,44 @@ async function commandStatus(flags) {
|
|
|
106
118
|
console.log(JSON.stringify(status, null, 2));
|
|
107
119
|
}
|
|
108
120
|
else {
|
|
109
|
-
console.log('\nš Status\n');
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
console.log('\nš Token Status\n');
|
|
122
|
+
// Search token (Substrate)
|
|
123
|
+
console.log('Search API (Substrate):');
|
|
124
|
+
if (status.search.available) {
|
|
125
|
+
console.log(` Status: ā
Valid`);
|
|
126
|
+
console.log(` Expires: ${status.search.expiresAt}`);
|
|
127
|
+
console.log(` Remaining: ${status.search.minutesRemaining} minutes`);
|
|
128
|
+
if (status.search.willAutoRefresh) {
|
|
129
|
+
console.log(` ā” Auto-refresh will trigger (< ${REFRESH_THRESHOLD_MINS} min remaining)`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(' Status: ā No valid token');
|
|
134
|
+
}
|
|
135
|
+
// Messaging token (Skype)
|
|
136
|
+
console.log('\nMessaging API (Skype):');
|
|
137
|
+
if (status.messaging.available) {
|
|
138
|
+
console.log(' Status: ā
Valid');
|
|
112
139
|
}
|
|
113
140
|
else {
|
|
114
|
-
console.log('
|
|
141
|
+
console.log(' Status: ā No valid token');
|
|
115
142
|
}
|
|
116
|
-
|
|
143
|
+
// Session info
|
|
144
|
+
console.log('\nSession:');
|
|
145
|
+
console.log(` Exists: ${status.session.exists ? 'ā
Yes' : 'ā No'}`);
|
|
117
146
|
if (status.session.ageHours !== null) {
|
|
118
|
-
console.log(`
|
|
147
|
+
console.log(` Age: ${status.session.ageHours} hours`);
|
|
119
148
|
if (status.session.likelyExpired) {
|
|
120
|
-
console.log('ā ļø Session may be expired');
|
|
149
|
+
console.log(' ā ļø Session may be expired (>12 hours old)');
|
|
121
150
|
}
|
|
122
151
|
}
|
|
152
|
+
// Action hint
|
|
153
|
+
if (!status.search.available || !status.messaging.available) {
|
|
154
|
+
console.log('\nš” Run: npm run cli -- login');
|
|
155
|
+
}
|
|
156
|
+
else if (status.search.willAutoRefresh) {
|
|
157
|
+
console.log('\nš” Run: npm run cli -- refresh (to test auto-refresh)');
|
|
158
|
+
}
|
|
123
159
|
}
|
|
124
160
|
}
|
|
125
161
|
async function commandSearch(query, flags, options) {
|
|
@@ -267,6 +303,79 @@ async function commandMe(flags) {
|
|
|
267
303
|
}
|
|
268
304
|
}
|
|
269
305
|
}
|
|
306
|
+
async function commandRefresh(flags) {
|
|
307
|
+
const asJson = flags.has('json');
|
|
308
|
+
// Show current status
|
|
309
|
+
const beforeStatus = getSubstrateTokenStatus();
|
|
310
|
+
if (!asJson) {
|
|
311
|
+
console.log('\nš Token Refresh Test\n');
|
|
312
|
+
console.log('Before refresh:');
|
|
313
|
+
if (beforeStatus.hasToken) {
|
|
314
|
+
console.log(` Token valid: ā
Yes`);
|
|
315
|
+
console.log(` Expires at: ${beforeStatus.expiresAt}`);
|
|
316
|
+
console.log(` Minutes remaining: ${beforeStatus.minutesRemaining}`);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
console.log(` Token valid: ā No`);
|
|
320
|
+
}
|
|
321
|
+
console.log('\nOpening headless browser to refresh tokens...');
|
|
322
|
+
}
|
|
323
|
+
// Attempt refresh via headless browser
|
|
324
|
+
const result = await refreshTokensViaBrowser();
|
|
325
|
+
if (!result.ok) {
|
|
326
|
+
if (asJson) {
|
|
327
|
+
console.log(JSON.stringify({
|
|
328
|
+
success: false,
|
|
329
|
+
error: result.error.message,
|
|
330
|
+
code: result.error.code,
|
|
331
|
+
before: beforeStatus,
|
|
332
|
+
}, null, 2));
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
console.log(`\nā Refresh failed: ${result.error.message}`);
|
|
336
|
+
if (result.error.code) {
|
|
337
|
+
console.log(` Error code: ${result.error.code}`);
|
|
338
|
+
}
|
|
339
|
+
if (result.error.suggestions) {
|
|
340
|
+
console.log(` Suggestions: ${result.error.suggestions.join(', ')}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
// Show new status
|
|
346
|
+
const afterStatus = getSubstrateTokenStatus();
|
|
347
|
+
if (asJson) {
|
|
348
|
+
console.log(JSON.stringify({
|
|
349
|
+
success: true,
|
|
350
|
+
before: beforeStatus,
|
|
351
|
+
after: afterStatus,
|
|
352
|
+
refreshResult: {
|
|
353
|
+
previousExpiry: result.value.previousExpiry.toISOString(),
|
|
354
|
+
newExpiry: result.value.newExpiry.toISOString(),
|
|
355
|
+
minutesGained: result.value.minutesGained,
|
|
356
|
+
refreshNeeded: result.value.refreshNeeded,
|
|
357
|
+
},
|
|
358
|
+
}, null, 2));
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
if (result.value.refreshNeeded) {
|
|
362
|
+
console.log('\nā
Token refreshed!\n');
|
|
363
|
+
console.log('After refresh:');
|
|
364
|
+
console.log(` Token valid: ā
Yes`);
|
|
365
|
+
console.log(` Expires at: ${afterStatus.expiresAt}`);
|
|
366
|
+
console.log(` Minutes remaining: ${afterStatus.minutesRemaining}`);
|
|
367
|
+
console.log(` Time gained: +${result.value.minutesGained} minutes`);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
console.log('\nā
Token is still valid (no refresh needed)\n');
|
|
371
|
+
console.log('Current token:');
|
|
372
|
+
console.log(` Expires at: ${afterStatus.expiresAt}`);
|
|
373
|
+
console.log(` Minutes remaining: ${afterStatus.minutesRemaining}`);
|
|
374
|
+
console.log('\n Note: MSAL only refreshes tokens when they\'re close to expiry.');
|
|
375
|
+
console.log(' Proactive refresh will trigger when <10 minutes remain.');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
270
379
|
async function commandLogin(flags) {
|
|
271
380
|
const force = flags.has('force');
|
|
272
381
|
if (force) {
|
|
@@ -311,6 +420,9 @@ async function main() {
|
|
|
311
420
|
case 'me':
|
|
312
421
|
await commandMe(flags);
|
|
313
422
|
break;
|
|
423
|
+
case 'refresh':
|
|
424
|
+
await commandRefresh(flags);
|
|
425
|
+
break;
|
|
314
426
|
case 'login':
|
|
315
427
|
await commandLogin(flags);
|
|
316
428
|
break;
|
package/dist/test/mcp-harness.js
CHANGED
|
@@ -33,6 +33,9 @@ const SHORTCUTS = {
|
|
|
33
33
|
contacts: { tool: 'teams_get_frequent_contacts' },
|
|
34
34
|
channel: { tool: 'teams_find_channel', primaryArg: 'query' },
|
|
35
35
|
chat: { tool: 'teams_get_chat', primaryArg: 'userId' },
|
|
36
|
+
unread: { tool: 'teams_get_unread' },
|
|
37
|
+
markread: { tool: 'teams_mark_read' },
|
|
38
|
+
activity: { tool: 'teams_get_activity' },
|
|
36
39
|
};
|
|
37
40
|
// Map CLI flags to tool parameter names
|
|
38
41
|
const FLAG_MAPPINGS = {
|
|
@@ -48,6 +51,7 @@ const FLAG_MAPPINGS = {
|
|
|
48
51
|
'--force': 'forceNew',
|
|
49
52
|
'--user': 'userId',
|
|
50
53
|
'--userId': 'userId',
|
|
54
|
+
'--markRead': 'markRead',
|
|
51
55
|
};
|
|
52
56
|
function parseArgs() {
|
|
53
57
|
const args = process.argv.slice(2);
|
|
@@ -251,6 +255,9 @@ function prettyPrintResponse(response, toolName) {
|
|
|
251
255
|
else if (response.messages && Array.isArray(response.messages)) {
|
|
252
256
|
printMessagesList(response.messages);
|
|
253
257
|
}
|
|
258
|
+
else if (response.activities && Array.isArray(response.activities)) {
|
|
259
|
+
printActivityList(response.activities);
|
|
260
|
+
}
|
|
254
261
|
else if (response.contacts && Array.isArray(response.contacts)) {
|
|
255
262
|
printContactsList(response.contacts);
|
|
256
263
|
}
|
|
@@ -340,6 +347,30 @@ function printContactsList(contacts) {
|
|
|
340
347
|
log('');
|
|
341
348
|
}
|
|
342
349
|
}
|
|
350
|
+
function printActivityList(activities) {
|
|
351
|
+
log(`Found ${activities.length} activity items:\n`);
|
|
352
|
+
const typeIcons = {
|
|
353
|
+
mention: 'š£',
|
|
354
|
+
reaction: 'š',
|
|
355
|
+
reply: 'š¬',
|
|
356
|
+
message: 'š',
|
|
357
|
+
unknown: 'ā',
|
|
358
|
+
};
|
|
359
|
+
for (const a of activities) {
|
|
360
|
+
const activity = a;
|
|
361
|
+
const type = activity.type || 'unknown';
|
|
362
|
+
const icon = typeIcons[type] || 'ā';
|
|
363
|
+
const sender = activity.sender?.displayName || 'Unknown';
|
|
364
|
+
const time = activity.timestamp ? new Date(activity.timestamp).toLocaleString() : '';
|
|
365
|
+
const topic = activity.topic ? ` in "${activity.topic}"` : '';
|
|
366
|
+
const preview = String(activity.content ?? '').substring(0, 80).replace(/\n/g, ' ');
|
|
367
|
+
log(`${icon} [${type}] ${sender}${topic} - ${time}`);
|
|
368
|
+
log(` ${preview}${String(activity.content ?? '').length > 80 ? '...' : ''}`);
|
|
369
|
+
if (activity.conversationId)
|
|
370
|
+
log(` ConversationId: ${activity.conversationId}`);
|
|
371
|
+
log('');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
343
374
|
function printProfile(profile) {
|
|
344
375
|
log('š¤ Profile:\n');
|
|
345
376
|
for (const [key, value] of Object.entries(profile)) {
|