msteams-mcp 0.3.4 → 0.3.5
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/substrate-api.js +5 -5
- package/dist/auth/token-extractor.d.ts +14 -4
- package/dist/auth/token-extractor.js +156 -128
- package/dist/auth/token-refresh.d.ts +0 -22
- package/dist/auth/token-refresh.js +1 -39
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +5 -0
- package/dist/utils/auth-guards.d.ts +0 -5
- package/dist/utils/auth-guards.js +0 -12
- package/package.json +1 -1
|
@@ -8,14 +8,14 @@ import { SUBSTRATE_API, getBearerHeaders } from '../utils/api-config.js';
|
|
|
8
8
|
import { ErrorCode } from '../types/errors.js';
|
|
9
9
|
import { ok } from '../types/result.js';
|
|
10
10
|
import { clearTokenCache } from '../auth/token-extractor.js';
|
|
11
|
-
import {
|
|
11
|
+
import { requireSubstrateTokenAsync } from '../utils/auth-guards.js';
|
|
12
12
|
import { parseSearchResults, parsePeopleResults, parseChannelResults, filterChannelsByName, } from '../utils/parsers.js';
|
|
13
13
|
import { getMyTeamsAndChannels } from './csa-api.js';
|
|
14
14
|
/**
|
|
15
15
|
* Searches Teams messages using the Substrate v2 query API.
|
|
16
16
|
*/
|
|
17
17
|
export async function searchMessages(query, options = {}) {
|
|
18
|
-
const tokenResult =
|
|
18
|
+
const tokenResult = await requireSubstrateTokenAsync();
|
|
19
19
|
if (!tokenResult.ok) {
|
|
20
20
|
return tokenResult;
|
|
21
21
|
}
|
|
@@ -92,7 +92,7 @@ export async function searchMessages(query, options = {}) {
|
|
|
92
92
|
* Searches for people by name or email.
|
|
93
93
|
*/
|
|
94
94
|
export async function searchPeople(query, limit = 10) {
|
|
95
|
-
const tokenResult =
|
|
95
|
+
const tokenResult = await requireSubstrateTokenAsync();
|
|
96
96
|
if (!tokenResult.ok) {
|
|
97
97
|
return tokenResult;
|
|
98
98
|
}
|
|
@@ -143,7 +143,7 @@ export async function searchPeople(query, limit = 10) {
|
|
|
143
143
|
* Gets the user's frequently contacted people.
|
|
144
144
|
*/
|
|
145
145
|
export async function getFrequentContacts(limit = 50) {
|
|
146
|
-
const tokenResult =
|
|
146
|
+
const tokenResult = await requireSubstrateTokenAsync();
|
|
147
147
|
if (!tokenResult.ok) {
|
|
148
148
|
return tokenResult;
|
|
149
149
|
}
|
|
@@ -201,7 +201,7 @@ export async function getFrequentContacts(limit = 50) {
|
|
|
201
201
|
* @param limit - Maximum number of results (default: 10, max: 50)
|
|
202
202
|
*/
|
|
203
203
|
export async function searchChannels(query, limit = 10) {
|
|
204
|
-
const tokenResult =
|
|
204
|
+
const tokenResult = await requireSubstrateTokenAsync();
|
|
205
205
|
if (!tokenResult.ok) {
|
|
206
206
|
return tokenResult;
|
|
207
207
|
}
|
|
@@ -2,21 +2,23 @@
|
|
|
2
2
|
* Token extraction from session state.
|
|
3
3
|
*
|
|
4
4
|
* Extracts various authentication tokens from Playwright's saved session state.
|
|
5
|
+
* Teams stores MSAL tokens in localStorage; we parse these to get bearer tokens
|
|
6
|
+
* for various APIs (Substrate search, chatsvc messaging, etc.).
|
|
5
7
|
*/
|
|
6
8
|
import { clearTokenCache, type SessionState } from './session-store.js';
|
|
7
9
|
import { type UserProfile } from '../utils/parsers.js';
|
|
8
|
-
/** Substrate search token
|
|
10
|
+
/** Substrate search token (for search/people APIs). */
|
|
9
11
|
export interface SubstrateTokenInfo {
|
|
10
12
|
token: string;
|
|
11
13
|
expiry: Date;
|
|
12
14
|
}
|
|
13
|
-
/** Teams API token
|
|
15
|
+
/** Teams chat API token (for chatsvc). */
|
|
14
16
|
export interface TeamsTokenInfo {
|
|
15
17
|
token: string;
|
|
16
18
|
expiry: Date;
|
|
17
19
|
userMri: string;
|
|
18
20
|
}
|
|
19
|
-
/**
|
|
21
|
+
/** Cookie-based auth for messaging APIs. */
|
|
20
22
|
export interface MessageAuthInfo {
|
|
21
23
|
skypeToken: string;
|
|
22
24
|
authToken: string;
|
|
@@ -24,6 +26,7 @@ export interface MessageAuthInfo {
|
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* Extracts the Substrate search token from session state.
|
|
29
|
+
* This token is used for search and people APIs.
|
|
27
30
|
*/
|
|
28
31
|
export declare function extractSubstrateToken(state?: SessionState): SubstrateTokenInfo | null;
|
|
29
32
|
/**
|
|
@@ -44,14 +47,20 @@ export declare function getSubstrateTokenStatus(): {
|
|
|
44
47
|
};
|
|
45
48
|
/**
|
|
46
49
|
* Extracts the Teams chat API token from session state.
|
|
50
|
+
*
|
|
51
|
+
* Teams stores multiple tokens for different services. We prefer:
|
|
52
|
+
* 1. chatsvcagg.teams.microsoft.com (primary chat API)
|
|
53
|
+
* 2. api.spaces.skype.com (fallback)
|
|
47
54
|
*/
|
|
48
55
|
export declare function extractTeamsToken(state?: SessionState): TeamsTokenInfo | null;
|
|
49
56
|
/**
|
|
50
|
-
* Extracts authentication info needed for messaging API
|
|
57
|
+
* Extracts authentication info needed for messaging API.
|
|
58
|
+
* Unlike other APIs, messaging uses cookies rather than localStorage tokens.
|
|
51
59
|
*/
|
|
52
60
|
export declare function extractMessageAuth(state?: SessionState): MessageAuthInfo | null;
|
|
53
61
|
/**
|
|
54
62
|
* Extracts the CSA token for the conversationFolders API.
|
|
63
|
+
* This searches all origins, not just teams.microsoft.com.
|
|
55
64
|
*/
|
|
56
65
|
export declare function extractCsaToken(state?: SessionState): string | null;
|
|
57
66
|
/**
|
|
@@ -60,6 +69,7 @@ export declare function extractCsaToken(state?: SessionState): string | null;
|
|
|
60
69
|
export declare function getUserProfile(state?: SessionState): UserProfile | null;
|
|
61
70
|
/**
|
|
62
71
|
* Gets user's display name from session state.
|
|
72
|
+
* Searches localStorage entries first, then falls back to JWT claims.
|
|
63
73
|
*/
|
|
64
74
|
export declare function getUserDisplayName(state?: SessionState): string | null;
|
|
65
75
|
/**
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
* Token extraction from session state.
|
|
3
3
|
*
|
|
4
4
|
* Extracts various authentication tokens from Playwright's saved session state.
|
|
5
|
+
* Teams stores MSAL tokens in localStorage; we parse these to get bearer tokens
|
|
6
|
+
* for various APIs (Substrate search, chatsvc messaging, etc.).
|
|
5
7
|
*/
|
|
6
8
|
import { readSessionState, readTokenCache, writeTokenCache, clearTokenCache, getTeamsOrigin, } from './session-store.js';
|
|
7
9
|
import { parseJwtProfile } from '../utils/parsers.js';
|
|
10
|
+
import { MRI_ORGID_PREFIX } from '../constants.js';
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// JWT Utilities
|
|
13
|
+
// ============================================================================
|
|
8
14
|
/**
|
|
9
15
|
* Decodes a JWT token's payload without verifying the signature.
|
|
10
|
-
* Returns null if the token is invalid.
|
|
11
16
|
*/
|
|
12
17
|
function decodeJwtPayload(token) {
|
|
13
18
|
try {
|
|
@@ -21,7 +26,7 @@ function decodeJwtPayload(token) {
|
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
|
-
* Gets the expiry date from a JWT token.
|
|
29
|
+
* Gets the expiry date from a JWT token's `exp` claim.
|
|
25
30
|
*/
|
|
26
31
|
function getJwtExpiry(token) {
|
|
27
32
|
const payload = decodeJwtPayload(token);
|
|
@@ -30,26 +35,48 @@ function getJwtExpiry(token) {
|
|
|
30
35
|
return new Date(payload.exp * 1000);
|
|
31
36
|
}
|
|
32
37
|
/**
|
|
33
|
-
*
|
|
38
|
+
* Checks if a string looks like a JWT (starts with 'ey').
|
|
34
39
|
*/
|
|
35
|
-
|
|
40
|
+
function isJwtToken(value) {
|
|
41
|
+
return typeof value === 'string' && value.startsWith('ey');
|
|
42
|
+
}
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Session Helpers
|
|
45
|
+
// ============================================================================
|
|
46
|
+
/**
|
|
47
|
+
* Resolves session state and Teams origin in one call.
|
|
48
|
+
* Many functions need both, so this reduces boilerplate.
|
|
49
|
+
*/
|
|
50
|
+
function getTeamsLocalStorage(state) {
|
|
36
51
|
const sessionState = state ?? readSessionState();
|
|
37
52
|
if (!sessionState)
|
|
38
53
|
return null;
|
|
39
54
|
const teamsOrigin = getTeamsOrigin(sessionState);
|
|
40
|
-
|
|
55
|
+
return teamsOrigin?.localStorage ?? null;
|
|
56
|
+
}
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Token Extraction
|
|
59
|
+
// ============================================================================
|
|
60
|
+
/**
|
|
61
|
+
* Extracts the Substrate search token from session state.
|
|
62
|
+
* This token is used for search and people APIs.
|
|
63
|
+
*/
|
|
64
|
+
export function extractSubstrateToken(state) {
|
|
65
|
+
const localStorage = getTeamsLocalStorage(state);
|
|
66
|
+
if (!localStorage)
|
|
41
67
|
return null;
|
|
42
|
-
for (const item of
|
|
68
|
+
for (const item of localStorage) {
|
|
43
69
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
const entry = JSON.parse(item.value);
|
|
71
|
+
// Look for the Substrate search token by its target scope
|
|
72
|
+
if (!entry.target?.includes('substrate.office.com/search/SubstrateSearch')) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!isJwtToken(entry.secret))
|
|
76
|
+
continue;
|
|
77
|
+
const expiry = getJwtExpiry(entry.secret);
|
|
78
|
+
if (expiry) {
|
|
79
|
+
return { token: entry.secret, expiry };
|
|
53
80
|
}
|
|
54
81
|
}
|
|
55
82
|
catch {
|
|
@@ -58,6 +85,9 @@ export function extractSubstrateToken(state) {
|
|
|
58
85
|
}
|
|
59
86
|
return null;
|
|
60
87
|
}
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Cached Token Access
|
|
90
|
+
// ============================================================================
|
|
61
91
|
/**
|
|
62
92
|
* Gets a valid Substrate token, either from cache or by extracting from session.
|
|
63
93
|
*/
|
|
@@ -108,47 +138,40 @@ export function getSubstrateTokenStatus() {
|
|
|
108
138
|
}
|
|
109
139
|
/**
|
|
110
140
|
* Extracts the Teams chat API token from session state.
|
|
141
|
+
*
|
|
142
|
+
* Teams stores multiple tokens for different services. We prefer:
|
|
143
|
+
* 1. chatsvcagg.teams.microsoft.com (primary chat API)
|
|
144
|
+
* 2. api.spaces.skype.com (fallback)
|
|
111
145
|
*/
|
|
112
146
|
export function extractTeamsToken(state) {
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
115
|
-
return null;
|
|
116
|
-
const teamsOrigin = getTeamsOrigin(sessionState);
|
|
117
|
-
if (!teamsOrigin)
|
|
147
|
+
const localStorage = getTeamsLocalStorage(state);
|
|
148
|
+
if (!localStorage)
|
|
118
149
|
return null;
|
|
119
|
-
let
|
|
120
|
-
let
|
|
121
|
-
let skypeToken = null;
|
|
122
|
-
let skypeTokenExpiry = null;
|
|
150
|
+
let chatsvcCandidate = null;
|
|
151
|
+
let skypeCandidate = null;
|
|
123
152
|
let userMri = null;
|
|
124
|
-
for (const item of
|
|
153
|
+
for (const item of localStorage) {
|
|
125
154
|
try {
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
128
|
-
continue;
|
|
129
|
-
const secret = val.secret;
|
|
130
|
-
if (typeof secret !== 'string' || !secret.startsWith('ey'))
|
|
155
|
+
const entry = JSON.parse(item.value);
|
|
156
|
+
if (!entry.target || !isJwtToken(entry.secret))
|
|
131
157
|
continue;
|
|
132
|
-
const payload = decodeJwtPayload(secret);
|
|
158
|
+
const payload = decodeJwtPayload(entry.secret);
|
|
133
159
|
if (!payload?.exp || typeof payload.exp !== 'number')
|
|
134
160
|
continue;
|
|
135
|
-
const
|
|
136
|
-
//
|
|
161
|
+
const expiry = new Date(payload.exp * 1000);
|
|
162
|
+
// Capture user MRI from any token's oid claim
|
|
137
163
|
if (typeof payload.oid === 'string' && !userMri) {
|
|
138
|
-
userMri =
|
|
164
|
+
userMri = `${MRI_ORGID_PREFIX}${payload.oid}`;
|
|
139
165
|
}
|
|
140
|
-
//
|
|
141
|
-
if (
|
|
142
|
-
if (!
|
|
143
|
-
|
|
144
|
-
chatTokenExpiry = tokenExpiry;
|
|
166
|
+
// Track best candidate for each service
|
|
167
|
+
if (entry.target.includes('chatsvcagg.teams.microsoft.com')) {
|
|
168
|
+
if (!chatsvcCandidate || expiry > chatsvcCandidate.expiry) {
|
|
169
|
+
chatsvcCandidate = { token: entry.secret, expiry };
|
|
145
170
|
}
|
|
146
171
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
skypeToken = secret;
|
|
151
|
-
skypeTokenExpiry = tokenExpiry;
|
|
172
|
+
else if (entry.target.includes('api.spaces.skype.com')) {
|
|
173
|
+
if (!skypeCandidate || expiry > skypeCandidate.expiry) {
|
|
174
|
+
skypeCandidate = { token: entry.secret, expiry };
|
|
152
175
|
}
|
|
153
176
|
}
|
|
154
177
|
}
|
|
@@ -156,107 +179,108 @@ export function extractTeamsToken(state) {
|
|
|
156
179
|
continue;
|
|
157
180
|
}
|
|
158
181
|
}
|
|
159
|
-
//
|
|
182
|
+
// Fallback: extract userMri from Substrate token if not found
|
|
160
183
|
if (!userMri) {
|
|
161
|
-
|
|
162
|
-
if (substrateInfo) {
|
|
163
|
-
const payload = decodeJwtPayload(substrateInfo.token);
|
|
164
|
-
if (typeof payload?.oid === 'string') {
|
|
165
|
-
userMri = `8:orgid:${payload.oid}`;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
184
|
+
userMri = extractUserMriFromSubstrate(state);
|
|
168
185
|
}
|
|
169
|
-
// Prefer chatsvc
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
// Prefer chatsvc, fall back to skype
|
|
187
|
+
const best = chatsvcCandidate ?? skypeCandidate;
|
|
188
|
+
if (!best || !userMri || best.expiry.getTime() <= Date.now()) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return { token: best.token, expiry: best.expiry, userMri };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Extracts user MRI from the Substrate token's oid claim.
|
|
195
|
+
*/
|
|
196
|
+
function extractUserMriFromSubstrate(state) {
|
|
197
|
+
const substrateInfo = extractSubstrateToken(state);
|
|
198
|
+
if (!substrateInfo)
|
|
199
|
+
return null;
|
|
200
|
+
const payload = decodeJwtPayload(substrateInfo.token);
|
|
201
|
+
if (typeof payload?.oid === 'string') {
|
|
202
|
+
return `${MRI_ORGID_PREFIX}${payload.oid}`;
|
|
174
203
|
}
|
|
175
204
|
return null;
|
|
176
205
|
}
|
|
177
206
|
/**
|
|
178
|
-
* Extracts authentication info needed for messaging API
|
|
207
|
+
* Extracts authentication info needed for messaging API.
|
|
208
|
+
* Unlike other APIs, messaging uses cookies rather than localStorage tokens.
|
|
179
209
|
*/
|
|
180
210
|
export function extractMessageAuth(state) {
|
|
181
211
|
const sessionState = state ?? readSessionState();
|
|
182
212
|
if (!sessionState)
|
|
183
213
|
return null;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
authToken = authToken.substring(7);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Get userMri from skypeToken payload
|
|
200
|
-
if (skypeToken) {
|
|
201
|
-
const payload = decodeJwtPayload(skypeToken);
|
|
202
|
-
if (typeof payload?.skypeid === 'string') {
|
|
203
|
-
userMri = payload.skypeid;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// Fallback to extracting userMri from authToken
|
|
207
|
-
if (!userMri && authToken) {
|
|
208
|
-
const payload = decodeJwtPayload(authToken);
|
|
209
|
-
if (typeof payload?.oid === 'string') {
|
|
210
|
-
userMri = `8:orgid:${payload.oid}`;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (skypeToken && authToken && userMri) {
|
|
214
|
-
return { skypeToken, authToken, userMri };
|
|
214
|
+
const cookies = sessionState.cookies ?? [];
|
|
215
|
+
const teamsCookies = cookies.filter(c => c.domain?.includes('teams.microsoft.com'));
|
|
216
|
+
// Extract the two required cookies
|
|
217
|
+
const skypeToken = teamsCookies.find(c => c.name === 'skypetoken_asm')?.value ?? null;
|
|
218
|
+
const rawAuthToken = teamsCookies.find(c => c.name === 'authtoken')?.value ?? null;
|
|
219
|
+
if (!skypeToken || !rawAuthToken)
|
|
220
|
+
return null;
|
|
221
|
+
// Decode authtoken (URL-encoded, may have 'Bearer=' prefix)
|
|
222
|
+
let authToken = decodeURIComponent(rawAuthToken);
|
|
223
|
+
if (authToken.startsWith('Bearer=')) {
|
|
224
|
+
authToken = authToken.substring(7);
|
|
215
225
|
}
|
|
216
|
-
|
|
226
|
+
// Extract userMri from skypeToken's skypeid claim, or fall back to authToken's oid
|
|
227
|
+
const userMri = extractMriFromSkypeToken(skypeToken)
|
|
228
|
+
?? extractMriFromAuthToken(authToken);
|
|
229
|
+
if (!userMri)
|
|
230
|
+
return null;
|
|
231
|
+
return { skypeToken, authToken, userMri };
|
|
232
|
+
}
|
|
233
|
+
function extractMriFromSkypeToken(token) {
|
|
234
|
+
const payload = decodeJwtPayload(token);
|
|
235
|
+
return typeof payload?.skypeid === 'string' ? payload.skypeid : null;
|
|
236
|
+
}
|
|
237
|
+
function extractMriFromAuthToken(token) {
|
|
238
|
+
const payload = decodeJwtPayload(token);
|
|
239
|
+
return typeof payload?.oid === 'string' ? `${MRI_ORGID_PREFIX}${payload.oid}` : null;
|
|
217
240
|
}
|
|
218
241
|
/**
|
|
219
242
|
* Extracts the CSA token for the conversationFolders API.
|
|
243
|
+
* This searches all origins, not just teams.microsoft.com.
|
|
220
244
|
*/
|
|
221
245
|
export function extractCsaToken(state) {
|
|
222
246
|
const sessionState = state ?? readSessionState();
|
|
223
247
|
if (!sessionState)
|
|
224
248
|
return null;
|
|
225
|
-
for (const origin of sessionState.origins
|
|
226
|
-
for (const item of origin.localStorage
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
249
|
+
for (const origin of sessionState.origins ?? []) {
|
|
250
|
+
for (const item of origin.localStorage ?? []) {
|
|
251
|
+
// Skip temporary entries, look for chatsvcagg tokens
|
|
252
|
+
if (item.name.startsWith('tmp.'))
|
|
253
|
+
continue;
|
|
254
|
+
if (!item.name.includes('chatsvcagg.teams.microsoft.com'))
|
|
255
|
+
continue;
|
|
256
|
+
try {
|
|
257
|
+
const entry = JSON.parse(item.value);
|
|
258
|
+
if (entry.secret)
|
|
259
|
+
return entry.secret;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Ignore parse errors
|
|
237
263
|
}
|
|
238
264
|
}
|
|
239
265
|
}
|
|
240
266
|
return null;
|
|
241
267
|
}
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// User Profile
|
|
270
|
+
// ============================================================================
|
|
242
271
|
/**
|
|
243
272
|
* Gets the current user's profile from cached JWT tokens.
|
|
244
273
|
*/
|
|
245
274
|
export function getUserProfile(state) {
|
|
246
|
-
const
|
|
247
|
-
if (!
|
|
248
|
-
return null;
|
|
249
|
-
const teamsOrigin = getTeamsOrigin(sessionState);
|
|
250
|
-
if (!teamsOrigin)
|
|
275
|
+
const localStorage = getTeamsLocalStorage(state);
|
|
276
|
+
if (!localStorage)
|
|
251
277
|
return null;
|
|
252
|
-
for (const item of
|
|
278
|
+
for (const item of localStorage) {
|
|
253
279
|
try {
|
|
254
|
-
const
|
|
255
|
-
if (!
|
|
256
|
-
continue;
|
|
257
|
-
if (!val.secret.startsWith('ey'))
|
|
280
|
+
const entry = JSON.parse(item.value);
|
|
281
|
+
if (!isJwtToken(entry.secret))
|
|
258
282
|
continue;
|
|
259
|
-
const payload = decodeJwtPayload(
|
|
283
|
+
const payload = decodeJwtPayload(entry.secret);
|
|
260
284
|
if (payload) {
|
|
261
285
|
const profile = parseJwtProfile(payload);
|
|
262
286
|
if (profile)
|
|
@@ -271,30 +295,31 @@ export function getUserProfile(state) {
|
|
|
271
295
|
}
|
|
272
296
|
/**
|
|
273
297
|
* Gets user's display name from session state.
|
|
298
|
+
* Searches localStorage entries first, then falls back to JWT claims.
|
|
274
299
|
*/
|
|
275
300
|
export function getUserDisplayName(state) {
|
|
276
|
-
const
|
|
277
|
-
if (!
|
|
278
|
-
return null;
|
|
279
|
-
const teamsOrigin = getTeamsOrigin(sessionState);
|
|
280
|
-
if (!teamsOrigin)
|
|
301
|
+
const localStorage = getTeamsLocalStorage(state);
|
|
302
|
+
if (!localStorage)
|
|
281
303
|
return null;
|
|
282
|
-
for
|
|
304
|
+
// First pass: look for explicit displayName in localStorage
|
|
305
|
+
for (const item of localStorage) {
|
|
306
|
+
// Quick filter before parsing
|
|
307
|
+
if (!item.value?.includes('displayName') && !item.value?.includes('givenName')) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
283
310
|
try {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return val.name.displayName;
|
|
290
|
-
}
|
|
311
|
+
const entry = JSON.parse(item.value);
|
|
312
|
+
if (entry.displayName)
|
|
313
|
+
return entry.displayName;
|
|
314
|
+
if (entry.name?.displayName)
|
|
315
|
+
return entry.name.displayName;
|
|
291
316
|
}
|
|
292
317
|
catch {
|
|
293
318
|
continue;
|
|
294
319
|
}
|
|
295
320
|
}
|
|
296
|
-
//
|
|
297
|
-
const teamsToken = extractTeamsToken(
|
|
321
|
+
// Fallback: extract from Teams token's name claim
|
|
322
|
+
const teamsToken = extractTeamsToken(state);
|
|
298
323
|
if (teamsToken) {
|
|
299
324
|
const payload = decodeJwtPayload(teamsToken.token);
|
|
300
325
|
if (typeof payload?.name === 'string')
|
|
@@ -302,6 +327,9 @@ export function getUserDisplayName(state) {
|
|
|
302
327
|
}
|
|
303
328
|
return null;
|
|
304
329
|
}
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// Token Status Checks
|
|
332
|
+
// ============================================================================
|
|
305
333
|
/**
|
|
306
334
|
* Checks if tokens in session state are expired.
|
|
307
335
|
*/
|
|
@@ -23,25 +23,3 @@ export interface TokenRefreshResult {
|
|
|
23
23
|
* trigger that and save the updated state.
|
|
24
24
|
*/
|
|
25
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>;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* requests. We open a headless browser with saved session state, let MSAL
|
|
6
6
|
* silently refresh tokens, then save the updated state. Seamless to the user.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { MSAL_TOKEN_DELAY_MS, TOKEN_REFRESH_THRESHOLD_MS } from '../constants.js';
|
|
9
9
|
import { ErrorCode, createError } from '../types/errors.js';
|
|
10
10
|
import { ok, err } from '../types/result.js';
|
|
11
11
|
import { extractSubstrateToken, clearTokenCache, } from './token-extractor.js';
|
|
@@ -81,41 +81,3 @@ export async function refreshTokensViaBrowser() {
|
|
|
81
81
|
return err(createError(ErrorCode.UNKNOWN, `Token refresh via browser failed: ${message}`, { suggestions: ['Call teams_login to re-authenticate'] }));
|
|
82
82
|
}
|
|
83
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
|
@@ -62,3 +62,5 @@ export declare const MAX_ACTIVITY_LIMIT = 200;
|
|
|
62
62
|
export declare const MAX_UNREAD_AGGREGATE_CHECK = 20;
|
|
63
63
|
/** Threshold for proactive token refresh (10 minutes before expiry). */
|
|
64
64
|
export declare const TOKEN_REFRESH_THRESHOLD_MS: number;
|
|
65
|
+
/** MRI prefix for organisation users (orgid). */
|
|
66
|
+
export declare const MRI_ORGID_PREFIX = "8:orgid:";
|
package/dist/constants.js
CHANGED
|
@@ -89,3 +89,8 @@ export const MAX_UNREAD_AGGREGATE_CHECK = 20;
|
|
|
89
89
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
90
|
/** Threshold for proactive token refresh (10 minutes before expiry). */
|
|
91
91
|
export const TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
93
|
+
// User Identity
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
95
|
+
/** MRI prefix for organisation users (orgid). */
|
|
96
|
+
export const MRI_ORGID_PREFIX = '8:orgid:';
|
|
@@ -12,11 +12,6 @@ export interface CsaAuthInfo {
|
|
|
12
12
|
auth: MessageAuthInfo;
|
|
13
13
|
csaToken: string;
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Requires a valid Substrate token.
|
|
17
|
-
* Use for search and people APIs.
|
|
18
|
-
*/
|
|
19
|
-
export declare function requireSubstrateToken(): Result<string, McpError>;
|
|
20
15
|
/**
|
|
21
16
|
* Requires a valid Substrate token with proactive refresh.
|
|
22
17
|
*
|
|
@@ -13,24 +13,12 @@ import { refreshTokensViaBrowser } from '../auth/token-refresh.js';
|
|
|
13
13
|
// Error Messages
|
|
14
14
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
15
|
const AUTH_ERROR_MESSAGES = {
|
|
16
|
-
substrateToken: 'No valid token available. Browser login required.',
|
|
17
16
|
messageAuth: 'No valid authentication. Browser login required.',
|
|
18
17
|
csaToken: 'No valid authentication for favourites. Browser login required.',
|
|
19
18
|
};
|
|
20
19
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
20
|
// Guard Functions
|
|
22
21
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
-
/**
|
|
24
|
-
* Requires a valid Substrate token.
|
|
25
|
-
* Use for search and people APIs.
|
|
26
|
-
*/
|
|
27
|
-
export function requireSubstrateToken() {
|
|
28
|
-
const token = getValidSubstrateToken();
|
|
29
|
-
if (!token) {
|
|
30
|
-
return err(createError(ErrorCode.AUTH_REQUIRED, AUTH_ERROR_MESSAGES.substrateToken));
|
|
31
|
-
}
|
|
32
|
-
return ok(token);
|
|
33
|
-
}
|
|
34
22
|
/**
|
|
35
23
|
* Checks if the Substrate token is approaching expiry and needs refresh.
|
|
36
24
|
*
|