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,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSA (Chat Service Aggregator) API client for favorites and teams operations.
|
|
3
|
+
*
|
|
4
|
+
* Handles all calls to teams.microsoft.com/api/csa endpoints.
|
|
5
|
+
* Base URL is extracted from session config to support different Teams environments.
|
|
6
|
+
*/
|
|
7
|
+
import { type Result } from '../types/result.js';
|
|
8
|
+
import { type TeamWithChannels } from '../utils/parsers.js';
|
|
9
|
+
/** A favourite/pinned conversation item. */
|
|
10
|
+
export interface FavoriteItem {
|
|
11
|
+
conversationId: string;
|
|
12
|
+
displayName?: string;
|
|
13
|
+
conversationType?: string;
|
|
14
|
+
createdTime?: number;
|
|
15
|
+
lastUpdatedTime?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Response from getting favorites. */
|
|
18
|
+
export interface FavoritesResult {
|
|
19
|
+
favorites: FavoriteItem[];
|
|
20
|
+
folderHierarchyVersion?: number;
|
|
21
|
+
folderId?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Gets the user's favourite/pinned conversations.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getFavorites(): Promise<Result<FavoritesResult>>;
|
|
27
|
+
/**
|
|
28
|
+
* Adds a conversation to the user's favourites.
|
|
29
|
+
*/
|
|
30
|
+
export declare function addFavorite(conversationId: string): Promise<Result<void>>;
|
|
31
|
+
/**
|
|
32
|
+
* Removes a conversation from the user's favourites.
|
|
33
|
+
*/
|
|
34
|
+
export declare function removeFavorite(conversationId: string): Promise<Result<void>>;
|
|
35
|
+
/** Response from getting the user's teams and channels. */
|
|
36
|
+
export interface TeamsListResult {
|
|
37
|
+
teams: TeamWithChannels[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Gets all teams and channels the user is a member of.
|
|
41
|
+
*
|
|
42
|
+
* This returns the complete list of teams with their channels - not a search,
|
|
43
|
+
* but a full enumeration of the user's memberships.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getMyTeamsAndChannels(): Promise<Result<TeamsListResult>>;
|
|
46
|
+
/** A custom emoji from the organisation. */
|
|
47
|
+
export interface CustomEmoji {
|
|
48
|
+
/** The emoji ID (use as reaction key). */
|
|
49
|
+
id: string;
|
|
50
|
+
/** Short name/shortcut for the emoji. */
|
|
51
|
+
shortcut: string;
|
|
52
|
+
/** Description of the emoji. */
|
|
53
|
+
description: string;
|
|
54
|
+
/** When the emoji was created. */
|
|
55
|
+
createdOn?: number;
|
|
56
|
+
}
|
|
57
|
+
/** Response from getting custom emojis. */
|
|
58
|
+
export interface CustomEmojisResult {
|
|
59
|
+
emojis: CustomEmoji[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Gets the organisation's custom emojis.
|
|
63
|
+
*/
|
|
64
|
+
export declare function getCustomEmojis(): Promise<Result<CustomEmojisResult>>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSA (Chat Service Aggregator) API client for favorites and teams operations.
|
|
3
|
+
*
|
|
4
|
+
* Handles all calls to teams.microsoft.com/api/csa endpoints.
|
|
5
|
+
* Base URL is extracted from session config to support different Teams environments.
|
|
6
|
+
*/
|
|
7
|
+
import { httpRequest } from '../utils/http.js';
|
|
8
|
+
import { CSA_API, getCsaHeaders } from '../utils/api-config.js';
|
|
9
|
+
import { ErrorCode, createError } from '../types/errors.js';
|
|
10
|
+
import { ok, err } from '../types/result.js';
|
|
11
|
+
import { requireCsaAuth, getRegion, getTeamsBaseUrl } from '../utils/auth-guards.js';
|
|
12
|
+
import { getConversationProperties, extractParticipantNames, } from './chatsvc-api.js';
|
|
13
|
+
import { parseTeamsList, } from '../utils/parsers.js';
|
|
14
|
+
/** Gets region and base URL together for API calls. */
|
|
15
|
+
function getApiConfig() {
|
|
16
|
+
return {
|
|
17
|
+
region: getRegion(),
|
|
18
|
+
baseUrl: getTeamsBaseUrl(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Gets the user's favourite/pinned conversations.
|
|
23
|
+
*/
|
|
24
|
+
export async function getFavorites() {
|
|
25
|
+
const authResult = requireCsaAuth();
|
|
26
|
+
if (!authResult.ok) {
|
|
27
|
+
return authResult;
|
|
28
|
+
}
|
|
29
|
+
const { auth, csaToken } = authResult.value;
|
|
30
|
+
const { region, baseUrl } = getApiConfig();
|
|
31
|
+
const url = CSA_API.conversationFolders(region, baseUrl);
|
|
32
|
+
const response = await httpRequest(url, {
|
|
33
|
+
method: 'GET',
|
|
34
|
+
headers: getCsaHeaders(auth.skypeToken, csaToken, baseUrl),
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
return response;
|
|
38
|
+
}
|
|
39
|
+
const data = response.value.data;
|
|
40
|
+
// Find the Favorites folder
|
|
41
|
+
const folders = data.conversationFolders;
|
|
42
|
+
const favoritesFolder = folders?.find((f) => {
|
|
43
|
+
const folder = f;
|
|
44
|
+
return folder.folderType === 'Favorites';
|
|
45
|
+
});
|
|
46
|
+
if (!favoritesFolder) {
|
|
47
|
+
return ok({
|
|
48
|
+
favorites: [],
|
|
49
|
+
folderHierarchyVersion: data.folderHierarchyVersion,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const items = favoritesFolder.conversationFolderItems;
|
|
53
|
+
const favorites = (items || []).map((item) => {
|
|
54
|
+
const i = item;
|
|
55
|
+
return {
|
|
56
|
+
conversationId: i.conversationId,
|
|
57
|
+
createdTime: i.createdTime,
|
|
58
|
+
lastUpdatedTime: i.lastUpdatedTime,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
// Enrich favorites with display names in parallel
|
|
62
|
+
const enrichmentPromises = favorites.map(async (fav) => {
|
|
63
|
+
const props = await getConversationProperties(fav.conversationId);
|
|
64
|
+
if (props.ok) {
|
|
65
|
+
fav.displayName = props.value.displayName;
|
|
66
|
+
fav.conversationType = props.value.conversationType;
|
|
67
|
+
}
|
|
68
|
+
// Fallback: extract from recent messages if no display name
|
|
69
|
+
if (!fav.displayName) {
|
|
70
|
+
const names = await extractParticipantNames(fav.conversationId);
|
|
71
|
+
if (names.ok && names.value) {
|
|
72
|
+
fav.displayName = names.value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
await Promise.allSettled(enrichmentPromises);
|
|
77
|
+
return ok({
|
|
78
|
+
favorites,
|
|
79
|
+
folderHierarchyVersion: data.folderHierarchyVersion,
|
|
80
|
+
folderId: favoritesFolder.id,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Adds a conversation to the user's favourites.
|
|
85
|
+
*/
|
|
86
|
+
export async function addFavorite(conversationId) {
|
|
87
|
+
return modifyFavorite(conversationId, 'AddItem');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Removes a conversation from the user's favourites.
|
|
91
|
+
*/
|
|
92
|
+
export async function removeFavorite(conversationId) {
|
|
93
|
+
return modifyFavorite(conversationId, 'RemoveItem');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Internal helper to modify the favourites folder.
|
|
97
|
+
*/
|
|
98
|
+
async function modifyFavorite(conversationId, action) {
|
|
99
|
+
const authResult = requireCsaAuth();
|
|
100
|
+
if (!authResult.ok) {
|
|
101
|
+
return authResult;
|
|
102
|
+
}
|
|
103
|
+
const { auth, csaToken } = authResult.value;
|
|
104
|
+
const { region, baseUrl } = getApiConfig();
|
|
105
|
+
// Get current folder state
|
|
106
|
+
const currentState = await getFavorites();
|
|
107
|
+
if (!currentState.ok) {
|
|
108
|
+
return err(currentState.error);
|
|
109
|
+
}
|
|
110
|
+
if (!currentState.value.folderId) {
|
|
111
|
+
return err(createError(ErrorCode.NOT_FOUND, 'Could not find Favorites folder'));
|
|
112
|
+
}
|
|
113
|
+
const url = CSA_API.conversationFolders(region, baseUrl);
|
|
114
|
+
const response = await httpRequest(url, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: getCsaHeaders(auth.skypeToken, csaToken, baseUrl),
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
folderHierarchyVersion: currentState.value.folderHierarchyVersion,
|
|
119
|
+
actions: [
|
|
120
|
+
{
|
|
121
|
+
action,
|
|
122
|
+
folderId: currentState.value.folderId,
|
|
123
|
+
itemId: conversationId,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
return response;
|
|
130
|
+
}
|
|
131
|
+
return ok(undefined);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Gets all teams and channels the user is a member of.
|
|
135
|
+
*
|
|
136
|
+
* This returns the complete list of teams with their channels - not a search,
|
|
137
|
+
* but a full enumeration of the user's memberships.
|
|
138
|
+
*/
|
|
139
|
+
export async function getMyTeamsAndChannels() {
|
|
140
|
+
const authResult = requireCsaAuth();
|
|
141
|
+
if (!authResult.ok) {
|
|
142
|
+
return authResult;
|
|
143
|
+
}
|
|
144
|
+
const { auth, csaToken } = authResult.value;
|
|
145
|
+
const { region, baseUrl } = getApiConfig();
|
|
146
|
+
const url = CSA_API.teamsList(region, baseUrl);
|
|
147
|
+
const response = await httpRequest(url, {
|
|
148
|
+
method: 'GET',
|
|
149
|
+
headers: getCsaHeaders(auth.skypeToken, csaToken, baseUrl),
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
return response;
|
|
153
|
+
}
|
|
154
|
+
const teams = parseTeamsList(response.value.data);
|
|
155
|
+
return ok({ teams });
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Gets the organisation's custom emojis.
|
|
159
|
+
*/
|
|
160
|
+
export async function getCustomEmojis() {
|
|
161
|
+
const authResult = requireCsaAuth();
|
|
162
|
+
if (!authResult.ok) {
|
|
163
|
+
return authResult;
|
|
164
|
+
}
|
|
165
|
+
const { auth, csaToken } = authResult.value;
|
|
166
|
+
const { region, baseUrl } = getApiConfig();
|
|
167
|
+
const url = CSA_API.customEmojis(region, baseUrl);
|
|
168
|
+
const response = await httpRequest(url, {
|
|
169
|
+
method: 'GET',
|
|
170
|
+
headers: getCsaHeaders(auth.skypeToken, csaToken, baseUrl),
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
return response;
|
|
174
|
+
}
|
|
175
|
+
const data = response.value.data;
|
|
176
|
+
const categories = data.categories;
|
|
177
|
+
const emojis = [];
|
|
178
|
+
if (categories) {
|
|
179
|
+
for (const category of categories) {
|
|
180
|
+
const emoticons = category.emoticons;
|
|
181
|
+
if (emoticons) {
|
|
182
|
+
for (const emoticon of emoticons) {
|
|
183
|
+
if (emoticon.isDeleted)
|
|
184
|
+
continue;
|
|
185
|
+
const id = emoticon.id;
|
|
186
|
+
const shortcuts = emoticon.shortcuts;
|
|
187
|
+
const description = emoticon.description;
|
|
188
|
+
const createdOn = emoticon.createdOn;
|
|
189
|
+
emojis.push({
|
|
190
|
+
id,
|
|
191
|
+
shortcut: shortcuts?.[0] || id.split(';')[0],
|
|
192
|
+
description: description || shortcuts?.[0] || id.split(';')[0],
|
|
193
|
+
createdOn,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return ok({ emojis });
|
|
200
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Substrate API client for search and people operations.
|
|
3
|
+
*
|
|
4
|
+
* Handles all calls to substrate.office.com endpoints.
|
|
5
|
+
*/
|
|
6
|
+
import { type Result } from '../types/result.js';
|
|
7
|
+
import { type PersonSearchResult, type ChannelSearchResult } from '../utils/parsers.js';
|
|
8
|
+
import type { TeamsSearchResult, SearchPaginationResult } from '../types/teams.js';
|
|
9
|
+
/** Search result with pagination. */
|
|
10
|
+
export interface SearchResult {
|
|
11
|
+
results: TeamsSearchResult[];
|
|
12
|
+
pagination: SearchPaginationResult;
|
|
13
|
+
}
|
|
14
|
+
/** People search result. */
|
|
15
|
+
export interface PeopleSearchResult {
|
|
16
|
+
results: PersonSearchResult[];
|
|
17
|
+
returned: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Searches Teams messages using the Substrate v2 query API.
|
|
21
|
+
*/
|
|
22
|
+
export declare function searchMessages(query: string, options?: {
|
|
23
|
+
from?: number;
|
|
24
|
+
size?: number;
|
|
25
|
+
maxResults?: number;
|
|
26
|
+
}): Promise<Result<SearchResult>>;
|
|
27
|
+
/**
|
|
28
|
+
* Searches for people by name or email.
|
|
29
|
+
*/
|
|
30
|
+
export declare function searchPeople(query: string, limit?: number): Promise<Result<PeopleSearchResult>>;
|
|
31
|
+
/**
|
|
32
|
+
* Gets the user's frequently contacted people.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getFrequentContacts(limit?: number): Promise<Result<PeopleSearchResult>>;
|
|
35
|
+
/** Channel search result. */
|
|
36
|
+
export interface ChannelSearchResultSet {
|
|
37
|
+
results: ChannelSearchResult[];
|
|
38
|
+
returned: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Searches for Teams channels by name using both:
|
|
42
|
+
* 1. User's own teams/channels (Teams List API) - reliable, shows membership
|
|
43
|
+
* 2. Organisation-wide discovery (Substrate suggestions) - broader but less reliable
|
|
44
|
+
*
|
|
45
|
+
* Results are merged and deduplicated, with membership status indicated.
|
|
46
|
+
*
|
|
47
|
+
* @param query - Channel name to search for
|
|
48
|
+
* @param limit - Maximum number of results (default: 10, max: 50)
|
|
49
|
+
*/
|
|
50
|
+
export declare function searchChannels(query: string, limit?: number): Promise<Result<ChannelSearchResultSet>>;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Substrate API client for search and people operations.
|
|
3
|
+
*
|
|
4
|
+
* Handles all calls to substrate.office.com endpoints.
|
|
5
|
+
*/
|
|
6
|
+
import { httpRequest } from '../utils/http.js';
|
|
7
|
+
import { SUBSTRATE_API, getBearerHeaders } from '../utils/api-config.js';
|
|
8
|
+
import { ErrorCode } from '../types/errors.js';
|
|
9
|
+
import { ok } from '../types/result.js';
|
|
10
|
+
import { clearTokenCache } from '../auth/token-extractor.js';
|
|
11
|
+
import { requireSubstrateTokenAsync } from '../utils/auth-guards.js';
|
|
12
|
+
import { parseSearchResults, parsePeopleResults, parseChannelResults, filterChannelsByName, } from '../utils/parsers.js';
|
|
13
|
+
import { getMyTeamsAndChannels } from './csa-api.js';
|
|
14
|
+
/**
|
|
15
|
+
* Searches Teams messages using the Substrate v2 query API.
|
|
16
|
+
*/
|
|
17
|
+
export async function searchMessages(query, options = {}) {
|
|
18
|
+
const tokenResult = await requireSubstrateTokenAsync();
|
|
19
|
+
if (!tokenResult.ok) {
|
|
20
|
+
return tokenResult;
|
|
21
|
+
}
|
|
22
|
+
const token = tokenResult.value;
|
|
23
|
+
const from = options.from ?? 0;
|
|
24
|
+
const size = options.size ?? 25;
|
|
25
|
+
// Generate unique IDs for this request
|
|
26
|
+
const cvid = crypto.randomUUID();
|
|
27
|
+
const logicalId = crypto.randomUUID();
|
|
28
|
+
const body = {
|
|
29
|
+
entityRequests: [{
|
|
30
|
+
entityType: 'Message',
|
|
31
|
+
contentSources: ['Teams'],
|
|
32
|
+
propertySet: 'Optimized',
|
|
33
|
+
fields: [
|
|
34
|
+
'Extension_SkypeSpaces_ConversationPost_Extension_FromSkypeInternalId_String',
|
|
35
|
+
'Extension_SkypeSpaces_ConversationPost_Extension_ThreadType_String',
|
|
36
|
+
'Extension_SkypeSpaces_ConversationPost_Extension_SkypeGroupId_String',
|
|
37
|
+
],
|
|
38
|
+
query: {
|
|
39
|
+
queryString: `${query} AND NOT (isClientSoftDeleted:TRUE)`,
|
|
40
|
+
displayQueryString: query,
|
|
41
|
+
},
|
|
42
|
+
from,
|
|
43
|
+
size,
|
|
44
|
+
topResultsCount: 5,
|
|
45
|
+
}],
|
|
46
|
+
QueryAlterationOptions: {
|
|
47
|
+
EnableAlteration: true,
|
|
48
|
+
EnableSuggestion: true,
|
|
49
|
+
SupportedRecourseDisplayTypes: ['Suggestion'],
|
|
50
|
+
},
|
|
51
|
+
cvid,
|
|
52
|
+
logicalId,
|
|
53
|
+
scenario: {
|
|
54
|
+
Dimensions: [
|
|
55
|
+
{ DimensionName: 'QueryType', DimensionValue: 'Messages' },
|
|
56
|
+
{ DimensionName: 'FormFactor', DimensionValue: 'general.web.reactSearch' },
|
|
57
|
+
],
|
|
58
|
+
Name: 'powerbar',
|
|
59
|
+
},
|
|
60
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
61
|
+
};
|
|
62
|
+
const response = await httpRequest(SUBSTRATE_API.search, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: getBearerHeaders(token),
|
|
65
|
+
body: JSON.stringify(body),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
// Clear cache on auth errors
|
|
69
|
+
if (response.error.code === ErrorCode.AUTH_EXPIRED) {
|
|
70
|
+
clearTokenCache();
|
|
71
|
+
}
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
const data = response.value.data;
|
|
75
|
+
const { results, total } = parseSearchResults(data.EntitySets);
|
|
76
|
+
const maxResults = options.maxResults ?? size;
|
|
77
|
+
const limitedResults = results.slice(0, maxResults);
|
|
78
|
+
return ok({
|
|
79
|
+
results: limitedResults,
|
|
80
|
+
pagination: {
|
|
81
|
+
from,
|
|
82
|
+
size,
|
|
83
|
+
returned: limitedResults.length,
|
|
84
|
+
total,
|
|
85
|
+
hasMore: total !== undefined
|
|
86
|
+
? from + results.length < total
|
|
87
|
+
: results.length >= size,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Searches for people by name or email.
|
|
93
|
+
*/
|
|
94
|
+
export async function searchPeople(query, limit = 10) {
|
|
95
|
+
const tokenResult = await requireSubstrateTokenAsync();
|
|
96
|
+
if (!tokenResult.ok) {
|
|
97
|
+
return tokenResult;
|
|
98
|
+
}
|
|
99
|
+
const token = tokenResult.value;
|
|
100
|
+
const cvid = crypto.randomUUID();
|
|
101
|
+
const logicalId = crypto.randomUUID();
|
|
102
|
+
const body = {
|
|
103
|
+
EntityRequests: [{
|
|
104
|
+
Query: {
|
|
105
|
+
QueryString: query,
|
|
106
|
+
DisplayQueryString: query,
|
|
107
|
+
},
|
|
108
|
+
EntityType: 'People',
|
|
109
|
+
Size: limit,
|
|
110
|
+
Fields: [
|
|
111
|
+
'Id',
|
|
112
|
+
'MRI',
|
|
113
|
+
'DisplayName',
|
|
114
|
+
'EmailAddresses',
|
|
115
|
+
'GivenName',
|
|
116
|
+
'Surname',
|
|
117
|
+
'JobTitle',
|
|
118
|
+
'Department',
|
|
119
|
+
'CompanyName',
|
|
120
|
+
],
|
|
121
|
+
}],
|
|
122
|
+
cvid,
|
|
123
|
+
logicalId,
|
|
124
|
+
};
|
|
125
|
+
const response = await httpRequest(SUBSTRATE_API.peopleSearch, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: getBearerHeaders(token),
|
|
128
|
+
body: JSON.stringify(body),
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
if (response.error.code === ErrorCode.AUTH_EXPIRED) {
|
|
132
|
+
clearTokenCache();
|
|
133
|
+
}
|
|
134
|
+
return response;
|
|
135
|
+
}
|
|
136
|
+
const results = parsePeopleResults(response.value.data.Groups);
|
|
137
|
+
return ok({
|
|
138
|
+
results,
|
|
139
|
+
returned: results.length,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Gets the user's frequently contacted people.
|
|
144
|
+
*/
|
|
145
|
+
export async function getFrequentContacts(limit = 50) {
|
|
146
|
+
const tokenResult = await requireSubstrateTokenAsync();
|
|
147
|
+
if (!tokenResult.ok) {
|
|
148
|
+
return tokenResult;
|
|
149
|
+
}
|
|
150
|
+
const token = tokenResult.value;
|
|
151
|
+
const cvid = crypto.randomUUID();
|
|
152
|
+
const logicalId = crypto.randomUUID();
|
|
153
|
+
const body = {
|
|
154
|
+
EntityRequests: [{
|
|
155
|
+
Query: {
|
|
156
|
+
QueryString: '',
|
|
157
|
+
DisplayQueryString: '',
|
|
158
|
+
},
|
|
159
|
+
EntityType: 'People',
|
|
160
|
+
Size: limit,
|
|
161
|
+
Fields: [
|
|
162
|
+
'Id',
|
|
163
|
+
'MRI',
|
|
164
|
+
'DisplayName',
|
|
165
|
+
'EmailAddresses',
|
|
166
|
+
'GivenName',
|
|
167
|
+
'Surname',
|
|
168
|
+
'JobTitle',
|
|
169
|
+
'Department',
|
|
170
|
+
'CompanyName',
|
|
171
|
+
],
|
|
172
|
+
}],
|
|
173
|
+
cvid,
|
|
174
|
+
logicalId,
|
|
175
|
+
};
|
|
176
|
+
const response = await httpRequest(SUBSTRATE_API.frequentContacts, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: getBearerHeaders(token),
|
|
179
|
+
body: JSON.stringify(body),
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
if (response.error.code === ErrorCode.AUTH_EXPIRED) {
|
|
183
|
+
clearTokenCache();
|
|
184
|
+
}
|
|
185
|
+
return response;
|
|
186
|
+
}
|
|
187
|
+
const contacts = parsePeopleResults(response.value.data.Groups);
|
|
188
|
+
return ok({
|
|
189
|
+
results: contacts,
|
|
190
|
+
returned: contacts.length,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Searches for Teams channels by name using both:
|
|
195
|
+
* 1. User's own teams/channels (Teams List API) - reliable, shows membership
|
|
196
|
+
* 2. Organisation-wide discovery (Substrate suggestions) - broader but less reliable
|
|
197
|
+
*
|
|
198
|
+
* Results are merged and deduplicated, with membership status indicated.
|
|
199
|
+
*
|
|
200
|
+
* @param query - Channel name to search for
|
|
201
|
+
* @param limit - Maximum number of results (default: 10, max: 50)
|
|
202
|
+
*/
|
|
203
|
+
export async function searchChannels(query, limit = 10) {
|
|
204
|
+
const tokenResult = await requireSubstrateTokenAsync();
|
|
205
|
+
if (!tokenResult.ok) {
|
|
206
|
+
return tokenResult;
|
|
207
|
+
}
|
|
208
|
+
const token = tokenResult.value;
|
|
209
|
+
// Run both searches in parallel
|
|
210
|
+
const [orgSearchResult, myTeamsResult] = await Promise.all([
|
|
211
|
+
searchChannelsOrgWide(query, limit, token),
|
|
212
|
+
getMyTeamsAndChannels(),
|
|
213
|
+
]);
|
|
214
|
+
// Build a map of channel IDs the user is a member of
|
|
215
|
+
const memberChannelIds = new Set();
|
|
216
|
+
const myChannelsMatching = [];
|
|
217
|
+
if (myTeamsResult.ok) {
|
|
218
|
+
// Filter the user's channels by the query and collect matching ones
|
|
219
|
+
const matching = filterChannelsByName(myTeamsResult.value.teams, query);
|
|
220
|
+
for (const channel of matching) {
|
|
221
|
+
memberChannelIds.add(channel.channelId);
|
|
222
|
+
myChannelsMatching.push(channel);
|
|
223
|
+
}
|
|
224
|
+
// Also add all channel IDs to the set for membership lookup
|
|
225
|
+
for (const team of myTeamsResult.value.teams) {
|
|
226
|
+
for (const channel of team.channels) {
|
|
227
|
+
memberChannelIds.add(channel.channelId);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Process org-wide results, marking membership status
|
|
232
|
+
const orgChannels = [];
|
|
233
|
+
if (orgSearchResult.ok) {
|
|
234
|
+
for (const channel of orgSearchResult.value) {
|
|
235
|
+
// Mark whether user is a member
|
|
236
|
+
channel.isMember = memberChannelIds.has(channel.channelId);
|
|
237
|
+
orgChannels.push(channel);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Merge results: start with user's matching channels (definitely accessible),
|
|
241
|
+
// then add org-wide results that aren't duplicates
|
|
242
|
+
const seenIds = new Set();
|
|
243
|
+
const merged = [];
|
|
244
|
+
// First add channels from user's teams (reliable, known accessible)
|
|
245
|
+
for (const channel of myChannelsMatching) {
|
|
246
|
+
if (!seenIds.has(channel.channelId)) {
|
|
247
|
+
seenIds.add(channel.channelId);
|
|
248
|
+
merged.push(channel);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Then add org-wide results that aren't duplicates
|
|
252
|
+
for (const channel of orgChannels) {
|
|
253
|
+
if (!seenIds.has(channel.channelId)) {
|
|
254
|
+
seenIds.add(channel.channelId);
|
|
255
|
+
merged.push(channel);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Apply limit
|
|
259
|
+
const limited = merged.slice(0, limit);
|
|
260
|
+
return ok({
|
|
261
|
+
results: limited,
|
|
262
|
+
returned: limited.length,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Internal: Searches for channels org-wide using the Substrate suggestions API.
|
|
267
|
+
*
|
|
268
|
+
* This is a typeahead/autocomplete API, so matching behaviour may be inconsistent
|
|
269
|
+
* for multi-word queries. Used as a supplement to the user's own teams list.
|
|
270
|
+
*/
|
|
271
|
+
async function searchChannelsOrgWide(query, limit, token) {
|
|
272
|
+
const cvid = crypto.randomUUID();
|
|
273
|
+
const logicalId = crypto.randomUUID();
|
|
274
|
+
const body = {
|
|
275
|
+
EntityRequests: [{
|
|
276
|
+
Query: {
|
|
277
|
+
QueryString: query,
|
|
278
|
+
DisplayQueryString: query,
|
|
279
|
+
},
|
|
280
|
+
EntityType: 'TeamsChannel',
|
|
281
|
+
Size: Math.min(limit, 50),
|
|
282
|
+
}],
|
|
283
|
+
cvid,
|
|
284
|
+
logicalId,
|
|
285
|
+
};
|
|
286
|
+
const response = await httpRequest(SUBSTRATE_API.channelSearch, {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: getBearerHeaders(token),
|
|
289
|
+
body: JSON.stringify(body),
|
|
290
|
+
});
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
if (response.error.code === ErrorCode.AUTH_EXPIRED) {
|
|
293
|
+
clearTokenCache();
|
|
294
|
+
}
|
|
295
|
+
return response;
|
|
296
|
+
}
|
|
297
|
+
const data = response.value.data;
|
|
298
|
+
const results = parseChannelResults(data?.Groups);
|
|
299
|
+
// Mark all org-wide results as isMember: false initially
|
|
300
|
+
// (caller will update based on actual membership)
|
|
301
|
+
for (const result of results) {
|
|
302
|
+
result.isMember = false;
|
|
303
|
+
}
|
|
304
|
+
return ok(results);
|
|
305
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption utilities for credential storage.
|
|
3
|
+
*
|
|
4
|
+
* Uses machine-specific key derivation to encrypt sensitive data at rest.
|
|
5
|
+
* This is not foolproof security, but significantly raises the bar compared
|
|
6
|
+
* to plaintext storage.
|
|
7
|
+
*/
|
|
8
|
+
/** Encrypted data format. */
|
|
9
|
+
export interface EncryptedData {
|
|
10
|
+
/** Initialisation vector (hex). */
|
|
11
|
+
iv: string;
|
|
12
|
+
/** Encrypted content (hex). */
|
|
13
|
+
content: string;
|
|
14
|
+
/** Authentication tag (hex). */
|
|
15
|
+
tag: string;
|
|
16
|
+
/** Version marker for format compatibility. */
|
|
17
|
+
version: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Encrypts a string value.
|
|
21
|
+
*/
|
|
22
|
+
export declare function encrypt(plaintext: string): EncryptedData;
|
|
23
|
+
/**
|
|
24
|
+
* Decrypts encrypted data.
|
|
25
|
+
*
|
|
26
|
+
* @throws Error if decryption fails (wrong machine, corrupted data, etc.)
|
|
27
|
+
*/
|
|
28
|
+
export declare function decrypt(data: EncryptedData): string;
|
|
29
|
+
/**
|
|
30
|
+
* Checks if data looks like encrypted format.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isEncrypted(data: unknown): data is EncryptedData;
|