@littlebearapps/outlook-assistant 3.4.1 → 3.5.1

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.
@@ -12,6 +12,7 @@ const mockData = require('./mock-data');
12
12
  * @param {string} path - API endpoint path
13
13
  * @param {object} data - Data to send for POST/PUT requests
14
14
  * @param {object} queryParams - Query parameters
15
+ * @param {object} extraHeaders - Additional headers (e.g. Prefer for immutable IDs)
15
16
  * @returns {Promise<object>} - The API response
16
17
  */
17
18
  async function callGraphAPI(
@@ -19,7 +20,8 @@ async function callGraphAPI(
19
20
  method,
20
21
  path,
21
22
  data = null,
22
- queryParams = {}
23
+ queryParams = {},
24
+ extraHeaders = {}
23
25
  ) {
24
26
  // For test tokens, we'll simulate the API call
25
27
  if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
@@ -75,12 +77,22 @@ async function callGraphAPI(
75
77
  }
76
78
 
77
79
  return new Promise((resolve, reject) => {
80
+ const headers = {
81
+ Authorization: `Bearer ${accessToken}`,
82
+ 'Content-Type': 'application/json',
83
+ };
84
+
85
+ // Add immutable IDs header when enabled globally
86
+ if (config.USE_IMMUTABLE_IDS) {
87
+ headers.Prefer = 'IdType="ImmutableId"';
88
+ }
89
+
90
+ // Merge any extra headers (caller overrides take precedence)
91
+ Object.assign(headers, extraHeaders);
92
+
78
93
  const options = {
79
94
  method: method,
80
- headers: {
81
- Authorization: `Bearer ${accessToken}`,
82
- 'Content-Type': 'application/json',
83
- },
95
+ headers,
84
96
  };
85
97
 
86
98
  const req = https.request(finalUrl, options, (res) => {
@@ -202,6 +214,58 @@ async function callGraphAPIPaginated(
202
214
  }
203
215
  }
204
216
 
217
+ /**
218
+ * Sends multiple Graph API requests in a single batch call ($batch).
219
+ * Supports up to 20 requests per batch (Graph API limit).
220
+ * @param {string} accessToken - The access token for authentication
221
+ * @param {Array<{id: string, method: string, url: string, body?: object, headers?: object}>} requests - Batch requests
222
+ * @returns {Promise<Array<{id: string, status: number, body: object}>>} - Array of responses
223
+ */
224
+ async function callGraphAPIBatch(accessToken, requests) {
225
+ if (!Array.isArray(requests) || requests.length === 0) {
226
+ throw new Error('Batch requests must be a non-empty array');
227
+ }
228
+
229
+ if (requests.length > 20) {
230
+ throw new Error('Batch requests cannot exceed 20 (Graph API limit)');
231
+ }
232
+
233
+ // Test mode
234
+ if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
235
+ return requests.map((req) => ({
236
+ id: req.id,
237
+ status: 200,
238
+ body: mockData.simulateGraphAPIResponse(
239
+ req.method,
240
+ req.url,
241
+ req.body || null,
242
+ {}
243
+ ),
244
+ }));
245
+ }
246
+
247
+ const batchPayload = {
248
+ requests: requests.map((req) => ({
249
+ id: req.id,
250
+ method: req.method,
251
+ url: req.url.startsWith('/') ? req.url : `/${req.url}`,
252
+ ...(req.body && { body: req.body }),
253
+ ...(req.headers && { headers: req.headers }),
254
+ })),
255
+ };
256
+
257
+ const response = await callGraphAPI(
258
+ accessToken,
259
+ 'POST',
260
+ '$batch',
261
+ batchPayload
262
+ );
263
+
264
+ return (response.responses || []).sort(
265
+ (a, b) => parseInt(a.id) - parseInt(b.id)
266
+ );
267
+ }
268
+
205
269
  /**
206
270
  * Calls Graph API to get raw MIME content (for email export)
207
271
  * @param {string} accessToken - The access token for authentication
@@ -261,8 +325,67 @@ async function callGraphAPIRaw(accessToken, emailId) {
261
325
  });
262
326
  }
263
327
 
328
+ /**
329
+ * Calls Graph API with automatic auth and 401 retry.
330
+ * Gets token via ensureAuthenticated(), and if a 401 occurs,
331
+ * refreshes the token and retries once.
332
+ * @param {string} method - HTTP method
333
+ * @param {string} path - API endpoint path
334
+ * @param {object} data - Request body
335
+ * @param {object} queryParams - Query parameters
336
+ * @param {object} extraHeaders - Additional headers
337
+ * @returns {Promise<object>} - API response
338
+ */
339
+ async function callGraphAPIWithAuth(
340
+ method,
341
+ path,
342
+ data = null,
343
+ queryParams = {},
344
+ extraHeaders = {}
345
+ ) {
346
+ // Lazy require to avoid circular dependency
347
+ const { ensureAuthenticated, tokenStorage } = require('../auth');
348
+
349
+ const accessToken = await ensureAuthenticated();
350
+ try {
351
+ return await callGraphAPI(
352
+ accessToken,
353
+ method,
354
+ path,
355
+ data,
356
+ queryParams,
357
+ extraHeaders
358
+ );
359
+ } catch (error) {
360
+ if (error.message === 'UNAUTHORIZED' && tokenStorage) {
361
+ console.error('[GRAPH-API] 401 received, attempting token refresh...');
362
+ try {
363
+ const newToken = await tokenStorage.refreshAccessToken();
364
+ if (newToken) {
365
+ return await callGraphAPI(
366
+ newToken,
367
+ method,
368
+ path,
369
+ data,
370
+ queryParams,
371
+ extraHeaders
372
+ );
373
+ }
374
+ } catch (refreshError) {
375
+ console.error(
376
+ '[GRAPH-API] Token refresh failed:',
377
+ refreshError.message
378
+ );
379
+ }
380
+ }
381
+ throw error;
382
+ }
383
+ }
384
+
264
385
  module.exports = {
265
386
  callGraphAPI,
266
387
  callGraphAPIPaginated,
388
+ callGraphAPIBatch,
267
389
  callGraphAPIRaw,
390
+ callGraphAPIWithAuth,
268
391
  };