@softeria/ms-365-mcp-server 0.11.4 → 0.12.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.
package/dist/auth.js CHANGED
@@ -1,450 +1,437 @@
1
- import { PublicClientApplication } from '@azure/msal-node';
2
- import keytar from 'keytar';
3
- import { fileURLToPath } from 'url';
4
- import path from 'path';
5
- import fs from 'fs';
6
- import logger from './logger.js';
1
+ import { PublicClientApplication } from "@azure/msal-node";
2
+ import keytar from "keytar";
3
+ import logger from "./logger.js";
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import path from "path";
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
- const endpointsPath = path.join(__dirname, 'endpoints.json');
9
+ const endpointsData = JSON.parse(
10
+ readFileSync(path.join(__dirname, "endpoints.json"), "utf8")
11
+ );
10
12
  const endpoints = {
11
- default: JSON.parse(fs.readFileSync(endpointsPath, 'utf8')),
13
+ default: endpointsData
12
14
  };
13
- const SERVICE_NAME = 'ms-365-mcp-server';
14
- const TOKEN_CACHE_ACCOUNT = 'msal-token-cache';
15
- const SELECTED_ACCOUNT_KEY = 'selected-account';
15
+ const SERVICE_NAME = "ms-365-mcp-server";
16
+ const TOKEN_CACHE_ACCOUNT = "msal-token-cache";
17
+ const SELECTED_ACCOUNT_KEY = "selected-account";
16
18
  const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
17
- const FALLBACK_PATH = path.join(FALLBACK_DIR, '..', '.token-cache.json');
18
- const SELECTED_ACCOUNT_PATH = path.join(FALLBACK_DIR, '..', '.selected-account.json');
19
+ const FALLBACK_PATH = path.join(FALLBACK_DIR, "..", ".token-cache.json");
20
+ const SELECTED_ACCOUNT_PATH = path.join(FALLBACK_DIR, "..", ".selected-account.json");
19
21
  const DEFAULT_CONFIG = {
20
- auth: {
21
- clientId: process.env.MS365_MCP_CLIENT_ID || '084a3e9f-a9f4-43f7-89f9-d229cf97853e',
22
- authority: `https://login.microsoftonline.com/${process.env.MS365_MCP_TENANT_ID || 'common'}`,
23
- },
22
+ auth: {
23
+ clientId: process.env.MS365_MCP_CLIENT_ID || "084a3e9f-a9f4-43f7-89f9-d229cf97853e",
24
+ authority: `https://login.microsoftonline.com/${process.env.MS365_MCP_TENANT_ID || "common"}`
25
+ }
24
26
  };
25
27
  const SCOPE_HIERARCHY = {
26
- 'Mail.ReadWrite': ['Mail.Read'],
27
- 'Calendars.ReadWrite': ['Calendars.Read'],
28
- 'Files.ReadWrite': ['Files.Read'],
29
- 'Tasks.ReadWrite': ['Tasks.Read'],
30
- 'Contacts.ReadWrite': ['Contacts.Read'],
28
+ "Mail.ReadWrite": ["Mail.Read"],
29
+ "Calendars.ReadWrite": ["Calendars.Read"],
30
+ "Files.ReadWrite": ["Files.Read"],
31
+ "Tasks.ReadWrite": ["Tasks.Read"],
32
+ "Contacts.ReadWrite": ["Contacts.Read"]
31
33
  };
32
34
  function buildScopesFromEndpoints(includeWorkAccountScopes = false) {
33
- const scopesSet = new Set();
34
- endpoints.default.forEach((endpoint) => {
35
- if (endpoint.requiresWorkAccount && !includeWorkAccountScopes) {
36
- return;
37
- }
38
- if (endpoint.scopes && Array.isArray(endpoint.scopes)) {
39
- endpoint.scopes.forEach((scope) => scopesSet.add(scope));
40
- }
41
- });
42
- Object.entries(SCOPE_HIERARCHY).forEach(([higherScope, lowerScopes]) => {
43
- if (lowerScopes.every((scope) => scopesSet.has(scope))) {
44
- lowerScopes.forEach((scope) => scopesSet.delete(scope));
45
- scopesSet.add(higherScope);
46
- }
47
- });
48
- return Array.from(scopesSet);
35
+ const scopesSet = /* @__PURE__ */ new Set();
36
+ endpoints.default.forEach((endpoint) => {
37
+ if (endpoint.requiresWorkAccount && !includeWorkAccountScopes) {
38
+ return;
39
+ }
40
+ if (endpoint.scopes && Array.isArray(endpoint.scopes)) {
41
+ endpoint.scopes.forEach((scope) => scopesSet.add(scope));
42
+ }
43
+ });
44
+ Object.entries(SCOPE_HIERARCHY).forEach(([higherScope, lowerScopes]) => {
45
+ if (lowerScopes.every((scope) => scopesSet.has(scope))) {
46
+ lowerScopes.forEach((scope) => scopesSet.delete(scope));
47
+ scopesSet.add(higherScope);
48
+ }
49
+ });
50
+ return Array.from(scopesSet);
49
51
  }
50
52
  function buildAllScopes() {
51
- return buildScopesFromEndpoints(true);
53
+ return buildScopesFromEndpoints(true);
52
54
  }
53
55
  class AuthManager {
54
- constructor(config = DEFAULT_CONFIG, scopes = buildScopesFromEndpoints()) {
55
- logger.info(`And scopes are ${scopes.join(', ')}`, scopes);
56
- this.config = config;
57
- this.scopes = scopes;
58
- this.msalApp = new PublicClientApplication(this.config);
59
- this.accessToken = null;
60
- this.tokenExpiry = null;
61
- this.selectedAccountId = null;
62
- const oauthTokenFromEnv = process.env.MS365_MCP_OAUTH_TOKEN;
63
- this.oauthToken = oauthTokenFromEnv ?? null;
64
- this.isOAuthMode = oauthTokenFromEnv != null;
56
+ constructor(config = DEFAULT_CONFIG, scopes = buildScopesFromEndpoints()) {
57
+ logger.info(`And scopes are ${scopes.join(", ")}`, scopes);
58
+ this.config = config;
59
+ this.scopes = scopes;
60
+ this.msalApp = new PublicClientApplication(this.config);
61
+ this.accessToken = null;
62
+ this.tokenExpiry = null;
63
+ this.selectedAccountId = null;
64
+ const oauthTokenFromEnv = process.env.MS365_MCP_OAUTH_TOKEN;
65
+ this.oauthToken = oauthTokenFromEnv ?? null;
66
+ this.isOAuthMode = oauthTokenFromEnv != null;
67
+ }
68
+ async loadTokenCache() {
69
+ try {
70
+ let cacheData;
71
+ try {
72
+ const cachedData = await keytar.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
73
+ if (cachedData) {
74
+ cacheData = cachedData;
75
+ }
76
+ } catch (keytarError) {
77
+ logger.warn(
78
+ `Keychain access failed, falling back to file storage: ${keytarError.message}`
79
+ );
80
+ }
81
+ if (!cacheData && existsSync(FALLBACK_PATH)) {
82
+ cacheData = readFileSync(FALLBACK_PATH, "utf8");
83
+ }
84
+ if (cacheData) {
85
+ this.msalApp.getTokenCache().deserialize(cacheData);
86
+ }
87
+ await this.loadSelectedAccount();
88
+ } catch (error) {
89
+ logger.error(`Error loading token cache: ${error.message}`);
65
90
  }
66
- async loadTokenCache() {
67
- try {
68
- let cacheData;
69
- try {
70
- const cachedData = await keytar.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
71
- if (cachedData) {
72
- cacheData = cachedData;
73
- }
74
- }
75
- catch (keytarError) {
76
- logger.warn(`Keychain access failed, falling back to file storage: ${keytarError.message}`);
77
- }
78
- if (!cacheData && fs.existsSync(FALLBACK_PATH)) {
79
- cacheData = fs.readFileSync(FALLBACK_PATH, 'utf8');
80
- }
81
- if (cacheData) {
82
- this.msalApp.getTokenCache().deserialize(cacheData);
83
- }
84
- // Load selected account
85
- await this.loadSelectedAccount();
86
- }
87
- catch (error) {
88
- logger.error(`Error loading token cache: ${error.message}`);
89
- }
91
+ }
92
+ async loadSelectedAccount() {
93
+ try {
94
+ let selectedAccountData;
95
+ try {
96
+ const cachedData = await keytar.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
97
+ if (cachedData) {
98
+ selectedAccountData = cachedData;
99
+ }
100
+ } catch (keytarError) {
101
+ logger.warn(
102
+ `Keychain access failed for selected account, falling back to file storage: ${keytarError.message}`
103
+ );
104
+ }
105
+ if (!selectedAccountData && existsSync(SELECTED_ACCOUNT_PATH)) {
106
+ selectedAccountData = readFileSync(SELECTED_ACCOUNT_PATH, "utf8");
107
+ }
108
+ if (selectedAccountData) {
109
+ const parsed = JSON.parse(selectedAccountData);
110
+ this.selectedAccountId = parsed.accountId;
111
+ logger.info(`Loaded selected account: ${this.selectedAccountId}`);
112
+ }
113
+ } catch (error) {
114
+ logger.error(`Error loading selected account: ${error.message}`);
90
115
  }
91
- async loadSelectedAccount() {
92
- try {
93
- let selectedAccountData;
94
- try {
95
- const cachedData = await keytar.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
96
- if (cachedData) {
97
- selectedAccountData = cachedData;
98
- }
99
- }
100
- catch (keytarError) {
101
- logger.warn(`Keychain access failed for selected account, falling back to file storage: ${keytarError.message}`);
102
- }
103
- if (!selectedAccountData && fs.existsSync(SELECTED_ACCOUNT_PATH)) {
104
- selectedAccountData = fs.readFileSync(SELECTED_ACCOUNT_PATH, 'utf8');
105
- }
106
- if (selectedAccountData) {
107
- const parsed = JSON.parse(selectedAccountData);
108
- this.selectedAccountId = parsed.accountId;
109
- logger.info(`Loaded selected account: ${this.selectedAccountId}`);
110
- }
111
- }
112
- catch (error) {
113
- logger.error(`Error loading selected account: ${error.message}`);
114
- }
116
+ }
117
+ async saveTokenCache() {
118
+ try {
119
+ const cacheData = this.msalApp.getTokenCache().serialize();
120
+ try {
121
+ await keytar.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
122
+ } catch (keytarError) {
123
+ logger.warn(
124
+ `Keychain save failed, falling back to file storage: ${keytarError.message}`
125
+ );
126
+ fs.writeFileSync(FALLBACK_PATH, cacheData);
127
+ }
128
+ } catch (error) {
129
+ logger.error(`Error saving token cache: ${error.message}`);
115
130
  }
116
- async saveTokenCache() {
117
- try {
118
- const cacheData = this.msalApp.getTokenCache().serialize();
119
- try {
120
- await keytar.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
121
- }
122
- catch (keytarError) {
123
- logger.warn(`Keychain save failed, falling back to file storage: ${keytarError.message}`);
124
- fs.writeFileSync(FALLBACK_PATH, cacheData);
125
- }
126
- }
127
- catch (error) {
128
- logger.error(`Error saving token cache: ${error.message}`);
129
- }
131
+ }
132
+ async saveSelectedAccount() {
133
+ try {
134
+ const selectedAccountData = JSON.stringify({ accountId: this.selectedAccountId });
135
+ try {
136
+ await keytar.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, selectedAccountData);
137
+ } catch (keytarError) {
138
+ logger.warn(
139
+ `Keychain save failed for selected account, falling back to file storage: ${keytarError.message}`
140
+ );
141
+ fs.writeFileSync(SELECTED_ACCOUNT_PATH, selectedAccountData);
142
+ }
143
+ } catch (error) {
144
+ logger.error(`Error saving selected account: ${error.message}`);
130
145
  }
131
- async saveSelectedAccount() {
132
- try {
133
- const selectedAccountData = JSON.stringify({ accountId: this.selectedAccountId });
134
- try {
135
- await keytar.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, selectedAccountData);
136
- }
137
- catch (keytarError) {
138
- logger.warn(`Keychain save failed for selected account, falling back to file storage: ${keytarError.message}`);
139
- fs.writeFileSync(SELECTED_ACCOUNT_PATH, selectedAccountData);
140
- }
141
- }
142
- catch (error) {
143
- logger.error(`Error saving selected account: ${error.message}`);
144
- }
146
+ }
147
+ async setOAuthToken(token) {
148
+ this.oauthToken = token;
149
+ this.isOAuthMode = true;
150
+ }
151
+ async getToken(forceRefresh = false) {
152
+ if (this.isOAuthMode && this.oauthToken) {
153
+ return this.oauthToken;
145
154
  }
146
- async setOAuthToken(token) {
147
- this.oauthToken = token;
148
- this.isOAuthMode = true;
155
+ if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
156
+ return this.accessToken;
149
157
  }
150
- async getToken(forceRefresh = false) {
151
- if (this.isOAuthMode && this.oauthToken) {
152
- return this.oauthToken;
153
- }
154
- if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
155
- return this.accessToken;
156
- }
157
- const currentAccount = await this.getCurrentAccount();
158
- if (currentAccount) {
159
- const silentRequest = {
160
- account: currentAccount,
161
- scopes: this.scopes,
162
- };
163
- try {
164
- const response = await this.msalApp.acquireTokenSilent(silentRequest);
165
- this.accessToken = response.accessToken;
166
- this.tokenExpiry = response.expiresOn ? new Date(response.expiresOn).getTime() : null;
167
- return this.accessToken;
168
- }
169
- catch (error) {
170
- logger.error('Silent token acquisition failed');
171
- throw new Error('Silent token acquisition failed');
172
- }
173
- }
174
- throw new Error('No valid token found');
158
+ const currentAccount = await this.getCurrentAccount();
159
+ if (currentAccount) {
160
+ const silentRequest = {
161
+ account: currentAccount,
162
+ scopes: this.scopes
163
+ };
164
+ try {
165
+ const response = await this.msalApp.acquireTokenSilent(silentRequest);
166
+ this.accessToken = response.accessToken;
167
+ this.tokenExpiry = response.expiresOn ? new Date(response.expiresOn).getTime() : null;
168
+ return this.accessToken;
169
+ } catch (error) {
170
+ logger.error("Silent token acquisition failed");
171
+ throw new Error("Silent token acquisition failed");
172
+ }
175
173
  }
176
- async getCurrentAccount() {
177
- const accounts = await this.msalApp.getTokenCache().getAllAccounts();
178
- if (accounts.length === 0) {
179
- return null;
180
- }
181
- // If a specific account is selected, find it
182
- if (this.selectedAccountId) {
183
- const selectedAccount = accounts.find((account) => account.homeAccountId === this.selectedAccountId);
184
- if (selectedAccount) {
185
- return selectedAccount;
186
- }
187
- logger.warn(`Selected account ${this.selectedAccountId} not found, falling back to first account`);
188
- }
189
- // Fall back to first account (backward compatibility)
190
- return accounts[0];
174
+ throw new Error("No valid token found");
175
+ }
176
+ async getCurrentAccount() {
177
+ const accounts = await this.msalApp.getTokenCache().getAllAccounts();
178
+ if (accounts.length === 0) {
179
+ return null;
191
180
  }
192
- async acquireTokenByDeviceCode(hack) {
193
- const deviceCodeRequest = {
194
- scopes: this.scopes,
195
- deviceCodeCallback: (response) => {
196
- const text = ['\n', response.message, '\n'].join('');
197
- if (hack) {
198
- hack(text + 'After login run the "verify login" command');
199
- }
200
- else {
201
- console.log(text);
202
- }
203
- logger.info('Device code login initiated');
204
- },
205
- };
206
- try {
207
- logger.info('Requesting device code...');
208
- logger.info(`Requesting scopes: ${this.scopes.join(', ')}`);
209
- const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
210
- logger.info(`Granted scopes: ${response?.scopes?.join(', ') || 'none'}`);
211
- logger.info('Device code login successful');
212
- this.accessToken = response?.accessToken || null;
213
- this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
214
- // Set the newly authenticated account as selected if no account is currently selected
215
- if (!this.selectedAccountId && response?.account) {
216
- this.selectedAccountId = response.account.homeAccountId;
217
- await this.saveSelectedAccount();
218
- logger.info(`Auto-selected new account: ${response.account.username}`);
219
- }
220
- await this.saveTokenCache();
221
- return this.accessToken;
222
- }
223
- catch (error) {
224
- logger.error(`Error in device code flow: ${error.message}`);
225
- throw error;
226
- }
181
+ if (this.selectedAccountId) {
182
+ const selectedAccount = accounts.find(
183
+ (account) => account.homeAccountId === this.selectedAccountId
184
+ );
185
+ if (selectedAccount) {
186
+ return selectedAccount;
187
+ }
188
+ logger.warn(
189
+ `Selected account ${this.selectedAccountId} not found, falling back to first account`
190
+ );
227
191
  }
228
- async testLogin() {
229
- try {
230
- logger.info('Testing login...');
231
- const token = await this.getToken();
232
- if (!token) {
233
- logger.error('Login test failed - no token received');
234
- return {
235
- success: false,
236
- message: 'Login failed - no token received',
237
- };
238
- }
239
- logger.info('Token retrieved successfully, testing Graph API access...');
240
- try {
241
- const response = await fetch('https://graph.microsoft.com/v1.0/me', {
242
- headers: {
243
- Authorization: `Bearer ${token}`,
244
- },
245
- });
246
- if (response.ok) {
247
- const userData = await response.json();
248
- logger.info('Graph API user data fetch successful');
249
- return {
250
- success: true,
251
- message: 'Login successful',
252
- userData: {
253
- displayName: userData.displayName,
254
- userPrincipalName: userData.userPrincipalName,
255
- },
256
- };
257
- }
258
- else {
259
- const errorText = await response.text();
260
- logger.error(`Graph API user data fetch failed: ${response.status} - ${errorText}`);
261
- return {
262
- success: false,
263
- message: `Login successful but Graph API access failed: ${response.status}`,
264
- };
265
- }
266
- }
267
- catch (graphError) {
268
- logger.error(`Error fetching user data: ${graphError.message}`);
269
- return {
270
- success: false,
271
- message: `Login successful but Graph API access failed: ${graphError.message}`,
272
- };
273
- }
274
- }
275
- catch (error) {
276
- logger.error(`Login test failed: ${error.message}`);
277
- return {
278
- success: false,
279
- message: `Login failed: ${error.message}`,
280
- };
281
- }
192
+ return accounts[0];
193
+ }
194
+ async acquireTokenByDeviceCode(hack) {
195
+ const deviceCodeRequest = {
196
+ scopes: this.scopes,
197
+ deviceCodeCallback: (response) => {
198
+ const text = ["\n", response.message, "\n"].join("");
199
+ if (hack) {
200
+ hack(text + 'After login run the "verify login" command');
201
+ } else {
202
+ console.log(text);
203
+ }
204
+ logger.info("Device code login initiated");
205
+ }
206
+ };
207
+ try {
208
+ logger.info("Requesting device code...");
209
+ logger.info(`Requesting scopes: ${this.scopes.join(", ")}`);
210
+ const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
211
+ logger.info(`Granted scopes: ${response?.scopes?.join(", ") || "none"}`);
212
+ logger.info("Device code login successful");
213
+ this.accessToken = response?.accessToken || null;
214
+ this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
215
+ if (!this.selectedAccountId && response?.account) {
216
+ this.selectedAccountId = response.account.homeAccountId;
217
+ await this.saveSelectedAccount();
218
+ logger.info(`Auto-selected new account: ${response.account.username}`);
219
+ }
220
+ await this.saveTokenCache();
221
+ return this.accessToken;
222
+ } catch (error) {
223
+ logger.error(`Error in device code flow: ${error.message}`);
224
+ throw error;
282
225
  }
283
- async logout() {
284
- try {
285
- const accounts = await this.msalApp.getTokenCache().getAllAccounts();
286
- for (const account of accounts) {
287
- await this.msalApp.getTokenCache().removeAccount(account);
288
- }
289
- this.accessToken = null;
290
- this.tokenExpiry = null;
291
- this.selectedAccountId = null;
292
- try {
293
- await keytar.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
294
- await keytar.deletePassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
295
- }
296
- catch (keytarError) {
297
- logger.warn(`Keychain deletion failed: ${keytarError.message}`);
298
- }
299
- if (fs.existsSync(FALLBACK_PATH)) {
300
- fs.unlinkSync(FALLBACK_PATH);
301
- }
302
- if (fs.existsSync(SELECTED_ACCOUNT_PATH)) {
303
- fs.unlinkSync(SELECTED_ACCOUNT_PATH);
226
+ }
227
+ async testLogin() {
228
+ try {
229
+ logger.info("Testing login...");
230
+ const token = await this.getToken();
231
+ if (!token) {
232
+ logger.error("Login test failed - no token received");
233
+ return {
234
+ success: false,
235
+ message: "Login failed - no token received"
236
+ };
237
+ }
238
+ logger.info("Token retrieved successfully, testing Graph API access...");
239
+ try {
240
+ const response = await fetch("https://graph.microsoft.com/v1.0/me", {
241
+ headers: {
242
+ Authorization: `Bearer ${token}`
243
+ }
244
+ });
245
+ if (response.ok) {
246
+ const userData = await response.json();
247
+ logger.info("Graph API user data fetch successful");
248
+ return {
249
+ success: true,
250
+ message: "Login successful",
251
+ userData: {
252
+ displayName: userData.displayName,
253
+ userPrincipalName: userData.userPrincipalName
304
254
  }
305
- return true;
306
- }
307
- catch (error) {
308
- logger.error(`Error during logout: ${error.message}`);
309
- throw error;
310
- }
255
+ };
256
+ } else {
257
+ const errorText = await response.text();
258
+ logger.error(`Graph API user data fetch failed: ${response.status} - ${errorText}`);
259
+ return {
260
+ success: false,
261
+ message: `Login successful but Graph API access failed: ${response.status}`
262
+ };
263
+ }
264
+ } catch (graphError) {
265
+ logger.error(`Error fetching user data: ${graphError.message}`);
266
+ return {
267
+ success: false,
268
+ message: `Login successful but Graph API access failed: ${graphError.message}`
269
+ };
270
+ }
271
+ } catch (error) {
272
+ logger.error(`Login test failed: ${error.message}`);
273
+ return {
274
+ success: false,
275
+ message: `Login failed: ${error.message}`
276
+ };
311
277
  }
312
- async hasWorkAccountPermissions() {
313
- try {
314
- const currentAccount = await this.getCurrentAccount();
315
- if (!currentAccount) {
316
- return false;
317
- }
318
- const workScopes = endpoints.default
319
- .filter((e) => e.requiresWorkAccount)
320
- .flatMap((e) => e.scopes || []);
321
- try {
322
- await this.msalApp.acquireTokenSilent({
323
- scopes: workScopes.slice(0, 1),
324
- account: currentAccount,
325
- });
326
- return true;
327
- }
328
- catch {
329
- return false;
330
- }
331
- }
332
- catch (error) {
333
- logger.error(`Error checking work account permissions: ${error.message}`);
334
- return false;
335
- }
278
+ }
279
+ async logout() {
280
+ try {
281
+ const accounts = await this.msalApp.getTokenCache().getAllAccounts();
282
+ for (const account of accounts) {
283
+ await this.msalApp.getTokenCache().removeAccount(account);
284
+ }
285
+ this.accessToken = null;
286
+ this.tokenExpiry = null;
287
+ this.selectedAccountId = null;
288
+ try {
289
+ await keytar.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
290
+ await keytar.deletePassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
291
+ } catch (keytarError) {
292
+ logger.warn(`Keychain deletion failed: ${keytarError.message}`);
293
+ }
294
+ if (fs.existsSync(FALLBACK_PATH)) {
295
+ fs.unlinkSync(FALLBACK_PATH);
296
+ }
297
+ if (fs.existsSync(SELECTED_ACCOUNT_PATH)) {
298
+ fs.unlinkSync(SELECTED_ACCOUNT_PATH);
299
+ }
300
+ return true;
301
+ } catch (error) {
302
+ logger.error(`Error during logout: ${error.message}`);
303
+ throw error;
336
304
  }
337
- async expandToWorkAccountScopes(hack) {
338
- try {
339
- logger.info('Expanding to work account scopes...');
340
- const allScopes = buildAllScopes();
341
- const deviceCodeRequest = {
342
- scopes: allScopes,
343
- deviceCodeCallback: (response) => {
344
- const text = [
345
- '\n',
346
- '🔄 This feature requires additional permissions (work account scopes)',
347
- '\n',
348
- response.message,
349
- '\n',
350
- ].join('');
351
- if (hack) {
352
- hack(text + 'After login run the "verify login" command');
353
- }
354
- else {
355
- console.log(text);
356
- }
357
- logger.info('Work account scope expansion initiated');
358
- },
359
- };
360
- const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
361
- logger.info('Work account scope expansion successful');
362
- this.accessToken = response?.accessToken || null;
363
- this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
364
- this.scopes = allScopes;
365
- // Update selected account if this is a new account
366
- if (response?.account) {
367
- this.selectedAccountId = response.account.homeAccountId;
368
- await this.saveSelectedAccount();
369
- logger.info(`Updated selected account after scope expansion: ${response.account.username}`);
370
- }
371
- await this.saveTokenCache();
372
- return true;
373
- }
374
- catch (error) {
375
- logger.error(`Error expanding to work account scopes: ${error.message}`);
376
- return false;
377
- }
378
- }
379
- // Multi-account support methods
380
- async listAccounts() {
381
- return await this.msalApp.getTokenCache().getAllAccounts();
305
+ }
306
+ async hasWorkAccountPermissions() {
307
+ try {
308
+ const currentAccount = await this.getCurrentAccount();
309
+ if (!currentAccount) {
310
+ return false;
311
+ }
312
+ const workScopes = endpoints.default.filter((e) => e.requiresWorkAccount).flatMap((e) => e.scopes || []);
313
+ try {
314
+ await this.msalApp.acquireTokenSilent({
315
+ scopes: workScopes.slice(0, 1),
316
+ account: currentAccount
317
+ });
318
+ return true;
319
+ } catch {
320
+ return false;
321
+ }
322
+ } catch (error) {
323
+ logger.error(`Error checking work account permissions: ${error.message}`);
324
+ return false;
382
325
  }
383
- async selectAccount(accountId) {
384
- const accounts = await this.listAccounts();
385
- const account = accounts.find((acc) => acc.homeAccountId === accountId);
386
- if (!account) {
387
- logger.error(`Account with ID ${accountId} not found`);
388
- return false;
389
- }
390
- this.selectedAccountId = accountId;
326
+ }
327
+ async expandToWorkAccountScopes(hack) {
328
+ try {
329
+ logger.info("Expanding to work account scopes...");
330
+ const allScopes = buildAllScopes();
331
+ const deviceCodeRequest = {
332
+ scopes: allScopes,
333
+ deviceCodeCallback: (response2) => {
334
+ const text = [
335
+ "\n",
336
+ "\u{1F504} This feature requires additional permissions (work account scopes)",
337
+ "\n",
338
+ response2.message,
339
+ "\n"
340
+ ].join("");
341
+ if (hack) {
342
+ hack(text + 'After login run the "verify login" command');
343
+ } else {
344
+ console.log(text);
345
+ }
346
+ logger.info("Work account scope expansion initiated");
347
+ }
348
+ };
349
+ const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
350
+ logger.info("Work account scope expansion successful");
351
+ this.accessToken = response?.accessToken || null;
352
+ this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
353
+ this.scopes = allScopes;
354
+ if (response?.account) {
355
+ this.selectedAccountId = response.account.homeAccountId;
391
356
  await this.saveSelectedAccount();
392
- // Clear cached tokens to force refresh with new account
393
- this.accessToken = null;
394
- this.tokenExpiry = null;
395
- logger.info(`Selected account: ${account.username} (${accountId})`);
396
- return true;
357
+ logger.info(`Updated selected account after scope expansion: ${response.account.username}`);
358
+ }
359
+ await this.saveTokenCache();
360
+ return true;
361
+ } catch (error) {
362
+ logger.error(`Error expanding to work account scopes: ${error.message}`);
363
+ return false;
397
364
  }
398
- async getTokenForAccount(accountId) {
399
- const accounts = await this.listAccounts();
400
- const account = accounts.find((acc) => acc.homeAccountId === accountId);
401
- if (!account) {
402
- throw new Error(`Account with ID ${accountId} not found`);
403
- }
404
- const silentRequest = {
405
- account: account,
406
- scopes: this.scopes,
407
- };
408
- try {
409
- const response = await this.msalApp.acquireTokenSilent(silentRequest);
410
- return response.accessToken;
411
- }
412
- catch (error) {
413
- logger.error(`Failed to get token for account ${accountId}: ${error.message}`);
414
- throw error;
415
- }
365
+ }
366
+ // Multi-account support methods
367
+ async listAccounts() {
368
+ return await this.msalApp.getTokenCache().getAllAccounts();
369
+ }
370
+ async selectAccount(accountId) {
371
+ const accounts = await this.listAccounts();
372
+ const account = accounts.find((acc) => acc.homeAccountId === accountId);
373
+ if (!account) {
374
+ logger.error(`Account with ID ${accountId} not found`);
375
+ return false;
416
376
  }
417
- async removeAccount(accountId) {
418
- const accounts = await this.listAccounts();
419
- const account = accounts.find((acc) => acc.homeAccountId === accountId);
420
- if (!account) {
421
- logger.error(`Account with ID ${accountId} not found`);
422
- return false;
423
- }
424
- try {
425
- await this.msalApp.getTokenCache().removeAccount(account);
426
- // If this was the selected account, clear the selection
427
- if (this.selectedAccountId === accountId) {
428
- this.selectedAccountId = null;
429
- await this.saveSelectedAccount();
430
- this.accessToken = null;
431
- this.tokenExpiry = null;
432
- }
433
- logger.info(`Removed account: ${account.username} (${accountId})`);
434
- return true;
435
- }
436
- catch (error) {
437
- logger.error(`Failed to remove account ${accountId}: ${error.message}`);
438
- return false;
439
- }
377
+ this.selectedAccountId = accountId;
378
+ await this.saveSelectedAccount();
379
+ this.accessToken = null;
380
+ this.tokenExpiry = null;
381
+ logger.info(`Selected account: ${account.username} (${accountId})`);
382
+ return true;
383
+ }
384
+ async getTokenForAccount(accountId) {
385
+ const accounts = await this.listAccounts();
386
+ const account = accounts.find((acc) => acc.homeAccountId === accountId);
387
+ if (!account) {
388
+ throw new Error(`Account with ID ${accountId} not found`);
389
+ }
390
+ const silentRequest = {
391
+ account,
392
+ scopes: this.scopes
393
+ };
394
+ try {
395
+ const response = await this.msalApp.acquireTokenSilent(silentRequest);
396
+ return response.accessToken;
397
+ } catch (error) {
398
+ logger.error(`Failed to get token for account ${accountId}: ${error.message}`);
399
+ throw error;
440
400
  }
441
- getSelectedAccountId() {
442
- return this.selectedAccountId;
401
+ }
402
+ async removeAccount(accountId) {
403
+ const accounts = await this.listAccounts();
404
+ const account = accounts.find((acc) => acc.homeAccountId === accountId);
405
+ if (!account) {
406
+ logger.error(`Account with ID ${accountId} not found`);
407
+ return false;
443
408
  }
444
- requiresWorkAccountScope(toolName) {
445
- const endpoint = endpoints.default.find((e) => e.toolName === toolName);
446
- return endpoint?.requiresWorkAccount === true;
409
+ try {
410
+ await this.msalApp.getTokenCache().removeAccount(account);
411
+ if (this.selectedAccountId === accountId) {
412
+ this.selectedAccountId = null;
413
+ await this.saveSelectedAccount();
414
+ this.accessToken = null;
415
+ this.tokenExpiry = null;
416
+ }
417
+ logger.info(`Removed account: ${account.username} (${accountId})`);
418
+ return true;
419
+ } catch (error) {
420
+ logger.error(`Failed to remove account ${accountId}: ${error.message}`);
421
+ return false;
447
422
  }
423
+ }
424
+ getSelectedAccountId() {
425
+ return this.selectedAccountId;
426
+ }
427
+ requiresWorkAccountScope(toolName) {
428
+ const endpoint = endpoints.default.find((e) => e.toolName === toolName);
429
+ return endpoint?.requiresWorkAccount === true;
430
+ }
448
431
  }
449
- export default AuthManager;
450
- export { buildScopesFromEndpoints, buildAllScopes };
432
+ var auth_default = AuthManager;
433
+ export {
434
+ buildAllScopes,
435
+ buildScopesFromEndpoints,
436
+ auth_default as default
437
+ };