@littlebearapps/outlook-assistant 3.7.1 → 3.7.2
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/auth/token-storage.js +18 -6
- package/auth/tools.js +67 -3
- package/package.json +1 -1
package/auth/token-storage.js
CHANGED
|
@@ -49,11 +49,13 @@ class TokenStorage {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
if (!this.config.clientId
|
|
52
|
+
if (!this.config.clientId) {
|
|
53
53
|
console.warn(
|
|
54
|
-
'TokenStorage: OUTLOOK_CLIENT_ID
|
|
54
|
+
'TokenStorage: OUTLOOK_CLIENT_ID is not configured. Token operations will fail.'
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
|
+
// client_secret is only required for browser flow (confidential client).
|
|
58
|
+
// Device code flow (public client) does not use client_secret.
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
async _loadTokensFromFile() {
|
|
@@ -158,14 +160,24 @@ class TokenStorage {
|
|
|
158
160
|
return this._refreshPromise.then((tokens) => tokens.access_token);
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
// Device code flow is a public client flow — Microsoft rejects client_secret
|
|
164
|
+
// in refresh requests for tokens obtained via device code.
|
|
165
|
+
// Browser flow (confidential client) requires client_secret.
|
|
166
|
+
const isDeviceCode = this.tokens.auth_method === 'device-code';
|
|
167
|
+
console.log(
|
|
168
|
+
`Attempting to refresh access token (auth_method: ${this.tokens.auth_method || 'browser'})...`
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const refreshParams = {
|
|
163
172
|
client_id: this.config.clientId,
|
|
164
|
-
client_secret: this.config.clientSecret,
|
|
165
173
|
grant_type: 'refresh_token',
|
|
166
174
|
refresh_token: this.tokens.refresh_token,
|
|
167
175
|
scope: this.config.scopes.join(' '),
|
|
168
|
-
}
|
|
176
|
+
};
|
|
177
|
+
if (!isDeviceCode) {
|
|
178
|
+
refreshParams.client_secret = this.config.clientSecret;
|
|
179
|
+
}
|
|
180
|
+
const postData = querystring.stringify(refreshParams);
|
|
169
181
|
|
|
170
182
|
const requestOptions = {
|
|
171
183
|
method: 'POST',
|
package/auth/tools.js
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
* Authentication-related tools for the Outlook Assistant server
|
|
3
3
|
*/
|
|
4
4
|
const config = require('../config');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
5
7
|
const tokenManager = require('./token-manager');
|
|
6
8
|
const { initiateDeviceCodeFlow, pollForToken } = require('./device-code');
|
|
7
9
|
|
|
10
|
+
// Path for persisting device code state across MCP server restarts
|
|
11
|
+
const DEVICE_CODE_STATE_PATH = path.join(
|
|
12
|
+
process.env.HOME || process.env.USERPROFILE,
|
|
13
|
+
'.outlook-assistant-pending-auth.json'
|
|
14
|
+
);
|
|
15
|
+
|
|
8
16
|
// Dynamic tool count — set by index.js after TOOLS array is built
|
|
9
17
|
let _toolCount = 0;
|
|
10
18
|
function setToolCount(count) {
|
|
@@ -89,12 +97,57 @@ async function handleAuthenticate(args) {
|
|
|
89
97
|
};
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
//
|
|
100
|
+
// In-memory state for pending device code flow (also persisted to disk)
|
|
93
101
|
let pendingDeviceCode = null;
|
|
94
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Save device code state to disk so it survives MCP server restarts.
|
|
105
|
+
* Uses mode 0o600 (owner-only) — same as token file.
|
|
106
|
+
* @param {object|null} state - Device code state or null to delete
|
|
107
|
+
*/
|
|
108
|
+
function saveDeviceCodeState(state) {
|
|
109
|
+
try {
|
|
110
|
+
if (state) {
|
|
111
|
+
fs.writeFileSync(DEVICE_CODE_STATE_PATH, JSON.stringify(state), {
|
|
112
|
+
mode: 0o600,
|
|
113
|
+
});
|
|
114
|
+
} else if (fs.existsSync(DEVICE_CODE_STATE_PATH)) {
|
|
115
|
+
fs.unlinkSync(DEVICE_CODE_STATE_PATH);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error(
|
|
119
|
+
`[AUTH] Failed to ${state ? 'save' : 'clean up'} device code state: ${error.message}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Load device code state from disk (fallback when in-memory state is lost).
|
|
126
|
+
* Returns null if no state exists or if the state has expired.
|
|
127
|
+
* @returns {object|null}
|
|
128
|
+
*/
|
|
129
|
+
function loadDeviceCodeState() {
|
|
130
|
+
try {
|
|
131
|
+
if (!fs.existsSync(DEVICE_CODE_STATE_PATH)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const state = JSON.parse(fs.readFileSync(DEVICE_CODE_STATE_PATH, 'utf8'));
|
|
135
|
+
if (Date.now() > state.expiresAt) {
|
|
136
|
+
console.error('[AUTH] Persisted device code has expired, cleaning up');
|
|
137
|
+
saveDeviceCodeState(null);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return state;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`[AUTH] Failed to load device code state: ${error.message}`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
95
147
|
/**
|
|
96
148
|
* Device code flow step 1 — request a code for the user to enter.
|
|
97
149
|
* Returns the code + URL immediately. Call device-code-complete to finish.
|
|
150
|
+
* State is persisted to disk so it survives MCP server restarts.
|
|
98
151
|
* @returns {object} - MCP response
|
|
99
152
|
*/
|
|
100
153
|
async function handleDeviceCodeAuth() {
|
|
@@ -116,13 +169,14 @@ async function handleDeviceCodeAuth() {
|
|
|
116
169
|
config.AUTH_CONFIG.scopes
|
|
117
170
|
);
|
|
118
171
|
|
|
119
|
-
// Store
|
|
172
|
+
// Store in memory and persist to disk
|
|
120
173
|
pendingDeviceCode = {
|
|
121
174
|
deviceCode: response.deviceCode,
|
|
122
175
|
interval: response.interval,
|
|
123
176
|
expiresIn: response.expiresIn,
|
|
124
177
|
expiresAt: Date.now() + response.expiresIn * 1000,
|
|
125
178
|
};
|
|
179
|
+
saveDeviceCodeState(pendingDeviceCode);
|
|
126
180
|
|
|
127
181
|
console.error(
|
|
128
182
|
`[AUTH] Device code: ${response.userCode}, expires in ${response.expiresIn}s`
|
|
@@ -146,9 +200,15 @@ async function handleDeviceCodeAuth() {
|
|
|
146
200
|
|
|
147
201
|
/**
|
|
148
202
|
* Device code flow step 2 — poll until the user completes authentication.
|
|
203
|
+
* Checks in-memory state first, falls back to disk-persisted state.
|
|
149
204
|
* @returns {object} - MCP response
|
|
150
205
|
*/
|
|
151
206
|
async function handleDeviceCodeComplete() {
|
|
207
|
+
// Try in-memory first, fall back to disk (survives server restarts)
|
|
208
|
+
if (!pendingDeviceCode) {
|
|
209
|
+
pendingDeviceCode = loadDeviceCodeState();
|
|
210
|
+
}
|
|
211
|
+
|
|
152
212
|
if (!pendingDeviceCode) {
|
|
153
213
|
return {
|
|
154
214
|
content: [
|
|
@@ -162,6 +222,7 @@ async function handleDeviceCodeComplete() {
|
|
|
162
222
|
|
|
163
223
|
if (Date.now() > pendingDeviceCode.expiresAt) {
|
|
164
224
|
pendingDeviceCode = null;
|
|
225
|
+
saveDeviceCodeState(null);
|
|
165
226
|
return {
|
|
166
227
|
content: [
|
|
167
228
|
{
|
|
@@ -184,8 +245,9 @@ async function handleDeviceCodeComplete() {
|
|
|
184
245
|
);
|
|
185
246
|
|
|
186
247
|
pendingDeviceCode = null;
|
|
248
|
+
saveDeviceCodeState(null);
|
|
187
249
|
|
|
188
|
-
// Save tokens using TokenStorage
|
|
250
|
+
// Save tokens using TokenStorage — mark as device-code auth
|
|
189
251
|
const TokenStorage = require('./token-storage');
|
|
190
252
|
const tokenStorage = new TokenStorage({
|
|
191
253
|
clientId: config.AUTH_CONFIG.clientId,
|
|
@@ -202,6 +264,7 @@ async function handleDeviceCodeComplete() {
|
|
|
202
264
|
expires_at: Date.now() + tokenResponse.expires_in * 1000,
|
|
203
265
|
scope: tokenResponse.scope,
|
|
204
266
|
token_type: tokenResponse.token_type,
|
|
267
|
+
auth_method: 'device-code',
|
|
205
268
|
};
|
|
206
269
|
await tokenStorage._saveTokensToFile();
|
|
207
270
|
|
|
@@ -217,6 +280,7 @@ async function handleDeviceCodeComplete() {
|
|
|
217
280
|
};
|
|
218
281
|
} catch (error) {
|
|
219
282
|
pendingDeviceCode = null;
|
|
283
|
+
saveDeviceCodeState(null);
|
|
220
284
|
return {
|
|
221
285
|
content: [
|
|
222
286
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebearapps/outlook-assistant",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.2",
|
|
4
4
|
"mcpName": "io.github.littlebearapps/outlook-assistant",
|
|
5
5
|
"description": "Outlook Assistant — MCP server with 22 tools for email, calendar, contacts, and settings via Microsoft Graph API",
|
|
6
6
|
"main": "index.js",
|