@littlebearapps/outlook-assistant 3.7.2 → 3.8.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 +19 -0
- package/README.md +47 -21
- package/advanced/index.js +26 -3
- package/auth/tools.js +54 -1
- package/calendar/index.js +126 -4
- package/calendar/update.js +287 -0
- package/categories/index.js +67 -28
- package/config.js +51 -5
- package/contacts/index.js +118 -27
- package/email/attachments.js +13 -3
- package/email/conversations.js +4 -7
- package/email/delta.js +20 -6
- package/email/export.js +26 -3
- package/email/index.js +38 -4
- package/email/list.js +6 -1
- package/email/mail-tips.js +26 -2
- package/email/search.js +82 -25
- package/folder/create.js +6 -1
- package/folder/index.js +10 -1
- package/index.js +33 -0
- package/llms.txt +8 -2
- package/outlook-auth-server.js +13 -4
- package/package.json +2 -2
- package/rules/create.js +5 -1
- package/rules/index.js +19 -1
- package/rules/update.js +11 -3
- package/settings/index.js +70 -2
- package/utils/response-formatter.js +27 -0
- package/utils/schema-coerce.js +248 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update event functionality.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the Microsoft Graph PATCH /me/events/{id} endpoint. Sends only
|
|
5
|
+
* the fields the caller provides — anything left out is preserved
|
|
6
|
+
* server-side. Useful for re-scheduling, adding/removing attendees,
|
|
7
|
+
* editing the body, or tweaking metadata without rebuilding the event
|
|
8
|
+
* from scratch (which loses RSVP state).
|
|
9
|
+
*
|
|
10
|
+
* Supports the following Graph event properties:
|
|
11
|
+
* - subject
|
|
12
|
+
* - start (ISO string or {dateTime, timeZone} object)
|
|
13
|
+
* - end (same shape as start)
|
|
14
|
+
* - attendees (full replacement list of emails)
|
|
15
|
+
* - body (sent as HTML)
|
|
16
|
+
* - location (displayName)
|
|
17
|
+
* - isOnlineMeeting
|
|
18
|
+
* - sensitivity (normal | personal | private | confidential)
|
|
19
|
+
* - showAs (free | tentative | busy | oof | workingElsewhere | unknown)
|
|
20
|
+
* - importance (low | normal | high)
|
|
21
|
+
* - categories (full replacement array of category names)
|
|
22
|
+
* - reminderMinutesBeforeStart
|
|
23
|
+
*
|
|
24
|
+
* `dryRun: true` returns a preview of the PATCH payload without calling
|
|
25
|
+
* Graph — useful for confirming behaviour before mutating real data.
|
|
26
|
+
*/
|
|
27
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
28
|
+
const { ensureAuthenticated } = require('../auth');
|
|
29
|
+
const { DEFAULT_TIMEZONE } = require('../config');
|
|
30
|
+
|
|
31
|
+
const SENSITIVITY_VALUES = new Set([
|
|
32
|
+
'normal',
|
|
33
|
+
'personal',
|
|
34
|
+
'private',
|
|
35
|
+
'confidential',
|
|
36
|
+
]);
|
|
37
|
+
const SHOW_AS_VALUES = new Set([
|
|
38
|
+
'free',
|
|
39
|
+
'tentative',
|
|
40
|
+
'busy',
|
|
41
|
+
'oof',
|
|
42
|
+
'workingElsewhere',
|
|
43
|
+
'unknown',
|
|
44
|
+
]);
|
|
45
|
+
const IMPORTANCE_VALUES = new Set(['low', 'normal', 'high']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Update event handler
|
|
49
|
+
* @param {object} args - Tool arguments
|
|
50
|
+
* @returns {object} - MCP response
|
|
51
|
+
*/
|
|
52
|
+
async function handleUpdateEvent(args) {
|
|
53
|
+
const {
|
|
54
|
+
eventId,
|
|
55
|
+
subject,
|
|
56
|
+
start,
|
|
57
|
+
end,
|
|
58
|
+
attendees,
|
|
59
|
+
body,
|
|
60
|
+
location,
|
|
61
|
+
isOnlineMeeting,
|
|
62
|
+
sensitivity,
|
|
63
|
+
showAs,
|
|
64
|
+
importance,
|
|
65
|
+
categories,
|
|
66
|
+
reminderMinutesBeforeStart,
|
|
67
|
+
dryRun = false,
|
|
68
|
+
} = args;
|
|
69
|
+
|
|
70
|
+
if (!eventId) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: 'Event ID is required to update an event.',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Build the patch body from only the fields the caller actually provided.
|
|
82
|
+
// Graph treats absent properties as "no change", so we never overwrite
|
|
83
|
+
// something the user didn't intend to touch.
|
|
84
|
+
const patch = {};
|
|
85
|
+
|
|
86
|
+
if (subject !== undefined) patch.subject = subject;
|
|
87
|
+
|
|
88
|
+
if (start !== undefined) {
|
|
89
|
+
patch.start = {
|
|
90
|
+
dateTime: start.dateTime || start,
|
|
91
|
+
timeZone: start.timeZone || DEFAULT_TIMEZONE,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (end !== undefined) {
|
|
96
|
+
patch.end = {
|
|
97
|
+
dateTime: end.dateTime || end,
|
|
98
|
+
timeZone: end.timeZone || DEFAULT_TIMEZONE,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (attendees !== undefined) {
|
|
103
|
+
// Replaces the full attendee list — Graph PATCH on this property is
|
|
104
|
+
// not additive. Caller must pass the desired complete list.
|
|
105
|
+
patch.attendees = (attendees || []).map((email) => ({
|
|
106
|
+
emailAddress: { address: email },
|
|
107
|
+
type: 'required',
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (body !== undefined) {
|
|
112
|
+
patch.body = { contentType: 'HTML', content: body };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (location !== undefined) {
|
|
116
|
+
patch.location = { displayName: location };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isOnlineMeeting !== undefined) {
|
|
120
|
+
patch.isOnlineMeeting = Boolean(isOnlineMeeting);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (sensitivity !== undefined) {
|
|
124
|
+
if (!SENSITIVITY_VALUES.has(sensitivity)) {
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: `Invalid sensitivity: '${sensitivity}'. Must be one of: ${[...SENSITIVITY_VALUES].join(', ')}.`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
patch.sensitivity = sensitivity;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (showAs !== undefined) {
|
|
138
|
+
if (!SHOW_AS_VALUES.has(showAs)) {
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: `Invalid showAs: '${showAs}'. Must be one of: ${[...SHOW_AS_VALUES].join(', ')}.`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
patch.showAs = showAs;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (importance !== undefined) {
|
|
152
|
+
if (!IMPORTANCE_VALUES.has(importance)) {
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: 'text',
|
|
157
|
+
text: `Invalid importance: '${importance}'. Must be one of: ${[...IMPORTANCE_VALUES].join(', ')}.`,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
patch.importance = importance;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (categories !== undefined) {
|
|
166
|
+
// Full replacement, like attendees. Pass [] to clear all categories.
|
|
167
|
+
patch.categories = Array.isArray(categories) ? categories : [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (reminderMinutesBeforeStart !== undefined) {
|
|
171
|
+
const reminder = Number(reminderMinutesBeforeStart);
|
|
172
|
+
if (!Number.isFinite(reminder) || reminder < 0) {
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{
|
|
176
|
+
type: 'text',
|
|
177
|
+
text: `Invalid reminderMinutesBeforeStart: '${reminderMinutesBeforeStart}'. Must be a non-negative number.`,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
patch.reminderMinutesBeforeStart = reminder;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (Object.keys(patch).length === 0) {
|
|
186
|
+
return {
|
|
187
|
+
content: [
|
|
188
|
+
{
|
|
189
|
+
type: 'text',
|
|
190
|
+
text: 'No fields to update — provide at least one updatable field (subject, start, end, attendees, body, location, isOnlineMeeting, sensitivity, showAs, importance, categories, reminderMinutesBeforeStart).',
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// dryRun: don't touch Graph; just show the caller what would be sent.
|
|
197
|
+
if (dryRun) {
|
|
198
|
+
return {
|
|
199
|
+
content: [
|
|
200
|
+
{
|
|
201
|
+
type: 'text',
|
|
202
|
+
text: [
|
|
203
|
+
`**Dry run** — would PATCH \`me/events/${eventId}\` with:`,
|
|
204
|
+
'',
|
|
205
|
+
'```json',
|
|
206
|
+
JSON.stringify(patch, null, 2),
|
|
207
|
+
'```',
|
|
208
|
+
'',
|
|
209
|
+
`Fields that would change: ${Object.keys(patch).join(', ')}`,
|
|
210
|
+
].join('\n'),
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
_meta: {
|
|
214
|
+
eventId,
|
|
215
|
+
dryRun: true,
|
|
216
|
+
patch,
|
|
217
|
+
fieldsChanged: Object.keys(patch),
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const accessToken = await ensureAuthenticated();
|
|
224
|
+
const endpoint = `me/events/${eventId}`;
|
|
225
|
+
|
|
226
|
+
const response = await callGraphAPI(accessToken, 'PATCH', endpoint, patch);
|
|
227
|
+
|
|
228
|
+
const output = [
|
|
229
|
+
`Event '${response.subject || eventId}' updated successfully.`,
|
|
230
|
+
];
|
|
231
|
+
if (response.id) {
|
|
232
|
+
output.push(`**ID**: \`${response.id}\``);
|
|
233
|
+
}
|
|
234
|
+
if (response.start) {
|
|
235
|
+
output.push(
|
|
236
|
+
`**Start**: ${response.start.dateTime} (${response.start.timeZone})`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (response.end) {
|
|
240
|
+
output.push(
|
|
241
|
+
`**End**: ${response.end.dateTime} (${response.end.timeZone})`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (response.webLink) {
|
|
245
|
+
output.push(`**Link**: ${response.webLink}`);
|
|
246
|
+
}
|
|
247
|
+
output.push(`\nFields changed: ${Object.keys(patch).join(', ')}`);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
content: [
|
|
251
|
+
{
|
|
252
|
+
type: 'text',
|
|
253
|
+
text: output.join('\n'),
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
_meta: {
|
|
257
|
+
eventId: response.id,
|
|
258
|
+
subject: response.subject,
|
|
259
|
+
start: response.start,
|
|
260
|
+
end: response.end,
|
|
261
|
+
fieldsChanged: Object.keys(patch),
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error.message === 'Authentication required') {
|
|
266
|
+
return {
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: 'text',
|
|
270
|
+
text: "Authentication required. Please use the 'authenticate' tool first.",
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
content: [
|
|
278
|
+
{
|
|
279
|
+
type: 'text',
|
|
280
|
+
text: `Error updating event: ${error.message}`,
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = handleUpdateEvent;
|
package/categories/index.js
CHANGED
|
@@ -116,9 +116,9 @@ async function handleListCategories(args) {
|
|
|
116
116
|
output.push('|----------|-------|-----|');
|
|
117
117
|
categories.forEach((cat) => {
|
|
118
118
|
const colorName = COLOR_NAMES[cat.color] || cat.color;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
output.push(`| ${cat.displayName} | ${colorName} | ${
|
|
119
|
+
// F-9: emit full IDs at standard verbosity. Truncated IDs were
|
|
120
|
+
// unusable downstream (the ellipsis became part of the copy).
|
|
121
|
+
output.push(`| ${cat.displayName} | ${colorName} | ${cat.id} |`);
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -255,7 +255,9 @@ async function handleCreateCategory(args) {
|
|
|
255
255
|
* Update category handler
|
|
256
256
|
*/
|
|
257
257
|
async function handleUpdateCategory(args) {
|
|
258
|
-
|
|
258
|
+
// F-34: accept `categoryId` as a deprecated alias for `id`.
|
|
259
|
+
const id = args.id || args.categoryId;
|
|
260
|
+
const { displayName, color } = args;
|
|
259
261
|
|
|
260
262
|
if (!id) {
|
|
261
263
|
return {
|
|
@@ -298,34 +300,45 @@ async function handleUpdateCategory(args) {
|
|
|
298
300
|
if (displayName) updateData.displayName = displayName;
|
|
299
301
|
if (color) updateData.color = color;
|
|
300
302
|
|
|
301
|
-
|
|
303
|
+
await callGraphAPI(
|
|
302
304
|
accessToken,
|
|
303
305
|
'PATCH',
|
|
304
306
|
`me/outlook/masterCategories/${id}`,
|
|
305
307
|
updateData
|
|
306
308
|
);
|
|
307
309
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
310
|
+
// F-35: Graph silently drops master-category color updates on
|
|
311
|
+
// some account types. Re-fetch the category and diff requested
|
|
312
|
+
// values against what Graph actually stored, so the caller
|
|
313
|
+
// doesn't get a misleading "Category updated!" when nothing
|
|
314
|
+
// changed.
|
|
315
|
+
const fresh = await callGraphAPI(
|
|
316
|
+
accessToken,
|
|
317
|
+
'GET',
|
|
318
|
+
`me/outlook/masterCategories/${id}`
|
|
319
|
+
);
|
|
312
320
|
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
321
|
+
const warnings = [];
|
|
322
|
+
if (color && fresh.color !== color) {
|
|
323
|
+
warnings.push(
|
|
324
|
+
`Requested color \`${color}\` but Graph stored \`${fresh.color}\` (master-category colors may be immutable on this account type).`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (displayName && fresh.displayName !== displayName) {
|
|
328
|
+
warnings.push(
|
|
329
|
+
`Requested name \`${displayName}\` but Graph stored \`${fresh.displayName}\`.`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const colorName = COLOR_NAMES[fresh.color] || fresh.color;
|
|
334
|
+
let text = `Category updated!\n\n**Name**: ${fresh.displayName}\n**Color**: ${colorName} (${fresh.color})\n**ID**: ${fresh.id || id}`;
|
|
335
|
+
if (warnings.length > 0) {
|
|
336
|
+
text += `\n\n**⚠ Warning**:\n${warnings.map((w) => `- ${w}`).join('\n')}`;
|
|
337
|
+
}
|
|
318
338
|
|
|
319
339
|
return {
|
|
320
|
-
content: [
|
|
321
|
-
|
|
322
|
-
type: 'text',
|
|
323
|
-
text: `Category updated!\n\n**Name**: ${updatedName}\n**Color**: ${colorName} (${updatedColor})\n**ID**: ${response.id || id}`,
|
|
324
|
-
},
|
|
325
|
-
],
|
|
326
|
-
_meta: {
|
|
327
|
-
category: formatCategory(updatedCategory),
|
|
328
|
-
},
|
|
340
|
+
content: [{ type: 'text', text }],
|
|
341
|
+
_meta: { category: formatCategory(fresh) },
|
|
329
342
|
};
|
|
330
343
|
} catch (error) {
|
|
331
344
|
if (error.message === 'Authentication required') {
|
|
@@ -353,7 +366,8 @@ async function handleUpdateCategory(args) {
|
|
|
353
366
|
* Delete category handler
|
|
354
367
|
*/
|
|
355
368
|
async function handleDeleteCategory(args) {
|
|
356
|
-
|
|
369
|
+
// F-34: accept `categoryId` as a deprecated alias for `id`.
|
|
370
|
+
const id = args.id || args.categoryId;
|
|
357
371
|
|
|
358
372
|
if (!id) {
|
|
359
373
|
return {
|
|
@@ -824,8 +838,9 @@ const categoriesTools = [
|
|
|
824
838
|
properties: {
|
|
825
839
|
action: {
|
|
826
840
|
type: 'string',
|
|
827
|
-
enum: ['list', 'create', 'update', 'delete'],
|
|
828
|
-
description:
|
|
841
|
+
enum: ['list', 'create', 'update', 'set', 'delete'],
|
|
842
|
+
description:
|
|
843
|
+
"Action to perform (default: list). 'set' is a deprecated alias for 'update'.",
|
|
829
844
|
},
|
|
830
845
|
// list params
|
|
831
846
|
outputVerbosity: {
|
|
@@ -850,7 +865,12 @@ const categoriesTools = [
|
|
|
850
865
|
type: 'string',
|
|
851
866
|
description: 'Category ID (action=update/delete, required)',
|
|
852
867
|
},
|
|
868
|
+
categoryId: {
|
|
869
|
+
type: 'string',
|
|
870
|
+
description: 'DEPRECATED: alias for `id`. Will be removed in v3.8.0.',
|
|
871
|
+
},
|
|
853
872
|
},
|
|
873
|
+
additionalProperties: false,
|
|
854
874
|
required: [],
|
|
855
875
|
},
|
|
856
876
|
handler: async (args) => {
|
|
@@ -858,13 +878,22 @@ const categoriesTools = [
|
|
|
858
878
|
switch (action) {
|
|
859
879
|
case 'create':
|
|
860
880
|
return handleCreateCategory(args);
|
|
881
|
+
case 'set': // deprecated alias
|
|
861
882
|
case 'update':
|
|
862
883
|
return handleUpdateCategory(args);
|
|
863
884
|
case 'delete':
|
|
864
885
|
return handleDeleteCategory(args);
|
|
865
886
|
case 'list':
|
|
866
|
-
default:
|
|
867
887
|
return handleListCategories(args);
|
|
888
|
+
default:
|
|
889
|
+
return {
|
|
890
|
+
content: [
|
|
891
|
+
{
|
|
892
|
+
type: 'text',
|
|
893
|
+
text: `Unknown action '${action}'. Valid actions: list, create, update, delete.`,
|
|
894
|
+
},
|
|
895
|
+
],
|
|
896
|
+
};
|
|
868
897
|
}
|
|
869
898
|
},
|
|
870
899
|
},
|
|
@@ -901,6 +930,7 @@ const categoriesTools = [
|
|
|
901
930
|
'set (replace all), add (append), remove (remove specific). Default: set',
|
|
902
931
|
},
|
|
903
932
|
},
|
|
933
|
+
additionalProperties: false,
|
|
904
934
|
required: ['categories'],
|
|
905
935
|
},
|
|
906
936
|
handler: handleApplyCategory,
|
|
@@ -945,6 +975,7 @@ const categoriesTools = [
|
|
|
945
975
|
'Where to put emails from this sender (action=set, default: focused)',
|
|
946
976
|
},
|
|
947
977
|
},
|
|
978
|
+
additionalProperties: false,
|
|
948
979
|
required: [],
|
|
949
980
|
},
|
|
950
981
|
handler: async (args) => {
|
|
@@ -955,8 +986,16 @@ const categoriesTools = [
|
|
|
955
986
|
case 'delete':
|
|
956
987
|
return handleSetFocusedInboxOverride(args);
|
|
957
988
|
case 'list':
|
|
958
|
-
default:
|
|
959
989
|
return handleGetFocusedInboxOverrides(args);
|
|
990
|
+
default:
|
|
991
|
+
return {
|
|
992
|
+
content: [
|
|
993
|
+
{
|
|
994
|
+
type: 'text',
|
|
995
|
+
text: `Unknown action '${action}'. Valid actions: list, set, delete.`,
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
};
|
|
960
999
|
}
|
|
961
1000
|
},
|
|
962
1001
|
},
|
package/config.js
CHANGED
|
@@ -23,6 +23,48 @@ if (!homeDir) {
|
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the OAuth audience segment used in Microsoft Graph endpoints.
|
|
28
|
+
*
|
|
29
|
+
* Microsoft's identity platform v2.0 routes by audience:
|
|
30
|
+
* - `common` — personal AND work/school accounts (multi-tenant + personal)
|
|
31
|
+
* - `consumers` — personal Microsoft accounts only
|
|
32
|
+
* - `organizations` — work/school accounts only
|
|
33
|
+
* - `<tenant-guid>` — single-tenant
|
|
34
|
+
*
|
|
35
|
+
* The right value depends on the Azure app registration's "Supported account
|
|
36
|
+
* types" setting. An app registered as "Personal Microsoft accounts only" is
|
|
37
|
+
* rejected by `/common/` with `AADSTS9002331` and must use `/consumers/`;
|
|
38
|
+
* a single-tenant app must use its tenant GUID; etc.
|
|
39
|
+
*
|
|
40
|
+
* Defaulting to `common` preserves existing behaviour. Set
|
|
41
|
+
* `OUTLOOK_AUTH_AUDIENCE` to override.
|
|
42
|
+
*/
|
|
43
|
+
const AUTH_AUDIENCE = process.env.OUTLOOK_AUTH_AUDIENCE || 'common';
|
|
44
|
+
|
|
45
|
+
// Surface obvious misconfigurations at startup rather than failing later with a
|
|
46
|
+
// cryptic AADSTS error from Microsoft. Warn rather than throw so we never break
|
|
47
|
+
// an existing deployment on upgrade — Graph itself remains the source of truth
|
|
48
|
+
// for what audiences it accepts.
|
|
49
|
+
const VALID_AUDIENCE_LITERALS = new Set([
|
|
50
|
+
'common',
|
|
51
|
+
'consumers',
|
|
52
|
+
'organizations',
|
|
53
|
+
]);
|
|
54
|
+
const TENANT_GUID_RE =
|
|
55
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
56
|
+
if (
|
|
57
|
+
!VALID_AUDIENCE_LITERALS.has(AUTH_AUDIENCE) &&
|
|
58
|
+
!TENANT_GUID_RE.test(AUTH_AUDIENCE)
|
|
59
|
+
) {
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.warn(
|
|
62
|
+
`[outlook-assistant] OUTLOOK_AUTH_AUDIENCE="${AUTH_AUDIENCE}" is not a recognised value. ` +
|
|
63
|
+
`Expected one of: common, consumers, organizations, or a tenant GUID. ` +
|
|
64
|
+
`Proceeding anyway — Microsoft's identity platform will reject it at runtime if invalid.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
26
68
|
module.exports = {
|
|
27
69
|
// Server information
|
|
28
70
|
SERVER_NAME: 'outlook-assistant',
|
|
@@ -54,9 +96,10 @@ module.exports = {
|
|
|
54
96
|
],
|
|
55
97
|
tokenStorePath: path.join(homeDir, '.outlook-assistant-tokens.json'),
|
|
56
98
|
authServerUrl: 'http://localhost:3333',
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
tokenEndpoint:
|
|
99
|
+
audience: AUTH_AUDIENCE,
|
|
100
|
+
deviceCodeEndpoint: `https://login.microsoftonline.com/${AUTH_AUDIENCE}/oauth2/v2.0/devicecode`,
|
|
101
|
+
tokenEndpoint: `https://login.microsoftonline.com/${AUTH_AUDIENCE}/oauth2/v2.0/token`,
|
|
102
|
+
authorizeEndpoint: `https://login.microsoftonline.com/${AUTH_AUDIENCE}/oauth2/v2.0/authorize`,
|
|
60
103
|
defaultAuthMethod: process.env.OUTLOOK_AUTH_METHOD || 'device-code',
|
|
61
104
|
},
|
|
62
105
|
|
|
@@ -97,6 +140,9 @@ module.exports = {
|
|
|
97
140
|
// Immutable IDs (opt-in: IDs persist through folder moves)
|
|
98
141
|
USE_IMMUTABLE_IDS: process.env.OUTLOOK_IMMUTABLE_IDS === 'true',
|
|
99
142
|
|
|
100
|
-
// Timezone
|
|
101
|
-
|
|
143
|
+
// Timezone — IANA zone (e.g. "Australia/Melbourne", "Europe/London",
|
|
144
|
+
// "America/New_York"). Override per-deployment via OUTLOOK_DEFAULT_TIMEZONE.
|
|
145
|
+
// Default preserves the historic value for backwards compatibility.
|
|
146
|
+
DEFAULT_TIMEZONE:
|
|
147
|
+
process.env.OUTLOOK_DEFAULT_TIMEZONE || 'Australia/Melbourne',
|
|
102
148
|
};
|