msteams-mcp 0.2.0

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.
Files changed (80) hide show
  1. package/README.md +229 -0
  2. package/dist/__fixtures__/api-responses.d.ts +228 -0
  3. package/dist/__fixtures__/api-responses.js +217 -0
  4. package/dist/api/chatsvc-api.d.ts +171 -0
  5. package/dist/api/chatsvc-api.js +459 -0
  6. package/dist/api/csa-api.d.ts +44 -0
  7. package/dist/api/csa-api.js +148 -0
  8. package/dist/api/index.d.ts +6 -0
  9. package/dist/api/index.js +6 -0
  10. package/dist/api/substrate-api.d.ts +50 -0
  11. package/dist/api/substrate-api.js +305 -0
  12. package/dist/auth/crypto.d.ts +32 -0
  13. package/dist/auth/crypto.js +66 -0
  14. package/dist/auth/index.d.ts +6 -0
  15. package/dist/auth/index.js +6 -0
  16. package/dist/auth/session-store.d.ts +82 -0
  17. package/dist/auth/session-store.js +136 -0
  18. package/dist/auth/token-extractor.d.ts +69 -0
  19. package/dist/auth/token-extractor.js +330 -0
  20. package/dist/browser/auth.d.ts +43 -0
  21. package/dist/browser/auth.js +232 -0
  22. package/dist/browser/context.d.ts +40 -0
  23. package/dist/browser/context.js +121 -0
  24. package/dist/browser/session.d.ts +34 -0
  25. package/dist/browser/session.js +92 -0
  26. package/dist/constants.d.ts +54 -0
  27. package/dist/constants.js +72 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.js +12 -0
  30. package/dist/research/explore.d.ts +11 -0
  31. package/dist/research/explore.js +267 -0
  32. package/dist/research/search-research.d.ts +17 -0
  33. package/dist/research/search-research.js +317 -0
  34. package/dist/server.d.ts +64 -0
  35. package/dist/server.js +291 -0
  36. package/dist/teams/api-interceptor.d.ts +54 -0
  37. package/dist/teams/api-interceptor.js +391 -0
  38. package/dist/teams/direct-api.d.ts +321 -0
  39. package/dist/teams/direct-api.js +1305 -0
  40. package/dist/teams/messages.d.ts +14 -0
  41. package/dist/teams/messages.js +142 -0
  42. package/dist/teams/search.d.ts +40 -0
  43. package/dist/teams/search.js +458 -0
  44. package/dist/test/cli.d.ts +12 -0
  45. package/dist/test/cli.js +328 -0
  46. package/dist/test/debug-search.d.ts +10 -0
  47. package/dist/test/debug-search.js +147 -0
  48. package/dist/test/manual-test.d.ts +11 -0
  49. package/dist/test/manual-test.js +160 -0
  50. package/dist/test/mcp-harness.d.ts +17 -0
  51. package/dist/test/mcp-harness.js +427 -0
  52. package/dist/tools/auth-tools.d.ts +26 -0
  53. package/dist/tools/auth-tools.js +127 -0
  54. package/dist/tools/index.d.ts +45 -0
  55. package/dist/tools/index.js +12 -0
  56. package/dist/tools/message-tools.d.ts +139 -0
  57. package/dist/tools/message-tools.js +433 -0
  58. package/dist/tools/people-tools.d.ts +46 -0
  59. package/dist/tools/people-tools.js +123 -0
  60. package/dist/tools/registry.d.ts +23 -0
  61. package/dist/tools/registry.js +61 -0
  62. package/dist/tools/search-tools.d.ts +79 -0
  63. package/dist/tools/search-tools.js +168 -0
  64. package/dist/types/errors.d.ts +58 -0
  65. package/dist/types/errors.js +132 -0
  66. package/dist/types/result.d.ts +43 -0
  67. package/dist/types/result.js +51 -0
  68. package/dist/types/teams.d.ts +79 -0
  69. package/dist/types/teams.js +5 -0
  70. package/dist/utils/api-config.d.ts +66 -0
  71. package/dist/utils/api-config.js +113 -0
  72. package/dist/utils/auth-guards.d.ts +29 -0
  73. package/dist/utils/auth-guards.js +54 -0
  74. package/dist/utils/http.d.ts +29 -0
  75. package/dist/utils/http.js +111 -0
  76. package/dist/utils/parsers.d.ts +187 -0
  77. package/dist/utils/parsers.js +574 -0
  78. package/dist/utils/parsers.test.d.ts +7 -0
  79. package/dist/utils/parsers.test.js +360 -0
  80. package/package.json +58 -0
@@ -0,0 +1,148 @@
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
+ */
6
+ import { httpRequest } from '../utils/http.js';
7
+ import { CSA_API, getCsaHeaders, validateRegion } from '../utils/api-config.js';
8
+ import { ErrorCode, createError } from '../types/errors.js';
9
+ import { ok, err } from '../types/result.js';
10
+ import { requireCsaAuth } from '../utils/auth-guards.js';
11
+ import { getConversationProperties, extractParticipantNames, } from './chatsvc-api.js';
12
+ import { parseTeamsList, } from '../utils/parsers.js';
13
+ /**
14
+ * Gets the user's favourite/pinned conversations.
15
+ */
16
+ export async function getFavorites(region = 'amer') {
17
+ const authResult = requireCsaAuth();
18
+ if (!authResult.ok) {
19
+ return authResult;
20
+ }
21
+ const { auth, csaToken } = authResult.value;
22
+ const validRegion = validateRegion(region);
23
+ const url = CSA_API.conversationFolders(validRegion);
24
+ const response = await httpRequest(url, {
25
+ method: 'GET',
26
+ headers: getCsaHeaders(auth.skypeToken, csaToken),
27
+ });
28
+ if (!response.ok) {
29
+ return response;
30
+ }
31
+ const data = response.value.data;
32
+ // Find the Favorites folder
33
+ const folders = data.conversationFolders;
34
+ const favoritesFolder = folders?.find((f) => {
35
+ const folder = f;
36
+ return folder.folderType === 'Favorites';
37
+ });
38
+ if (!favoritesFolder) {
39
+ return ok({
40
+ favorites: [],
41
+ folderHierarchyVersion: data.folderHierarchyVersion,
42
+ });
43
+ }
44
+ const items = favoritesFolder.conversationFolderItems;
45
+ const favorites = (items || []).map((item) => {
46
+ const i = item;
47
+ return {
48
+ conversationId: i.conversationId,
49
+ createdTime: i.createdTime,
50
+ lastUpdatedTime: i.lastUpdatedTime,
51
+ };
52
+ });
53
+ // Enrich favorites with display names in parallel
54
+ const enrichmentPromises = favorites.map(async (fav) => {
55
+ const props = await getConversationProperties(fav.conversationId, validRegion);
56
+ if (props.ok) {
57
+ fav.displayName = props.value.displayName;
58
+ fav.conversationType = props.value.conversationType;
59
+ }
60
+ // Fallback: extract from recent messages if no display name
61
+ if (!fav.displayName) {
62
+ const names = await extractParticipantNames(fav.conversationId, validRegion);
63
+ if (names.ok && names.value) {
64
+ fav.displayName = names.value;
65
+ }
66
+ }
67
+ });
68
+ await Promise.allSettled(enrichmentPromises);
69
+ return ok({
70
+ favorites,
71
+ folderHierarchyVersion: data.folderHierarchyVersion,
72
+ folderId: favoritesFolder.id,
73
+ });
74
+ }
75
+ /**
76
+ * Adds a conversation to the user's favourites.
77
+ */
78
+ export async function addFavorite(conversationId, region = 'amer') {
79
+ return modifyFavorite(conversationId, 'AddItem', region);
80
+ }
81
+ /**
82
+ * Removes a conversation from the user's favourites.
83
+ */
84
+ export async function removeFavorite(conversationId, region = 'amer') {
85
+ return modifyFavorite(conversationId, 'RemoveItem', region);
86
+ }
87
+ /**
88
+ * Internal helper to modify the favourites folder.
89
+ */
90
+ async function modifyFavorite(conversationId, action, region) {
91
+ const authResult = requireCsaAuth();
92
+ if (!authResult.ok) {
93
+ return authResult;
94
+ }
95
+ const { auth, csaToken } = authResult.value;
96
+ const validRegion = validateRegion(region);
97
+ // Get current folder state
98
+ const currentState = await getFavorites(validRegion);
99
+ if (!currentState.ok) {
100
+ return err(currentState.error);
101
+ }
102
+ if (!currentState.value.folderId) {
103
+ return err(createError(ErrorCode.NOT_FOUND, 'Could not find Favorites folder'));
104
+ }
105
+ const url = CSA_API.conversationFolders(validRegion);
106
+ const response = await httpRequest(url, {
107
+ method: 'POST',
108
+ headers: getCsaHeaders(auth.skypeToken, csaToken),
109
+ body: JSON.stringify({
110
+ folderHierarchyVersion: currentState.value.folderHierarchyVersion,
111
+ actions: [
112
+ {
113
+ action,
114
+ folderId: currentState.value.folderId,
115
+ itemId: conversationId,
116
+ },
117
+ ],
118
+ }),
119
+ });
120
+ if (!response.ok) {
121
+ return response;
122
+ }
123
+ return ok(undefined);
124
+ }
125
+ /**
126
+ * Gets all teams and channels the user is a member of.
127
+ *
128
+ * This returns the complete list of teams with their channels - not a search,
129
+ * but a full enumeration of the user's memberships.
130
+ */
131
+ export async function getMyTeamsAndChannels(region = 'amer') {
132
+ const authResult = requireCsaAuth();
133
+ if (!authResult.ok) {
134
+ return authResult;
135
+ }
136
+ const { auth, csaToken } = authResult.value;
137
+ const validRegion = validateRegion(region);
138
+ const url = CSA_API.teamsList(validRegion);
139
+ const response = await httpRequest(url, {
140
+ method: 'GET',
141
+ headers: getCsaHeaders(auth.skypeToken, csaToken),
142
+ });
143
+ if (!response.ok) {
144
+ return response;
145
+ }
146
+ const teams = parseTeamsList(response.value.data);
147
+ return ok({ teams });
148
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * API module exports.
3
+ */
4
+ export * from './substrate-api.js';
5
+ export * from './chatsvc-api.js';
6
+ export * from './csa-api.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * API module exports.
3
+ */
4
+ export * from './substrate-api.js';
5
+ export * from './chatsvc-api.js';
6
+ export * from './csa-api.js';
@@ -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 { requireSubstrateToken } 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 = requireSubstrateToken();
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, from, size);
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 = requireSubstrateToken();
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 = requireSubstrateToken();
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 = requireSubstrateToken();
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 future upgrades. */
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;
@@ -0,0 +1,66 @@
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
+ import * as crypto from 'crypto';
9
+ import * as os from 'os';
10
+ /** Algorithm for encryption. */
11
+ const ALGORITHM = 'aes-256-gcm';
12
+ /** Salt for key derivation. */
13
+ const SALT = 'teams-mcp-credential-salt-v1';
14
+ /** Derives an encryption key from machine-specific values. */
15
+ function deriveKey() {
16
+ // Combine hostname and username for machine-specific key
17
+ const machineId = `${os.hostname()}:${os.userInfo().username}`;
18
+ return crypto.scryptSync(machineId, SALT, 32);
19
+ }
20
+ /**
21
+ * Encrypts a string value.
22
+ */
23
+ export function encrypt(plaintext) {
24
+ const key = deriveKey();
25
+ const iv = crypto.randomBytes(16);
26
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
27
+ let encrypted = cipher.update(plaintext, 'utf8', 'hex');
28
+ encrypted += cipher.final('hex');
29
+ const tag = cipher.getAuthTag();
30
+ return {
31
+ iv: iv.toString('hex'),
32
+ content: encrypted,
33
+ tag: tag.toString('hex'),
34
+ version: 1,
35
+ };
36
+ }
37
+ /**
38
+ * Decrypts encrypted data.
39
+ *
40
+ * @throws Error if decryption fails (wrong machine, corrupted data, etc.)
41
+ */
42
+ export function decrypt(data) {
43
+ if (data.version !== 1) {
44
+ throw new Error(`Unsupported encryption version: ${data.version}`);
45
+ }
46
+ const key = deriveKey();
47
+ const iv = Buffer.from(data.iv, 'hex');
48
+ const tag = Buffer.from(data.tag, 'hex');
49
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
50
+ decipher.setAuthTag(tag);
51
+ let decrypted = decipher.update(data.content, 'hex', 'utf8');
52
+ decrypted += decipher.final('utf8');
53
+ return decrypted;
54
+ }
55
+ /**
56
+ * Checks if data looks like encrypted format.
57
+ */
58
+ export function isEncrypted(data) {
59
+ if (!data || typeof data !== 'object')
60
+ return false;
61
+ const obj = data;
62
+ return (typeof obj.iv === 'string' &&
63
+ typeof obj.content === 'string' &&
64
+ typeof obj.tag === 'string' &&
65
+ typeof obj.version === 'number');
66
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Auth module exports.
3
+ */
4
+ export * from './session-store.js';
5
+ export * from './token-extractor.js';
6
+ export { encrypt, decrypt, isEncrypted, type EncryptedData } from './crypto.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Auth module exports.
3
+ */
4
+ export * from './session-store.js';
5
+ export * from './token-extractor.js';
6
+ export { encrypt, decrypt, isEncrypted } from './crypto.js';