@littlebearapps/outlook-assistant 3.12.1 → 3.14.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 (69) hide show
  1. package/.env.example +27 -3
  2. package/README.md +108 -33
  3. package/advanced/index.js +44 -174
  4. package/auth/auth-errors.js +23 -1
  5. package/auth/client-config.js +142 -0
  6. package/auth/index.js +4 -2
  7. package/auth/oauth-server.js +12 -2
  8. package/auth/token-manager.js +7 -3
  9. package/auth/token-storage.js +46 -33
  10. package/auth/tools.js +223 -93
  11. package/calendar/attendees.js +36 -0
  12. package/calendar/cancel.js +9 -25
  13. package/calendar/create.js +42 -48
  14. package/calendar/decline.js +10 -25
  15. package/calendar/delete.js +10 -25
  16. package/calendar/index.js +20 -37
  17. package/calendar/list.js +4 -16
  18. package/calendar/preview.js +335 -0
  19. package/calendar/update.js +42 -86
  20. package/categories/index.js +59 -264
  21. package/config.js +36 -2
  22. package/contacts/index.js +72 -128
  23. package/email/attachments.js +42 -124
  24. package/email/conversations.js +44 -78
  25. package/email/delta.js +10 -34
  26. package/email/draft.js +140 -96
  27. package/email/export.js +141 -110
  28. package/email/folder-utils.js +3 -2
  29. package/email/headers.js +11 -49
  30. package/email/index.js +85 -109
  31. package/email/list.js +4 -17
  32. package/email/mail-tips.js +86 -57
  33. package/email/mark-as-read.js +13 -49
  34. package/email/mime.js +14 -49
  35. package/email/read.js +16 -50
  36. package/email/search.js +46 -86
  37. package/email/send.js +82 -48
  38. package/folder/create.js +6 -25
  39. package/folder/delete.js +117 -38
  40. package/folder/index.js +17 -16
  41. package/folder/list.js +5 -17
  42. package/folder/move.js +13 -42
  43. package/folder/resolve.js +11 -6
  44. package/folder/stats.js +6 -20
  45. package/index.js +23 -45
  46. package/llms-install.md +31 -7
  47. package/llms.txt +19 -10
  48. package/outlook-auth-server.js +10 -3
  49. package/package.json +6 -2
  50. package/request-handler.js +217 -116
  51. package/rules/create.js +27 -70
  52. package/rules/index.js +30 -92
  53. package/rules/list.js +5 -17
  54. package/rules/rule-builder.js +57 -20
  55. package/rules/update.js +26 -60
  56. package/server.js +37 -0
  57. package/settings/index.js +142 -143
  58. package/tools.js +30 -0
  59. package/utils/field-presets.js +4 -2
  60. package/utils/graph-api.js +65 -22
  61. package/utils/logger.js +251 -0
  62. package/utils/mock-data.js +91 -2
  63. package/utils/read-only.js +59 -0
  64. package/utils/response-formatter.js +54 -15
  65. package/utils/risk-classes.js +324 -0
  66. package/utils/safe-write.js +372 -6
  67. package/utils/safety.js +109 -25
  68. package/utils/server-instructions.js +62 -0
  69. package/utils/tool-error.js +33 -0
@@ -3,7 +3,9 @@ const fsSync = require('fs');
3
3
  const path = require('path');
4
4
  const https = require('https');
5
5
  const querystring = require('querystring');
6
- const { describeAuthError } = require('./auth-errors');
6
+ const { describeAuthError, authErrorLogLabel } = require('./auth-errors');
7
+ const { resolveClientId } = require('./client-config');
8
+ const { log } = require('../utils/logger');
7
9
 
8
10
  /**
9
11
  * Decide which scopes a refresh request should use. Prefer the scopes that were
@@ -42,7 +44,9 @@ class TokenStorage {
42
44
  process.env.HOME || process.env.USERPROFILE,
43
45
  '.outlook-assistant-tokens.json'
44
46
  ),
45
- clientId: process.env.OUTLOOK_CLIENT_ID || process.env.MS_CLIENT_ID,
47
+ // No default clientId: when none is passed, getClientId() resolves it on
48
+ // every use (env → saved ~/.outlook-assistant-config.json), so an ID
49
+ // saved at runtime via the auth tool takes effect without a restart.
46
50
  clientSecret:
47
51
  process.env.OUTLOOK_CLIENT_SECRET || process.env.MS_CLIENT_SECRET,
48
52
  redirectUri:
@@ -80,7 +84,7 @@ class TokenStorage {
80
84
  }
81
85
  }
82
86
 
83
- if (!this.config.clientId) {
87
+ if (!this.getClientId()) {
84
88
  console.warn(
85
89
  'TokenStorage: OUTLOOK_CLIENT_ID is not configured. Token operations will fail.'
86
90
  );
@@ -89,6 +93,15 @@ class TokenStorage {
89
93
  // Device code flow (public client) does not use client_secret.
90
94
  }
91
95
 
96
+ /**
97
+ * Client ID for token requests: an explicitly configured value wins,
98
+ * otherwise it is resolved now (OUTLOOK_CLIENT_ID → MS_CLIENT_ID → saved).
99
+ * @returns {string}
100
+ */
101
+ getClientId() {
102
+ return this.config.clientId || resolveClientId();
103
+ }
104
+
92
105
  async _loadTokensFromFile() {
93
106
  try {
94
107
  const tokenData = await fs.readFile(this.config.tokenStorePath, 'utf8');
@@ -96,7 +109,8 @@ class TokenStorage {
96
109
  return this.tokens;
97
110
  } catch (error) {
98
111
  if (error.code !== 'ENOENT') {
99
- console.error('Error loading token cache:', error.message);
112
+ log.note('auth', 'token-cache-unreadable');
113
+ log.debug('Error loading token cache:', error.message);
100
114
  }
101
115
  this.tokens = null;
102
116
  return null;
@@ -114,7 +128,8 @@ class TokenStorage {
114
128
  { mode: 0o600 }
115
129
  );
116
130
  } catch (error) {
117
- console.error('Error saving token cache:', error.message);
131
+ log.note('auth', 'token-cache-save-failed');
132
+ log.debug('Error saving token cache:', error.message);
118
133
  throw error;
119
134
  }
120
135
  }
@@ -149,19 +164,20 @@ class TokenStorage {
149
164
  await this.getTokens(); // Ensure tokens are loaded
150
165
 
151
166
  if (!this.tokens || !this.tokens.access_token) {
152
- console.log('No access token available.');
167
+ log.debug('No access token available.');
153
168
  return null;
154
169
  }
155
170
 
156
171
  if (this.isTokenExpired()) {
157
- console.log(
172
+ log.debug(
158
173
  'Access token expired or nearing expiration. Attempting refresh.'
159
174
  );
160
175
  if (this.tokens.refresh_token) {
161
176
  try {
162
177
  return await this.refreshAccessToken();
163
178
  } catch (refreshError) {
164
- console.error('Failed to refresh access token:', refreshError);
179
+ log.note('auth', authErrorLogLabel('refresh-failed', refreshError));
180
+ log.debug('Failed to refresh access token:', refreshError);
165
181
  // Drop the in-memory tokens so callers re-authenticate. The save
166
182
  // below is intentionally a no-op — `_saveTokensToFile` returns early
167
183
  // when `tokens` is null — which is the behaviour we want: a transient
@@ -172,9 +188,8 @@ class TokenStorage {
172
188
  return null;
173
189
  }
174
190
  } else {
175
- console.warn(
176
- 'No refresh token available. Cannot refresh access token.'
177
- );
191
+ log.note('auth', 'no-refresh-token');
192
+ log.debug('No refresh token available. Cannot refresh access token.');
178
193
  // Same as above: clears memory, leaves the file alone. (#72)
179
194
  this.tokens = null;
180
195
  await this._saveTokensToFile();
@@ -193,7 +208,7 @@ class TokenStorage {
193
208
 
194
209
  // Prevent multiple concurrent refresh attempts
195
210
  if (this._refreshPromise) {
196
- console.log('Refresh already in progress, returning existing promise.');
211
+ log.debug('Refresh already in progress, returning existing promise.');
197
212
  return this._refreshPromise.then((tokens) => tokens.access_token);
198
213
  }
199
214
 
@@ -201,12 +216,12 @@ class TokenStorage {
201
216
  // in refresh requests for tokens obtained via device code.
202
217
  // Browser flow (confidential client) requires client_secret.
203
218
  const isDeviceCode = this.tokens.auth_method === 'device-code';
204
- console.log(
219
+ log.debug(
205
220
  `Attempting to refresh access token (auth_method: ${this.tokens.auth_method || 'browser'})...`
206
221
  );
207
222
 
208
223
  const refreshParams = {
209
- client_id: this.config.clientId,
224
+ client_id: this.getClientId(),
210
225
  grant_type: 'refresh_token',
211
226
  refresh_token: this.tokens.refresh_token,
212
227
  // Use the GRANTED scopes, not the full configured set. After a base-only
@@ -247,10 +262,10 @@ class TokenStorage {
247
262
  Date.now() + responseBody.expires_in * 1000;
248
263
  try {
249
264
  await this._saveTokensToFile();
250
- console.log('Access token refreshed and saved successfully.');
265
+ log.debug('Access token refreshed and saved successfully.');
251
266
  resolve(this.tokens);
252
267
  } catch (saveError) {
253
- console.error('Failed to save refreshed tokens:', saveError);
268
+ log.debug('Failed to save refreshed tokens:', saveError);
254
269
  // Even if save fails, tokens are updated in memory.
255
270
  // Depending on desired strictness, could reject here.
256
271
  // For now, resolve with in-memory tokens but log critical error.
@@ -262,7 +277,7 @@ class TokenStorage {
262
277
  );
263
278
  }
264
279
  } else {
265
- console.error('Error refreshing token:', responseBody);
280
+ log.debug('Error refreshing token:', responseBody);
266
281
  reject(
267
282
  new Error(
268
283
  describeAuthError(
@@ -274,7 +289,7 @@ class TokenStorage {
274
289
  }
275
290
  } catch (e) {
276
291
  // Catch any error during parsing or saving
277
- console.error(
292
+ log.debug(
278
293
  'Error processing refresh token response or saving tokens:',
279
294
  e
280
295
  );
@@ -286,7 +301,7 @@ class TokenStorage {
286
301
  }
287
302
  );
288
303
  req.on('error', (error) => {
289
- console.error('HTTP error during token refresh:', error);
304
+ log.debug('HTTP error during token refresh:', error);
290
305
  reject(error);
291
306
  this._refreshPromise = null; // Clear promise on error
292
307
  });
@@ -298,15 +313,16 @@ class TokenStorage {
298
313
  }
299
314
 
300
315
  async exchangeCodeForTokens(authCode) {
301
- if (!this.config.clientId || !this.config.clientSecret) {
316
+ const clientId = this.getClientId();
317
+ if (!clientId || !this.config.clientSecret) {
302
318
  throw new Error(
303
319
  'Client ID or Client Secret is not configured. Cannot exchange code for tokens.'
304
320
  );
305
321
  }
306
- console.log('Exchanging authorization code for tokens...');
322
+ log.debug('Exchanging authorization code for tokens...');
307
323
  const requestedScopes = this.config.scopes;
308
324
  const postData = querystring.stringify({
309
- client_id: this.config.clientId,
325
+ client_id: clientId,
310
326
  client_secret: this.config.clientSecret,
311
327
  grant_type: 'authorization_code',
312
328
  code: authCode,
@@ -351,10 +367,10 @@ class TokenStorage {
351
367
  };
352
368
  try {
353
369
  await this._saveTokensToFile();
354
- console.log('Tokens exchanged and saved successfully.');
370
+ log.debug('Tokens exchanged and saved successfully.');
355
371
  resolve(this.tokens);
356
372
  } catch (saveError) {
357
- console.error('Failed to save exchanged tokens:', saveError);
373
+ log.debug('Failed to save exchanged tokens:', saveError);
358
374
  // Similar to refresh, tokens are in memory but not persisted.
359
375
  // Rejecting to indicate the operation wasn't fully successful.
360
376
  reject(
@@ -364,10 +380,7 @@ class TokenStorage {
364
380
  );
365
381
  }
366
382
  } else {
367
- console.error(
368
- 'Error exchanging code for tokens:',
369
- responseBody
370
- );
383
+ log.debug('Error exchanging code for tokens:', responseBody);
371
384
  reject(
372
385
  new Error(
373
386
  describeAuthError(
@@ -379,7 +392,7 @@ class TokenStorage {
379
392
  }
380
393
  } catch (e) {
381
394
  // Catch any error during parsing or saving
382
- console.error(
395
+ log.debug(
383
396
  'Error processing token exchange response or saving tokens:',
384
397
  e,
385
398
  'Raw data:',
@@ -395,7 +408,7 @@ class TokenStorage {
395
408
  }
396
409
  );
397
410
  req.on('error', (error) => {
398
- console.error('HTTP error during code exchange:', error);
411
+ log.debug('HTTP error during code exchange:', error);
399
412
  reject(error);
400
413
  });
401
414
  req.write(postData);
@@ -408,12 +421,12 @@ class TokenStorage {
408
421
  this.tokens = null;
409
422
  try {
410
423
  await fs.unlink(this.config.tokenStorePath);
411
- console.log('Token file deleted successfully.');
424
+ log.debug('Token file deleted successfully.');
412
425
  } catch (error) {
413
426
  if (error.code === 'ENOENT') {
414
- console.log('Token file not found, nothing to delete.');
427
+ log.debug('Token file not found, nothing to delete.');
415
428
  } else {
416
- console.error('Error deleting token file:', error);
429
+ log.debug('Error deleting token file:', error);
417
430
  }
418
431
  }
419
432
  }