@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.
@@ -49,11 +49,13 @@ class TokenStorage {
49
49
  }
50
50
  }
51
51
 
52
- if (!this.config.clientId || !this.config.clientSecret) {
52
+ if (!this.config.clientId) {
53
53
  console.warn(
54
- 'TokenStorage: OUTLOOK_CLIENT_ID or OUTLOOK_CLIENT_SECRET is not configured. Token operations might fail.'
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
- console.log('Attempting to refresh access token...');
162
- const postData = querystring.stringify({
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
- // Module-level state for pending device code flow
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 for the completion step
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.1",
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",