msteams-mcp 0.2.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.
Potentially problematic release.
This version of msteams-mcp might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/__fixtures__/api-responses.d.ts +254 -0
- package/dist/__fixtures__/api-responses.js +245 -0
- package/dist/api/calendar-api.d.ts +66 -0
- package/dist/api/calendar-api.js +179 -0
- package/dist/api/chatsvc-api.d.ts +352 -0
- package/dist/api/chatsvc-api.js +1100 -0
- package/dist/api/csa-api.d.ts +64 -0
- package/dist/api/csa-api.js +200 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.js +7 -0
- package/dist/api/substrate-api.d.ts +50 -0
- package/dist/api/substrate-api.js +305 -0
- package/dist/auth/crypto.d.ts +32 -0
- package/dist/auth/crypto.js +66 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/session-store.d.ts +87 -0
- package/dist/auth/session-store.js +230 -0
- package/dist/auth/token-extractor.d.ts +185 -0
- package/dist/auth/token-extractor.js +674 -0
- package/dist/auth/token-refresh.d.ts +25 -0
- package/dist/auth/token-refresh.js +85 -0
- package/dist/browser/auth.d.ts +53 -0
- package/dist/browser/auth.js +603 -0
- package/dist/browser/context.d.ts +40 -0
- package/dist/browser/context.js +122 -0
- package/dist/constants.d.ts +104 -0
- package/dist/constants.js +195 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/research/auth-research.d.ts +10 -0
- package/dist/research/auth-research.js +175 -0
- package/dist/research/explore.d.ts +11 -0
- package/dist/research/explore.js +270 -0
- package/dist/research/search-research.d.ts +17 -0
- package/dist/research/search-research.js +317 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.js +295 -0
- package/dist/test/debug-search.d.ts +10 -0
- package/dist/test/debug-search.js +147 -0
- package/dist/test/mcp-harness.d.ts +17 -0
- package/dist/test/mcp-harness.js +474 -0
- package/dist/tools/auth-tools.d.ts +26 -0
- package/dist/tools/auth-tools.js +191 -0
- package/dist/tools/index.d.ts +56 -0
- package/dist/tools/index.js +34 -0
- package/dist/tools/meeting-tools.d.ts +33 -0
- package/dist/tools/meeting-tools.js +64 -0
- package/dist/tools/message-tools.d.ts +269 -0
- package/dist/tools/message-tools.js +856 -0
- package/dist/tools/people-tools.d.ts +46 -0
- package/dist/tools/people-tools.js +112 -0
- package/dist/tools/registry.d.ts +23 -0
- package/dist/tools/registry.js +63 -0
- package/dist/tools/search-tools.d.ts +91 -0
- package/dist/tools/search-tools.js +222 -0
- package/dist/types/errors.d.ts +58 -0
- package/dist/types/errors.js +132 -0
- package/dist/types/result.d.ts +43 -0
- package/dist/types/result.js +51 -0
- package/dist/types/server.d.ts +27 -0
- package/dist/types/server.js +7 -0
- package/dist/types/teams.d.ts +85 -0
- package/dist/types/teams.js +4 -0
- package/dist/utils/api-config.d.ts +103 -0
- package/dist/utils/api-config.js +158 -0
- package/dist/utils/auth-guards.d.ts +67 -0
- package/dist/utils/auth-guards.js +147 -0
- package/dist/utils/http.d.ts +29 -0
- package/dist/utils/http.js +112 -0
- package/dist/utils/parsers.d.ts +247 -0
- package/dist/utils/parsers.js +731 -0
- package/dist/utils/parsers.test.d.ts +7 -0
- package/dist/utils/parsers.test.js +511 -0
- package/package.json +62 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication guard utilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides reusable auth checks that return Result types for consistent
|
|
5
|
+
* error handling across API modules.
|
|
6
|
+
*/
|
|
7
|
+
import { type McpError } from '../types/errors.js';
|
|
8
|
+
import { type Result } from '../types/result.js';
|
|
9
|
+
import { type MessageAuthInfo, type RegionConfig } from '../auth/token-extractor.js';
|
|
10
|
+
/** Authentication info for messaging and CSA APIs. */
|
|
11
|
+
export interface CsaAuthInfo {
|
|
12
|
+
auth: MessageAuthInfo;
|
|
13
|
+
csaToken: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Requires a valid Substrate token with proactive refresh.
|
|
17
|
+
*
|
|
18
|
+
* This async version attempts to refresh tokens if they're approaching
|
|
19
|
+
* expiry (within 10 minutes). Use this in tool handlers for better UX.
|
|
20
|
+
*/
|
|
21
|
+
export declare function requireSubstrateTokenAsync(): Promise<Result<string, McpError>>;
|
|
22
|
+
/**
|
|
23
|
+
* Requires valid message authentication.
|
|
24
|
+
* Use for chatsvc messaging APIs.
|
|
25
|
+
*/
|
|
26
|
+
export declare function requireMessageAuth(): Result<MessageAuthInfo, McpError>;
|
|
27
|
+
/**
|
|
28
|
+
* Requires valid CSA authentication (message auth + CSA token).
|
|
29
|
+
* Use for favourites and team list APIs.
|
|
30
|
+
*/
|
|
31
|
+
export declare function requireCsaAuth(): Result<CsaAuthInfo, McpError>;
|
|
32
|
+
/** Authentication info for calendar/meetings API. */
|
|
33
|
+
export interface CalendarAuthInfo {
|
|
34
|
+
skypeToken: string;
|
|
35
|
+
spacesToken: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Requires valid calendar authentication (Skype token + Spaces token).
|
|
39
|
+
* Use for mt/part calendar APIs.
|
|
40
|
+
*/
|
|
41
|
+
export declare function requireCalendarAuth(): Result<CalendarAuthInfo, McpError>;
|
|
42
|
+
/**
|
|
43
|
+
* Gets the user's region from session, with caching.
|
|
44
|
+
*
|
|
45
|
+
* The region is extracted from the DISCOVER-REGION-GTM config in localStorage.
|
|
46
|
+
* Falls back to 'amer' if not available (shouldn't happen with valid session).
|
|
47
|
+
*/
|
|
48
|
+
export declare function getRegion(): string;
|
|
49
|
+
/**
|
|
50
|
+
* Gets the Teams base URL from session config.
|
|
51
|
+
*
|
|
52
|
+
* Returns the base URL for API calls (e.g., "https://teams.microsoft.com" for
|
|
53
|
+
* commercial cloud, or "https://teams.microsoft.us" for GCC).
|
|
54
|
+
* Falls back to default if config not available.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getTeamsBaseUrl(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Gets the full region config including partition and URLs.
|
|
59
|
+
*
|
|
60
|
+
* Returns null if no valid session - caller should handle auth error.
|
|
61
|
+
*/
|
|
62
|
+
export declare function getRegionConfig(): RegionConfig | null;
|
|
63
|
+
/**
|
|
64
|
+
* Clears the cached region config.
|
|
65
|
+
* Call this after login/logout to pick up new session.
|
|
66
|
+
*/
|
|
67
|
+
export declare function clearRegionCache(): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication guard utilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides reusable auth checks that return Result types for consistent
|
|
5
|
+
* error handling across API modules.
|
|
6
|
+
*/
|
|
7
|
+
import { ErrorCode, createError } from '../types/errors.js';
|
|
8
|
+
import { err, ok } from '../types/result.js';
|
|
9
|
+
import { getValidSubstrateToken, extractMessageAuth, extractCsaToken, extractSubstrateToken, extractSkypeSpacesToken, extractRegionConfig, } from '../auth/token-extractor.js';
|
|
10
|
+
import { TOKEN_REFRESH_THRESHOLD_MS } from '../constants.js';
|
|
11
|
+
import { refreshTokensViaBrowser } from '../auth/token-refresh.js';
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
// Error Messages
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
const AUTH_ERROR_MESSAGES = {
|
|
16
|
+
messageAuth: 'No valid authentication. Browser login required.',
|
|
17
|
+
csaToken: 'No valid authentication for favourites. Browser login required.',
|
|
18
|
+
};
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
// Guard Functions
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Checks if the Substrate token needs refresh (expired or approaching expiry).
|
|
24
|
+
*
|
|
25
|
+
* @returns true if token is expired or will expire within the refresh threshold
|
|
26
|
+
*/
|
|
27
|
+
function shouldRefreshSubstrateToken() {
|
|
28
|
+
const substrate = extractSubstrateToken();
|
|
29
|
+
if (!substrate)
|
|
30
|
+
return false;
|
|
31
|
+
const timeRemaining = substrate.expiry.getTime() - Date.now();
|
|
32
|
+
// Refresh if expired (timeRemaining <= 0) OR approaching expiry
|
|
33
|
+
return timeRemaining < TOKEN_REFRESH_THRESHOLD_MS;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Requires a valid Substrate token with proactive refresh.
|
|
37
|
+
*
|
|
38
|
+
* This async version attempts to refresh tokens if they're approaching
|
|
39
|
+
* expiry (within 10 minutes). Use this in tool handlers for better UX.
|
|
40
|
+
*/
|
|
41
|
+
export async function requireSubstrateTokenAsync() {
|
|
42
|
+
// Check if we need to refresh proactively
|
|
43
|
+
if (shouldRefreshSubstrateToken()) {
|
|
44
|
+
const refreshResult = await refreshTokensViaBrowser();
|
|
45
|
+
if (refreshResult.ok) {
|
|
46
|
+
// Refresh succeeded, get the new token
|
|
47
|
+
const token = getValidSubstrateToken();
|
|
48
|
+
if (token) {
|
|
49
|
+
return ok(token);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Refresh failed but token might still be valid, continue
|
|
53
|
+
}
|
|
54
|
+
// Try to get existing token
|
|
55
|
+
const token = getValidSubstrateToken();
|
|
56
|
+
if (!token) {
|
|
57
|
+
// Token expired and refresh not available/failed
|
|
58
|
+
return err(createError(ErrorCode.AUTH_EXPIRED, 'Token expired and automatic refresh failed. Please run teams_login to re-authenticate.', { suggestions: ['Call teams_login to re-authenticate'] }));
|
|
59
|
+
}
|
|
60
|
+
return ok(token);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Requires valid message authentication.
|
|
64
|
+
* Use for chatsvc messaging APIs.
|
|
65
|
+
*/
|
|
66
|
+
export function requireMessageAuth() {
|
|
67
|
+
const auth = extractMessageAuth();
|
|
68
|
+
if (!auth) {
|
|
69
|
+
return err(createError(ErrorCode.AUTH_REQUIRED, AUTH_ERROR_MESSAGES.messageAuth));
|
|
70
|
+
}
|
|
71
|
+
return ok(auth);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Requires valid CSA authentication (message auth + CSA token).
|
|
75
|
+
* Use for favourites and team list APIs.
|
|
76
|
+
*/
|
|
77
|
+
export function requireCsaAuth() {
|
|
78
|
+
const auth = extractMessageAuth();
|
|
79
|
+
const csaToken = extractCsaToken();
|
|
80
|
+
if (!auth?.skypeToken || !csaToken) {
|
|
81
|
+
return err(createError(ErrorCode.AUTH_REQUIRED, AUTH_ERROR_MESSAGES.csaToken));
|
|
82
|
+
}
|
|
83
|
+
return ok({ auth, csaToken });
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Requires valid calendar authentication (Skype token + Spaces token).
|
|
87
|
+
* Use for mt/part calendar APIs.
|
|
88
|
+
*/
|
|
89
|
+
export function requireCalendarAuth() {
|
|
90
|
+
const auth = extractMessageAuth();
|
|
91
|
+
const spacesToken = extractSkypeSpacesToken();
|
|
92
|
+
if (!auth?.skypeToken || !spacesToken) {
|
|
93
|
+
return err(createError(ErrorCode.AUTH_REQUIRED, 'Calendar access requires authentication. Please run teams_login.', { suggestions: ['Call teams_login to authenticate'] }));
|
|
94
|
+
}
|
|
95
|
+
return ok({ skypeToken: auth.skypeToken, spacesToken });
|
|
96
|
+
}
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
// Region Configuration
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
import { DEFAULT_TEAMS_BASE_URL } from './api-config.js';
|
|
101
|
+
/** Default region when session config is unavailable. */
|
|
102
|
+
const DEFAULT_REGION = 'amer';
|
|
103
|
+
/** Cached region config to avoid repeated localStorage parsing. */
|
|
104
|
+
let cachedRegionConfig = null;
|
|
105
|
+
/**
|
|
106
|
+
* Gets the user's region from session, with caching.
|
|
107
|
+
*
|
|
108
|
+
* The region is extracted from the DISCOVER-REGION-GTM config in localStorage.
|
|
109
|
+
* Falls back to 'amer' if not available (shouldn't happen with valid session).
|
|
110
|
+
*/
|
|
111
|
+
export function getRegion() {
|
|
112
|
+
if (!cachedRegionConfig) {
|
|
113
|
+
cachedRegionConfig = extractRegionConfig();
|
|
114
|
+
}
|
|
115
|
+
return cachedRegionConfig?.region ?? DEFAULT_REGION;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Gets the Teams base URL from session config.
|
|
119
|
+
*
|
|
120
|
+
* Returns the base URL for API calls (e.g., "https://teams.microsoft.com" for
|
|
121
|
+
* commercial cloud, or "https://teams.microsoft.us" for GCC).
|
|
122
|
+
* Falls back to default if config not available.
|
|
123
|
+
*/
|
|
124
|
+
export function getTeamsBaseUrl() {
|
|
125
|
+
if (!cachedRegionConfig) {
|
|
126
|
+
cachedRegionConfig = extractRegionConfig();
|
|
127
|
+
}
|
|
128
|
+
return cachedRegionConfig?.teamsBaseUrl ?? DEFAULT_TEAMS_BASE_URL;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Gets the full region config including partition and URLs.
|
|
132
|
+
*
|
|
133
|
+
* Returns null if no valid session - caller should handle auth error.
|
|
134
|
+
*/
|
|
135
|
+
export function getRegionConfig() {
|
|
136
|
+
if (!cachedRegionConfig) {
|
|
137
|
+
cachedRegionConfig = extractRegionConfig();
|
|
138
|
+
}
|
|
139
|
+
return cachedRegionConfig;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Clears the cached region config.
|
|
143
|
+
* Call this after login/logout to pick up new session.
|
|
144
|
+
*/
|
|
145
|
+
export function clearRegionCache() {
|
|
146
|
+
cachedRegionConfig = null;
|
|
147
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP utilities with retry, timeout, and error handling.
|
|
3
|
+
*/
|
|
4
|
+
import { type Result } from '../types/result.js';
|
|
5
|
+
/** Options for HTTP requests. */
|
|
6
|
+
export interface HttpOptions extends Omit<RequestInit, 'signal'> {
|
|
7
|
+
/** Timeout in milliseconds (default: 30000). */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
/** Maximum retry attempts (default: 3). */
|
|
10
|
+
maxRetries?: number;
|
|
11
|
+
/** Base delay for exponential backoff in ms (default: 1000). */
|
|
12
|
+
retryBaseDelayMs?: number;
|
|
13
|
+
/** Maximum delay between retries in ms (default: 10000). */
|
|
14
|
+
retryMaxDelayMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/** Successful HTTP response. */
|
|
17
|
+
export interface HttpResponse<T = unknown> {
|
|
18
|
+
status: number;
|
|
19
|
+
headers: Headers;
|
|
20
|
+
data: T;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Makes an HTTP request with timeout, retry, and error handling.
|
|
24
|
+
*/
|
|
25
|
+
export declare function httpRequest<T = unknown>(url: string, options?: HttpOptions): Promise<Result<HttpResponse<T>>>;
|
|
26
|
+
/**
|
|
27
|
+
* Clears the rate limit state (for testing).
|
|
28
|
+
*/
|
|
29
|
+
export declare function clearRateLimitState(): void;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP utilities with retry, timeout, and error handling.
|
|
3
|
+
*/
|
|
4
|
+
import { ErrorCode, createError, classifyHttpError, extractRetryAfter, } from '../types/errors.js';
|
|
5
|
+
import { ok, err } from '../types/result.js';
|
|
6
|
+
/** Rate limit state tracking. */
|
|
7
|
+
let rateLimitedUntil = null;
|
|
8
|
+
/**
|
|
9
|
+
* Makes an HTTP request with timeout, retry, and error handling.
|
|
10
|
+
*/
|
|
11
|
+
export async function httpRequest(url, options = {}) {
|
|
12
|
+
const { timeoutMs = 30000, maxRetries = 3, retryBaseDelayMs = 1000, retryMaxDelayMs = 10000, ...fetchOptions } = options;
|
|
13
|
+
// Check rate limit state
|
|
14
|
+
if (rateLimitedUntil && Date.now() < rateLimitedUntil) {
|
|
15
|
+
const waitMs = rateLimitedUntil - Date.now();
|
|
16
|
+
return err(createError(ErrorCode.RATE_LIMITED, `Rate limited. Retry after ${Math.ceil(waitMs / 1000)}s`, { retryable: true, retryAfterMs: waitMs }));
|
|
17
|
+
}
|
|
18
|
+
let lastError = null;
|
|
19
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
const result = await fetchWithTimeout(url, fetchOptions, timeoutMs);
|
|
22
|
+
if (result.ok) {
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
lastError = result.error;
|
|
26
|
+
// Handle rate limiting
|
|
27
|
+
if (result.error.code === ErrorCode.RATE_LIMITED && result.error.retryAfterMs) {
|
|
28
|
+
rateLimitedUntil = Date.now() + result.error.retryAfterMs;
|
|
29
|
+
}
|
|
30
|
+
// Don't retry non-retryable errors
|
|
31
|
+
if (!result.error.retryable) {
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
// Don't retry on last attempt
|
|
35
|
+
if (attempt === maxRetries) {
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
// Calculate backoff delay
|
|
39
|
+
const delay = Math.min(retryBaseDelayMs * Math.pow(2, attempt - 1), retryMaxDelayMs);
|
|
40
|
+
await sleep(delay);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
// Unexpected errors
|
|
44
|
+
lastError = createError(ErrorCode.UNKNOWN, error instanceof Error ? error.message : String(error), { retryable: false });
|
|
45
|
+
if (attempt === maxRetries) {
|
|
46
|
+
return err(lastError);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return err(lastError ?? createError(ErrorCode.UNKNOWN, 'Request failed'));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Makes a single fetch request with timeout.
|
|
54
|
+
*/
|
|
55
|
+
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(url, {
|
|
60
|
+
...options,
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
});
|
|
63
|
+
clearTimeout(timeoutId);
|
|
64
|
+
// Handle error responses
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const retryAfterMs = extractRetryAfter(response.headers);
|
|
67
|
+
const errorText = await response.text().catch(() => '');
|
|
68
|
+
return err(createError(classifyHttpError(response.status, errorText), `HTTP ${response.status}: ${errorText || response.statusText}`, { retryAfterMs }));
|
|
69
|
+
}
|
|
70
|
+
// Parse JSON response
|
|
71
|
+
const contentType = response.headers.get('content-type') || '';
|
|
72
|
+
let data;
|
|
73
|
+
if (contentType.includes('application/json')) {
|
|
74
|
+
const text = await response.text();
|
|
75
|
+
data = text ? JSON.parse(text) : {};
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
data = await response.text();
|
|
79
|
+
}
|
|
80
|
+
return ok({
|
|
81
|
+
status: response.status,
|
|
82
|
+
headers: response.headers,
|
|
83
|
+
data,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
if (error instanceof Error) {
|
|
89
|
+
if (error.name === 'AbortError') {
|
|
90
|
+
return err(createError(ErrorCode.TIMEOUT, `Request timed out after ${timeoutMs}ms`, { retryable: true }));
|
|
91
|
+
}
|
|
92
|
+
if (error.message.includes('ECONNRESET') ||
|
|
93
|
+
error.message.includes('ETIMEDOUT') ||
|
|
94
|
+
error.message.includes('ENOTFOUND')) {
|
|
95
|
+
return err(createError(ErrorCode.NETWORK_ERROR, error.message, { retryable: true }));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return err(createError(ErrorCode.UNKNOWN, error instanceof Error ? error.message : String(error), { retryable: false }));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Sleep for a specified duration.
|
|
103
|
+
*/
|
|
104
|
+
function sleep(ms) {
|
|
105
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Clears the rate limit state (for testing).
|
|
109
|
+
*/
|
|
110
|
+
export function clearRateLimitState() {
|
|
111
|
+
rateLimitedUntil = null;
|
|
112
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parsing functions for Teams API responses.
|
|
3
|
+
*
|
|
4
|
+
* These functions transform raw API responses into our internal types.
|
|
5
|
+
* They are extracted here for testability - no side effects or external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
import type { TeamsSearchResult, ExtractedLink } from '../types/teams.js';
|
|
8
|
+
export type { ExtractedLink };
|
|
9
|
+
/** Person search result from Substrate suggestions API. */
|
|
10
|
+
export interface PersonSearchResult {
|
|
11
|
+
id: string;
|
|
12
|
+
mri: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
givenName?: string;
|
|
16
|
+
surname?: string;
|
|
17
|
+
jobTitle?: string;
|
|
18
|
+
department?: string;
|
|
19
|
+
companyName?: string;
|
|
20
|
+
}
|
|
21
|
+
/** User profile extracted from JWT tokens. */
|
|
22
|
+
export interface UserProfile {
|
|
23
|
+
id: string;
|
|
24
|
+
mri: string;
|
|
25
|
+
email: string;
|
|
26
|
+
displayName: string;
|
|
27
|
+
givenName?: string;
|
|
28
|
+
surname?: string;
|
|
29
|
+
tenantId?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extracts links from HTML content before stripping.
|
|
33
|
+
* Returns an array of { url, text } objects.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractLinks(html: string): ExtractedLink[];
|
|
36
|
+
/**
|
|
37
|
+
* Strips HTML tags from content for display.
|
|
38
|
+
*/
|
|
39
|
+
export declare function stripHtml(html: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Determines the conversation type from a Teams conversation ID.
|
|
42
|
+
*
|
|
43
|
+
* Conversation ID formats:
|
|
44
|
+
* - Channels: 19:xxx@thread.tacv2
|
|
45
|
+
* - Meetings: 19:meeting_xxx@thread.v2
|
|
46
|
+
* - 1:1 chats: 19:guid_guid@unq.gbl.spaces
|
|
47
|
+
* - Group chats: 19:xxx@thread.v2 (non-meeting)
|
|
48
|
+
*/
|
|
49
|
+
export declare function getConversationType(conversationId: string): 'channel' | 'meeting' | 'chat';
|
|
50
|
+
/**
|
|
51
|
+
* Builds a deep link to open a message in Teams.
|
|
52
|
+
*
|
|
53
|
+
* Different conversation types require different URL formats:
|
|
54
|
+
* - Channels: /l/message/{channelId}/{msgId}?parentMessageId={parentId} (for thread replies)
|
|
55
|
+
* - Chats/Meetings: /l/message/{chatId}/{msgId}?context={"contextType":"chat"}
|
|
56
|
+
*
|
|
57
|
+
* @param conversationId - The conversation/thread ID (e.g., "19:xxx@thread.tacv2")
|
|
58
|
+
* @param messageTimestamp - The message timestamp in epoch milliseconds
|
|
59
|
+
* @param parentMessageId - For channel thread replies, the ID of the parent/root message
|
|
60
|
+
* @param teamsBaseUrl - Optional Teams base URL (for GCC/GCC-High support)
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildMessageLink(conversationId: string, messageTimestamp: string | number, parentMessageId?: string, teamsBaseUrl?: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Extracts a timestamp-based message ID from various sources.
|
|
65
|
+
* Teams uses epoch milliseconds as message IDs in URLs.
|
|
66
|
+
*
|
|
67
|
+
* IMPORTANT: For channel threaded replies, the ;messageid= in ClientConversationId
|
|
68
|
+
* is the PARENT thread's ID, not this message's ID. We must prefer the actual
|
|
69
|
+
* message timestamp (DateTimeReceived/DateTimeSent) for accurate deep links.
|
|
70
|
+
*/
|
|
71
|
+
export declare function extractMessageTimestamp(source: Record<string, unknown> | undefined, timestamp?: string): string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Parses a person suggestion from the Substrate API response.
|
|
74
|
+
*
|
|
75
|
+
* The API can return IDs in various formats:
|
|
76
|
+
* - GUID with tenant: "ab76f827-...@tenant.onmicrosoft.com"
|
|
77
|
+
* - Base64-encoded GUID: "93qkaTtFGWpUHjyRafgdhg=="
|
|
78
|
+
*/
|
|
79
|
+
export declare function parsePersonSuggestion(item: Record<string, unknown>): PersonSearchResult | null;
|
|
80
|
+
/**
|
|
81
|
+
* Parses a v2 query result item into a search result.
|
|
82
|
+
*/
|
|
83
|
+
export declare function parseV2Result(item: Record<string, unknown>): TeamsSearchResult | null;
|
|
84
|
+
/**
|
|
85
|
+
* Parses user profile from a JWT payload.
|
|
86
|
+
*
|
|
87
|
+
* @param payload - Decoded JWT payload object
|
|
88
|
+
* @returns User profile or null if required fields are missing
|
|
89
|
+
*/
|
|
90
|
+
export declare function parseJwtProfile(payload: Record<string, unknown>): UserProfile | null;
|
|
91
|
+
/**
|
|
92
|
+
* Calculates token expiry status from an expiry timestamp.
|
|
93
|
+
*
|
|
94
|
+
* @param expiryMs - Token expiry time in milliseconds since epoch
|
|
95
|
+
* @param nowMs - Current time in milliseconds (for testing)
|
|
96
|
+
* @returns Token status including whether it's valid and time remaining
|
|
97
|
+
*/
|
|
98
|
+
export declare function calculateTokenStatus(expiryMs: number, nowMs?: number): {
|
|
99
|
+
isValid: boolean;
|
|
100
|
+
expiresAt: string;
|
|
101
|
+
minutesRemaining: number;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Parses the pagination result from a search API response.
|
|
105
|
+
*
|
|
106
|
+
* @param entitySets - Raw EntitySets array from API response
|
|
107
|
+
* @returns Parsed results and total count if available
|
|
108
|
+
*/
|
|
109
|
+
export declare function parseSearchResults(entitySets: unknown[] | undefined): {
|
|
110
|
+
results: TeamsSearchResult[];
|
|
111
|
+
total?: number;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Parses people search results from the Groups/Suggestions structure.
|
|
115
|
+
*
|
|
116
|
+
* @param groups - Raw Groups array from suggestions API response
|
|
117
|
+
* @returns Array of parsed person results
|
|
118
|
+
*/
|
|
119
|
+
export declare function parsePeopleResults(groups: unknown[] | undefined): PersonSearchResult[];
|
|
120
|
+
/** Channel search result from Substrate suggestions API or Teams List API. */
|
|
121
|
+
export interface ChannelSearchResult {
|
|
122
|
+
channelId: string;
|
|
123
|
+
channelName: string;
|
|
124
|
+
teamName: string;
|
|
125
|
+
teamId: string;
|
|
126
|
+
channelType: string;
|
|
127
|
+
description?: string;
|
|
128
|
+
isMember?: boolean;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Parses a single channel suggestion from the API response.
|
|
132
|
+
*
|
|
133
|
+
* @param suggestion - Raw suggestion object from API
|
|
134
|
+
* @returns Parsed channel result or null if required fields are missing
|
|
135
|
+
*/
|
|
136
|
+
export declare function parseChannelSuggestion(suggestion: Record<string, unknown>): ChannelSearchResult | null;
|
|
137
|
+
/**
|
|
138
|
+
* Parses channel search results from the Groups/Suggestions structure.
|
|
139
|
+
*
|
|
140
|
+
* @param groups - Raw Groups array from suggestions API response
|
|
141
|
+
* @returns Array of parsed channel results
|
|
142
|
+
*/
|
|
143
|
+
export declare function parseChannelResults(groups: unknown[] | undefined): ChannelSearchResult[];
|
|
144
|
+
/** Team with channels from the Teams List API response. */
|
|
145
|
+
export interface TeamWithChannels {
|
|
146
|
+
teamId: string;
|
|
147
|
+
teamName: string;
|
|
148
|
+
threadId: string;
|
|
149
|
+
description?: string;
|
|
150
|
+
channels: ChannelSearchResult[];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Parses the Teams List API response to extract all teams and channels.
|
|
154
|
+
*
|
|
155
|
+
* @param data - Raw response data from /api/csa/{region}/api/v3/teams/users/me
|
|
156
|
+
* @returns Array of teams with their channels
|
|
157
|
+
*/
|
|
158
|
+
export declare function parseTeamsList(data: Record<string, unknown> | undefined): TeamWithChannels[];
|
|
159
|
+
/**
|
|
160
|
+
* Filters channels from the Teams List by name.
|
|
161
|
+
*
|
|
162
|
+
* @param teams - Array of teams with channels from parseTeamsList
|
|
163
|
+
* @param query - Search query (case-insensitive partial match)
|
|
164
|
+
* @returns Matching channels flattened into a single array
|
|
165
|
+
*/
|
|
166
|
+
export declare function filterChannelsByName(teams: TeamWithChannels[], query: string): ChannelSearchResult[];
|
|
167
|
+
/**
|
|
168
|
+
* Decodes a base64-encoded GUID to its standard string representation.
|
|
169
|
+
*
|
|
170
|
+
* Microsoft encodes GUIDs as 16 bytes with little-endian ordering for the
|
|
171
|
+
* first three groups (Data1, Data2, Data3).
|
|
172
|
+
*
|
|
173
|
+
* @param base64 - Base64-encoded GUID (typically 24 chars with == padding)
|
|
174
|
+
* @returns The GUID string in standard format, or null if invalid
|
|
175
|
+
*/
|
|
176
|
+
export declare function decodeBase64Guid(base64: string): string | null;
|
|
177
|
+
/**
|
|
178
|
+
* Extracts the Azure AD object ID (GUID) from various formats.
|
|
179
|
+
*
|
|
180
|
+
* Handles:
|
|
181
|
+
* - MRI format: "8:orgid:ab76f827-27e2-4c67-a765-f1a53145fa24"
|
|
182
|
+
* - MRI with base64: "8:orgid:93qkaTtFGWpUHjyRafgdhg=="
|
|
183
|
+
* - Skype ID format: "orgid:ab76f827-27e2-4c67-a765-f1a53145fa24"
|
|
184
|
+
* - ID with tenant: "ab76f827-27e2-4c67-a765-f1a53145fa24@56b731a8-..."
|
|
185
|
+
* - Raw GUID: "ab76f827-27e2-4c67-a765-f1a53145fa24"
|
|
186
|
+
* - Base64-encoded GUID: "93qkaTtFGWpUHjyRafgdhg=="
|
|
187
|
+
*
|
|
188
|
+
* @param identifier - User identifier in any supported format
|
|
189
|
+
* @returns The extracted GUID or null if invalid format
|
|
190
|
+
*/
|
|
191
|
+
export declare function extractObjectId(identifier: string): string | null;
|
|
192
|
+
/**
|
|
193
|
+
* Builds a 1:1 conversation ID from two user object IDs.
|
|
194
|
+
*
|
|
195
|
+
* The conversation ID format for 1:1 chats in Teams is:
|
|
196
|
+
* `19:{userId1}_{userId2}@unq.gbl.spaces`
|
|
197
|
+
*
|
|
198
|
+
* The user IDs are sorted lexicographically to ensure consistency -
|
|
199
|
+
* both participants will generate the same conversation ID.
|
|
200
|
+
*
|
|
201
|
+
* @param userId1 - First user's object ID (GUID, MRI, or ID with tenant)
|
|
202
|
+
* @param userId2 - Second user's object ID (GUID, MRI, or ID with tenant)
|
|
203
|
+
* @returns The constructed conversation ID, or null if either ID is invalid
|
|
204
|
+
*/
|
|
205
|
+
export declare function buildOneOnOneConversationId(userId1: string, userId2: string): string | null;
|
|
206
|
+
/**
|
|
207
|
+
* Safely extracts a timestamp from an activity feed message.
|
|
208
|
+
*
|
|
209
|
+
* Tries multiple sources in order of preference:
|
|
210
|
+
* 1. originalarrivaltime - Primary timestamp field
|
|
211
|
+
* 2. composetime - When message was composed
|
|
212
|
+
* 3. id as numeric timestamp - Fallback if ID is a Unix timestamp
|
|
213
|
+
*
|
|
214
|
+
* Returns null if no valid timestamp can be determined, preventing
|
|
215
|
+
* RangeError from Date operations on invalid values.
|
|
216
|
+
*
|
|
217
|
+
* @param msg - Raw message object from activity feed API
|
|
218
|
+
* @returns ISO timestamp string, or null if no valid timestamp found
|
|
219
|
+
*/
|
|
220
|
+
export declare function extractActivityTimestamp(msg: Record<string, unknown>): string | null;
|
|
221
|
+
/** Common fields from a virtual conversation message (48:saved, 48:threads, etc). */
|
|
222
|
+
export interface VirtualConversationItem {
|
|
223
|
+
id: string;
|
|
224
|
+
content: string;
|
|
225
|
+
contentType: string;
|
|
226
|
+
sender: {
|
|
227
|
+
mri: string;
|
|
228
|
+
displayName?: string;
|
|
229
|
+
};
|
|
230
|
+
timestamp: string;
|
|
231
|
+
sourceConversationId: string;
|
|
232
|
+
sourceReferenceId?: string;
|
|
233
|
+
messageLink?: string;
|
|
234
|
+
links?: ExtractedLink[];
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Parses a raw message from a virtual conversation (48:saved, 48:threads, etc).
|
|
238
|
+
*
|
|
239
|
+
* Virtual conversations contain references to messages in other conversations.
|
|
240
|
+
* The clumpId field contains the source conversation ID, and secondaryReferenceId
|
|
241
|
+
* contains a composite key with the source message/post ID.
|
|
242
|
+
*
|
|
243
|
+
* @param msg - Raw message object from virtual conversation API
|
|
244
|
+
* @param referencePattern - Regex to extract source ID from secondaryReferenceId
|
|
245
|
+
* @returns Parsed virtual conversation item, or null if message should be skipped
|
|
246
|
+
*/
|
|
247
|
+
export declare function parseVirtualConversationMessage(msg: Record<string, unknown>, referencePattern: RegExp): VirtualConversationItem | null;
|