@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/.releaserc.json +12 -0
- package/README.md +2 -1
- package/bin/modules/simplified-openapi.mjs +465 -274
- package/dist/auth-tools.js +181 -173
- package/dist/auth.js +402 -415
- package/dist/cli.js +35 -36
- package/dist/generated/client.js +6976 -14312
- package/dist/generated/endpoint-types.js +0 -1
- package/dist/generated/hack.js +39 -33
- package/dist/graph-client.js +426 -473
- package/dist/graph-tools.js +217 -228
- package/dist/index.js +76 -79
- package/dist/lib/microsoft-auth.js +62 -72
- package/dist/logger.js +36 -27
- package/dist/oauth-provider.js +48 -47
- package/dist/server.js +277 -264
- package/dist/version.js +9 -6
- package/package.json +13 -3
- package/tsup.config.ts +30 -0
- package/bin/release.mjs +0 -69
package/dist/auth.js
CHANGED
|
@@ -1,450 +1,437 @@
|
|
|
1
|
-
import { PublicClientApplication } from
|
|
2
|
-
import keytar from
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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
|
|
9
|
+
const endpointsData = JSON.parse(
|
|
10
|
+
readFileSync(path.join(__dirname, "endpoints.json"), "utf8")
|
|
11
|
+
);
|
|
10
12
|
const endpoints = {
|
|
11
|
-
|
|
13
|
+
default: endpointsData
|
|
12
14
|
};
|
|
13
|
-
const SERVICE_NAME =
|
|
14
|
-
const TOKEN_CACHE_ACCOUNT =
|
|
15
|
-
const SELECTED_ACCOUNT_KEY =
|
|
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,
|
|
18
|
-
const SELECTED_ACCOUNT_PATH = path.join(FALLBACK_DIR,
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
+
return buildScopesFromEndpoints(true);
|
|
52
54
|
}
|
|
53
55
|
class AuthManager {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
this.isOAuthMode = true;
|
|
155
|
+
if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
|
|
156
|
+
return this.accessToken;
|
|
149
157
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
450
|
-
export {
|
|
432
|
+
var auth_default = AuthManager;
|
|
433
|
+
export {
|
|
434
|
+
buildAllScopes,
|
|
435
|
+
buildScopesFromEndpoints,
|
|
436
|
+
auth_default as default
|
|
437
|
+
};
|