@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.
- package/.env.example +27 -3
- package/README.md +108 -33
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/client-config.js +142 -0
- package/auth/index.js +4 -2
- package/auth/oauth-server.js +12 -2
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +46 -33
- package/auth/tools.js +223 -93
- package/calendar/attendees.js +36 -0
- package/calendar/cancel.js +9 -25
- package/calendar/create.js +42 -48
- package/calendar/decline.js +10 -25
- package/calendar/delete.js +10 -25
- package/calendar/index.js +20 -37
- package/calendar/list.js +4 -16
- package/calendar/preview.js +335 -0
- package/calendar/update.js +42 -86
- package/categories/index.js +59 -264
- package/config.js +36 -2
- package/contacts/index.js +72 -128
- package/email/attachments.js +42 -124
- package/email/conversations.js +44 -78
- package/email/delta.js +10 -34
- package/email/draft.js +140 -96
- package/email/export.js +141 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +85 -109
- package/email/list.js +4 -17
- package/email/mail-tips.js +86 -57
- package/email/mark-as-read.js +13 -49
- package/email/mime.js +14 -49
- package/email/read.js +16 -50
- package/email/search.js +46 -86
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +17 -16
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +6 -20
- package/index.js +23 -45
- package/llms-install.md +31 -7
- package/llms.txt +19 -10
- package/outlook-auth-server.js +10 -3
- package/package.json +6 -2
- package/request-handler.js +217 -116
- package/rules/create.js +27 -70
- package/rules/index.js +30 -92
- package/rules/list.js +5 -17
- package/rules/rule-builder.js +57 -20
- package/rules/update.js +26 -60
- package/server.js +37 -0
- package/settings/index.js +142 -143
- package/tools.js +30 -0
- package/utils/field-presets.js +4 -2
- package/utils/graph-api.js +65 -22
- package/utils/logger.js +251 -0
- package/utils/mock-data.js +91 -2
- package/utils/read-only.js +59 -0
- package/utils/response-formatter.js +54 -15
- package/utils/risk-classes.js +324 -0
- package/utils/safe-write.js +372 -6
- package/utils/safety.js +109 -25
- package/utils/server-instructions.js +62 -0
- package/utils/tool-error.js +33 -0
package/auth/token-storage.js
CHANGED
|
@@ -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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
+
log.debug('No access token available.');
|
|
153
168
|
return null;
|
|
154
169
|
}
|
|
155
170
|
|
|
156
171
|
if (this.isTokenExpired()) {
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
265
|
+
log.debug('Access token refreshed and saved successfully.');
|
|
251
266
|
resolve(this.tokens);
|
|
252
267
|
} catch (saveError) {
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
+
log.debug('Exchanging authorization code for tokens...');
|
|
307
323
|
const requestedScopes = this.config.scopes;
|
|
308
324
|
const postData = querystring.stringify({
|
|
309
|
-
client_id:
|
|
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
|
-
|
|
370
|
+
log.debug('Tokens exchanged and saved successfully.');
|
|
355
371
|
resolve(this.tokens);
|
|
356
372
|
} catch (saveError) {
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
+
log.debug('Token file deleted successfully.');
|
|
412
425
|
} catch (error) {
|
|
413
426
|
if (error.code === 'ENOENT') {
|
|
414
|
-
|
|
427
|
+
log.debug('Token file not found, nothing to delete.');
|
|
415
428
|
} else {
|
|
416
|
-
|
|
429
|
+
log.debug('Error deleting token file:', error);
|
|
417
430
|
}
|
|
418
431
|
}
|
|
419
432
|
}
|