m365-agent-cli 1.2.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 (92) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +916 -0
  3. package/package.json +50 -0
  4. package/src/cli.ts +100 -0
  5. package/src/commands/auto-reply.ts +182 -0
  6. package/src/commands/calendar.ts +576 -0
  7. package/src/commands/counter.ts +87 -0
  8. package/src/commands/create-event.ts +544 -0
  9. package/src/commands/delegates.ts +286 -0
  10. package/src/commands/delete-event.ts +321 -0
  11. package/src/commands/drafts.ts +502 -0
  12. package/src/commands/files.ts +532 -0
  13. package/src/commands/find.ts +195 -0
  14. package/src/commands/findtime.ts +270 -0
  15. package/src/commands/folders.ts +177 -0
  16. package/src/commands/forward-event.ts +49 -0
  17. package/src/commands/graph-calendar.ts +217 -0
  18. package/src/commands/login.ts +195 -0
  19. package/src/commands/mail.ts +950 -0
  20. package/src/commands/oof.ts +263 -0
  21. package/src/commands/outlook-categories.ts +173 -0
  22. package/src/commands/outlook-graph.ts +880 -0
  23. package/src/commands/planner.ts +1678 -0
  24. package/src/commands/respond.ts +291 -0
  25. package/src/commands/rooms.ts +210 -0
  26. package/src/commands/rules.ts +511 -0
  27. package/src/commands/schedule.ts +109 -0
  28. package/src/commands/send.ts +204 -0
  29. package/src/commands/serve.ts +14 -0
  30. package/src/commands/sharepoint.ts +179 -0
  31. package/src/commands/site-pages.ts +163 -0
  32. package/src/commands/subscribe.ts +103 -0
  33. package/src/commands/subscriptions.ts +29 -0
  34. package/src/commands/suggest.ts +155 -0
  35. package/src/commands/todo.ts +2092 -0
  36. package/src/commands/update-event.ts +608 -0
  37. package/src/commands/update.ts +88 -0
  38. package/src/commands/verify-token.ts +62 -0
  39. package/src/commands/whoami.ts +74 -0
  40. package/src/index.ts +190 -0
  41. package/src/lib/atomic-write.ts +20 -0
  42. package/src/lib/attach-link-spec.test.ts +24 -0
  43. package/src/lib/attach-link-spec.ts +70 -0
  44. package/src/lib/attachments.ts +79 -0
  45. package/src/lib/auth.ts +192 -0
  46. package/src/lib/calendar-range.test.ts +41 -0
  47. package/src/lib/calendar-range.ts +103 -0
  48. package/src/lib/dates.test.ts +74 -0
  49. package/src/lib/dates.ts +137 -0
  50. package/src/lib/delegate-client.test.ts +74 -0
  51. package/src/lib/delegate-client.ts +322 -0
  52. package/src/lib/ews-client.ts +3418 -0
  53. package/src/lib/git-commit.ts +4 -0
  54. package/src/lib/glitchtip-eligibility.ts +220 -0
  55. package/src/lib/glitchtip.ts +253 -0
  56. package/src/lib/global-env.ts +3 -0
  57. package/src/lib/graph-auth.ts +223 -0
  58. package/src/lib/graph-calendar-client.test.ts +118 -0
  59. package/src/lib/graph-calendar-client.ts +112 -0
  60. package/src/lib/graph-client.test.ts +107 -0
  61. package/src/lib/graph-client.ts +1058 -0
  62. package/src/lib/graph-constants.ts +12 -0
  63. package/src/lib/graph-directory.ts +116 -0
  64. package/src/lib/graph-event.ts +134 -0
  65. package/src/lib/graph-schedule.ts +173 -0
  66. package/src/lib/graph-subscriptions.ts +94 -0
  67. package/src/lib/graph-user-path.ts +13 -0
  68. package/src/lib/jwt-utils.ts +34 -0
  69. package/src/lib/markdown.test.ts +21 -0
  70. package/src/lib/markdown.ts +174 -0
  71. package/src/lib/mime-type.ts +106 -0
  72. package/src/lib/oof-client.test.ts +59 -0
  73. package/src/lib/oof-client.ts +122 -0
  74. package/src/lib/outlook-graph-client.test.ts +146 -0
  75. package/src/lib/outlook-graph-client.ts +649 -0
  76. package/src/lib/outlook-master-categories.ts +145 -0
  77. package/src/lib/package-info.ts +59 -0
  78. package/src/lib/places-client.ts +144 -0
  79. package/src/lib/planner-client.ts +1226 -0
  80. package/src/lib/rules-client.ts +178 -0
  81. package/src/lib/sharepoint-client.ts +101 -0
  82. package/src/lib/site-pages-client.ts +73 -0
  83. package/src/lib/todo-client.test.ts +298 -0
  84. package/src/lib/todo-client.ts +1309 -0
  85. package/src/lib/url-validation.ts +40 -0
  86. package/src/lib/utils.ts +45 -0
  87. package/src/lib/webhook-server.ts +51 -0
  88. package/src/test/auth.test.ts +104 -0
  89. package/src/test/cli.integration.test.ts +1083 -0
  90. package/src/test/ews-client.test.ts +268 -0
  91. package/src/test/mocks/index.ts +375 -0
  92. package/src/test/mocks/responses.ts +861 -0
@@ -0,0 +1,1309 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import {
4
+ callGraph,
5
+ callGraphAbsolute,
6
+ fetchAllPages,
7
+ fetchGraphRaw,
8
+ GraphApiError,
9
+ type GraphResponse,
10
+ graphError,
11
+ graphResult
12
+ } from './graph-client.js';
13
+ import { graphUserPath } from './graph-user-path.js';
14
+
15
+ function todoRoot(user?: string): string {
16
+ return graphUserPath(user, 'todo');
17
+ }
18
+
19
+ export type TodoImportance = 'low' | 'normal' | 'high';
20
+ export type TodoStatus = 'notStarted' | 'inProgress' | 'completed' | 'waitingOnOthers' | 'deferred';
21
+
22
+ /** Graph [linkedResource](https://learn.microsoft.com/en-us/graph/api/resources/linkedresource); use `displayName` (alias: `description`). */
23
+ export interface TodoLinkedResource {
24
+ id?: string;
25
+ webUrl?: string;
26
+ /** Graph `displayName` (title of the link). */
27
+ displayName?: string;
28
+ /** Legacy alias for `displayName` when creating/updating. */
29
+ description?: string;
30
+ applicationName?: string;
31
+ externalId?: string;
32
+ iconUrl?: string;
33
+ }
34
+
35
+ /** Shape payload for Graph `linkedResources` on todoTask (PATCH/POST). */
36
+ export function linkedResourceToGraphPayload(lr: TodoLinkedResource): Record<string, unknown> {
37
+ const displayName = lr.displayName ?? lr.description;
38
+ const out: Record<string, unknown> = {};
39
+ if (displayName !== undefined && displayName !== '') out.displayName = displayName;
40
+ if (lr.webUrl !== undefined) out.webUrl = lr.webUrl;
41
+ if (lr.applicationName !== undefined) out.applicationName = lr.applicationName;
42
+ if (lr.externalId !== undefined) out.externalId = lr.externalId;
43
+ if (lr.id !== undefined) out.id = lr.id;
44
+ return out;
45
+ }
46
+
47
+ export interface TodoChecklistItem {
48
+ id: string;
49
+ displayName: string;
50
+ isChecked: boolean;
51
+ createdDateTime?: string;
52
+ /** Set when `isChecked` is true (Graph). */
53
+ checkedDateTime?: string;
54
+ }
55
+
56
+ export interface TodoTask {
57
+ id: string;
58
+ title: string;
59
+ body?: { content: string; contentType: string };
60
+ isReminderOn?: boolean;
61
+ reminderDateTime?: { dateTime: string; timeZone: string };
62
+ dueDateTime?: { dateTime: string; timeZone: string };
63
+ startDateTime?: { dateTime: string; timeZone: string };
64
+ importance?: TodoImportance;
65
+ status?: TodoStatus;
66
+ /** Outlook-style category labels (strings). */
67
+ categories?: string[];
68
+ linkedResources?: TodoLinkedResource[];
69
+ checklistItems?: TodoChecklistItem[];
70
+ /** Graph `patternedRecurrence` resource (opaque JSON). */
71
+ recurrence?: Record<string, unknown>;
72
+ hasAttachments?: boolean;
73
+ createdDateTime?: string;
74
+ lastModifiedDateTime?: string;
75
+ completedDateTime?: { dateTime: string; timeZone: string };
76
+ bodyLastModifiedDateTime?: string;
77
+ }
78
+
79
+ /** Small file attachment on a To Do task (Graph `taskFileAttachment`). */
80
+ export interface TodoAttachment {
81
+ id: string;
82
+ name?: string;
83
+ contentType?: string;
84
+ size?: number;
85
+ lastModifiedDateTime?: string;
86
+ '@odata.type'?: string;
87
+ }
88
+
89
+ export interface TodoList {
90
+ id: string;
91
+ displayName: string;
92
+ isOwner?: boolean;
93
+ isShared?: boolean;
94
+ parentSectionId?: string;
95
+ wellknownListName?: string;
96
+ }
97
+
98
+ export async function createTodoList(
99
+ token: string,
100
+ displayName: string,
101
+ user?: string
102
+ ): Promise<GraphResponse<TodoList>> {
103
+ let result: GraphResponse<TodoList>;
104
+ try {
105
+ result = await callGraph<TodoList>(token, `${todoRoot(user)}/lists`, {
106
+ method: 'POST',
107
+ body: JSON.stringify({ displayName })
108
+ });
109
+ } catch (err) {
110
+ if (err instanceof GraphApiError) {
111
+ return graphError(err.message, err.code, err.status);
112
+ }
113
+ return graphError(err instanceof Error ? err.message : 'Failed to create list');
114
+ }
115
+ if (!result.ok || !result.data) {
116
+ return graphError(result.error?.message || 'Failed to create list', result.error?.code, result.error?.status);
117
+ }
118
+ return graphResult(result.data);
119
+ }
120
+
121
+ export async function updateTodoList(
122
+ token: string,
123
+ listId: string,
124
+ displayName: string,
125
+ user?: string
126
+ ): Promise<GraphResponse<TodoList>> {
127
+ let result: GraphResponse<TodoList>;
128
+ try {
129
+ result = await callGraph<TodoList>(token, `${todoRoot(user)}/lists/${encodeURIComponent(listId)}`, {
130
+ method: 'PATCH',
131
+ body: JSON.stringify({ displayName })
132
+ });
133
+ } catch (err) {
134
+ if (err instanceof GraphApiError) {
135
+ return graphError(err.message, err.code, err.status);
136
+ }
137
+ return graphError(err instanceof Error ? err.message : 'Failed to update list');
138
+ }
139
+ if (!result.ok || !result.data) {
140
+ return graphError(result.error?.message || 'Failed to update list', result.error?.code, result.error?.status);
141
+ }
142
+ return graphResult(result.data);
143
+ }
144
+
145
+ export async function deleteTodoList(token: string, listId: string, user?: string): Promise<GraphResponse<void>> {
146
+ try {
147
+ return await callGraph<void>(
148
+ token,
149
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}`,
150
+ { method: 'DELETE' },
151
+ false
152
+ );
153
+ } catch (err) {
154
+ if (err instanceof GraphApiError) {
155
+ return graphError(err.message, err.code, err.status);
156
+ }
157
+ return graphError(err instanceof Error ? err.message : 'Failed to delete list');
158
+ }
159
+ }
160
+
161
+ export async function getTodoLists(token: string, user?: string): Promise<GraphResponse<TodoList[]>> {
162
+ let result: GraphResponse<{ value: TodoList[] }>;
163
+ try {
164
+ result = await callGraph<{ value: TodoList[] }>(token, `${todoRoot(user)}/lists`);
165
+ } catch (err) {
166
+ if (err instanceof GraphApiError) {
167
+ return graphError(err.message, err.code, err.status);
168
+ }
169
+ return graphError(err instanceof Error ? err.message : 'Failed to get todo lists');
170
+ }
171
+ if (!result.ok || !result.data) {
172
+ return graphError(result.error?.message || 'Failed to get todo lists', result.error?.code, result.error?.status);
173
+ }
174
+ return graphResult(result.data.value);
175
+ }
176
+
177
+ export async function getTodoList(token: string, listId: string, user?: string): Promise<GraphResponse<TodoList>> {
178
+ try {
179
+ return await callGraph<TodoList>(token, `${todoRoot(user)}/lists/${encodeURIComponent(listId)}`);
180
+ } catch (err) {
181
+ if (err instanceof GraphApiError) {
182
+ return graphError(err.message, err.code, err.status);
183
+ }
184
+ return graphError(err instanceof Error ? err.message : 'Failed to get todo list');
185
+ }
186
+ }
187
+
188
+ export interface TodoTasksQueryOptions {
189
+ filter?: string;
190
+ orderby?: string;
191
+ select?: string;
192
+ /** When set, only one page is returned (no automatic paging). */
193
+ top?: number;
194
+ skip?: number;
195
+ /** OData `$expand` (e.g. `attachments`). */
196
+ expand?: string;
197
+ /** Set `$count=true` (may require `ConsistencyLevel: eventual` on some tenants). */
198
+ count?: boolean;
199
+ }
200
+
201
+ function tasksListPath(listId: string, user: string | undefined, query?: string | TodoTasksQueryOptions): string {
202
+ const params = new URLSearchParams();
203
+ if (query === undefined) {
204
+ // no query params
205
+ } else if (typeof query === 'string') {
206
+ if (query) params.set('$filter', query);
207
+ } else {
208
+ if (query.filter) params.set('$filter', query.filter);
209
+ if (query.orderby) params.set('$orderby', query.orderby);
210
+ if (query.select) params.set('$select', query.select);
211
+ if (query.top !== undefined) params.set('$top', String(query.top));
212
+ if (query.skip !== undefined) params.set('$skip', String(query.skip));
213
+ if (query.expand) params.set('$expand', query.expand);
214
+ if (query.count) params.set('$count', 'true');
215
+ }
216
+ const qs = params.toString() ? `?${params.toString()}` : '';
217
+ return `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks${qs}`;
218
+ }
219
+
220
+ export async function getTasks(
221
+ token: string,
222
+ listId: string,
223
+ filterOrQuery?: string | TodoTasksQueryOptions,
224
+ user?: string
225
+ ): Promise<GraphResponse<TodoTask[]>> {
226
+ const path = tasksListPath(listId, user, filterOrQuery);
227
+ const singlePage =
228
+ filterOrQuery !== undefined &&
229
+ typeof filterOrQuery === 'object' &&
230
+ (filterOrQuery.top !== undefined || filterOrQuery.skip !== undefined || filterOrQuery.count === true);
231
+
232
+ if (singlePage) {
233
+ let result: GraphResponse<{ value: TodoTask[] }>;
234
+ try {
235
+ result = await callGraph<{ value: TodoTask[] }>(token, path);
236
+ } catch (err) {
237
+ if (err instanceof GraphApiError) {
238
+ return graphError(err.message, err.code, err.status);
239
+ }
240
+ return graphError(err instanceof Error ? err.message : 'Failed to get tasks');
241
+ }
242
+ if (!result.ok || !result.data) {
243
+ return graphError(result.error?.message || 'Failed to get tasks', result.error?.code, result.error?.status);
244
+ }
245
+ return graphResult(result.data.value);
246
+ }
247
+
248
+ return fetchAllPages<TodoTask>(token, path, 'Failed to get tasks');
249
+ }
250
+
251
+ export async function getTask(
252
+ token: string,
253
+ listId: string,
254
+ taskId: string,
255
+ user?: string,
256
+ options?: { select?: string }
257
+ ): Promise<GraphResponse<TodoTask>> {
258
+ try {
259
+ const qs = options?.select ? `?$select=${encodeURIComponent(options.select)}` : '';
260
+ return await callGraph<TodoTask>(
261
+ token,
262
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}${qs}`
263
+ );
264
+ } catch (err) {
265
+ if (err instanceof GraphApiError) {
266
+ return graphError(err.message, err.code, err.status);
267
+ }
268
+ return graphError(err instanceof Error ? err.message : 'Failed to get task');
269
+ }
270
+ }
271
+
272
+ export interface CreateTaskOptions {
273
+ title: string;
274
+ body?: string;
275
+ bodyContentType?: string;
276
+ dueDateTime?: string;
277
+ startDateTime?: string;
278
+ timeZone?: string;
279
+ dueTimeZone?: string;
280
+ startTimeZone?: string;
281
+ reminderTimeZone?: string;
282
+ importance?: TodoImportance;
283
+ status?: TodoStatus;
284
+ isReminderOn?: boolean;
285
+ reminderDateTime?: string;
286
+ linkedResources?: TodoLinkedResource[];
287
+ categories?: string[];
288
+ /** Graph `patternedRecurrence` (see Microsoft Graph docs). */
289
+ recurrence?: Record<string, unknown>;
290
+ }
291
+
292
+ export async function createTask(
293
+ token: string,
294
+ listId: string,
295
+ options: CreateTaskOptions,
296
+ user?: string
297
+ ): Promise<GraphResponse<TodoTask>> {
298
+ const payload: Record<string, unknown> = { title: options.title };
299
+ if (options.body) payload.body = { content: options.body, contentType: options.bodyContentType || 'text' };
300
+ if (options.dueDateTime) {
301
+ payload.dueDateTime = {
302
+ dateTime: options.dueDateTime,
303
+ timeZone: options.dueTimeZone ?? options.timeZone ?? 'UTC'
304
+ };
305
+ }
306
+ if (options.startDateTime) {
307
+ payload.startDateTime = {
308
+ dateTime: options.startDateTime,
309
+ timeZone: options.startTimeZone ?? options.timeZone ?? 'UTC'
310
+ };
311
+ }
312
+ if (options.importance) payload.importance = options.importance;
313
+ if (options.status) payload.status = options.status;
314
+ if (options.isReminderOn !== undefined) payload.isReminderOn = options.isReminderOn;
315
+ if (options.reminderDateTime) {
316
+ payload.reminderDateTime = {
317
+ dateTime: options.reminderDateTime,
318
+ timeZone: options.reminderTimeZone ?? options.timeZone ?? 'UTC'
319
+ };
320
+ }
321
+ if (options.linkedResources?.length) {
322
+ payload.linkedResources = options.linkedResources.map((lr) => linkedResourceToGraphPayload(lr));
323
+ }
324
+ if (options.categories?.length) payload.categories = options.categories;
325
+ if (options.recurrence !== undefined) payload.recurrence = options.recurrence;
326
+ let result: GraphResponse<TodoTask>;
327
+ try {
328
+ result = await callGraph<TodoTask>(token, `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks`, {
329
+ method: 'POST',
330
+ body: JSON.stringify(payload)
331
+ });
332
+ } catch (err) {
333
+ if (err instanceof GraphApiError) {
334
+ return graphError(err.message, err.code, err.status);
335
+ }
336
+ return graphError(err instanceof Error ? err.message : 'Failed to create task');
337
+ }
338
+ if (!result.ok || !result.data) {
339
+ return graphError(result.error?.message || 'Failed to create task', result.error?.code, result.error?.status);
340
+ }
341
+ return graphResult(result.data);
342
+ }
343
+
344
+ export interface UpdateTaskOptions {
345
+ title?: string;
346
+ body?: string;
347
+ bodyContentType?: string;
348
+ dueDateTime?: string | null;
349
+ startDateTime?: string | null;
350
+ timeZone?: string;
351
+ dueTimeZone?: string;
352
+ startTimeZone?: string;
353
+ reminderTimeZone?: string;
354
+ importance?: TodoImportance;
355
+ status?: TodoStatus;
356
+ isReminderOn?: boolean;
357
+ reminderDateTime?: string | null;
358
+ completedDateTime?: string | null;
359
+ linkedResources?: TodoLinkedResource[];
360
+ /** Replace categories when set (including empty array). */
361
+ categories?: string[];
362
+ /** When true, PATCH with categories: []. Ignored if categories is set. */
363
+ clearCategories?: boolean;
364
+ /** Set or clear recurrence; `null` removes recurrence from the task. */
365
+ recurrence?: Record<string, unknown> | null;
366
+ }
367
+
368
+ export async function updateTask(
369
+ token: string,
370
+ listId: string,
371
+ taskId: string,
372
+ options: UpdateTaskOptions,
373
+ user?: string
374
+ ): Promise<GraphResponse<TodoTask>> {
375
+ const payload: Record<string, unknown> = {};
376
+ if (options.title !== undefined) payload.title = options.title;
377
+ if (options.body !== undefined)
378
+ payload.body = { content: options.body, contentType: options.bodyContentType || 'text' };
379
+ if (options.dueDateTime !== undefined) {
380
+ payload.dueDateTime =
381
+ options.dueDateTime === null
382
+ ? null
383
+ : { dateTime: options.dueDateTime, timeZone: options.dueTimeZone ?? options.timeZone ?? 'UTC' };
384
+ }
385
+ if (options.startDateTime !== undefined) {
386
+ payload.startDateTime =
387
+ options.startDateTime === null
388
+ ? null
389
+ : { dateTime: options.startDateTime, timeZone: options.startTimeZone ?? options.timeZone ?? 'UTC' };
390
+ }
391
+ if (options.importance !== undefined) payload.importance = options.importance;
392
+ if (options.status !== undefined) payload.status = options.status;
393
+ if (options.isReminderOn !== undefined) payload.isReminderOn = options.isReminderOn;
394
+ if (options.reminderDateTime !== undefined) {
395
+ payload.reminderDateTime =
396
+ options.reminderDateTime === null
397
+ ? null
398
+ : { dateTime: options.reminderDateTime, timeZone: options.reminderTimeZone ?? options.timeZone ?? 'UTC' };
399
+ }
400
+ if (options.completedDateTime !== undefined) {
401
+ payload.completedDateTime =
402
+ options.completedDateTime === null
403
+ ? null
404
+ : { dateTime: options.completedDateTime, timeZone: options.timeZone || 'UTC' };
405
+ }
406
+ if (options.linkedResources !== undefined) {
407
+ payload.linkedResources = options.linkedResources.map((lr) => linkedResourceToGraphPayload(lr));
408
+ }
409
+ if (options.categories !== undefined) payload.categories = options.categories;
410
+ else if (options.clearCategories) payload.categories = [];
411
+ if (options.recurrence !== undefined) payload.recurrence = options.recurrence;
412
+ let result: GraphResponse<TodoTask>;
413
+ try {
414
+ result = await callGraph<TodoTask>(
415
+ token,
416
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}`,
417
+ { method: 'PATCH', body: JSON.stringify(payload) }
418
+ );
419
+ } catch (err) {
420
+ if (err instanceof GraphApiError) {
421
+ return graphError(err.message, err.code, err.status);
422
+ }
423
+ return graphError(err instanceof Error ? err.message : 'Failed to update task');
424
+ }
425
+ if (!result.ok || !result.data) {
426
+ return graphError(result.error?.message || 'Failed to update task', result.error?.code, result.error?.status);
427
+ }
428
+ return graphResult(result.data);
429
+ }
430
+
431
+ export async function deleteTask(
432
+ token: string,
433
+ listId: string,
434
+ taskId: string,
435
+ user?: string
436
+ ): Promise<GraphResponse<void>> {
437
+ try {
438
+ return await callGraph<void>(
439
+ token,
440
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}`,
441
+ { method: 'DELETE' },
442
+ false
443
+ );
444
+ } catch (err) {
445
+ if (err instanceof GraphApiError) {
446
+ return graphError(err.message, err.code, err.status);
447
+ }
448
+ return graphError(err instanceof Error ? err.message : 'Failed to delete task');
449
+ }
450
+ }
451
+
452
+ export async function addChecklistItem(
453
+ token: string,
454
+ listId: string,
455
+ taskId: string,
456
+ displayName: string,
457
+ user?: string
458
+ ): Promise<GraphResponse<TodoChecklistItem>> {
459
+ let result: GraphResponse<TodoChecklistItem>;
460
+ try {
461
+ result = await callGraph<TodoChecklistItem>(
462
+ token,
463
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems`,
464
+ { method: 'POST', body: JSON.stringify({ displayName }) }
465
+ );
466
+ } catch (err) {
467
+ if (err instanceof GraphApiError) {
468
+ return graphError(err.message, err.code, err.status);
469
+ }
470
+ return graphError(err instanceof Error ? err.message : 'Failed to add checklist item');
471
+ }
472
+ if (!result.ok || !result.data) {
473
+ return graphError(
474
+ result.error?.message || 'Failed to add checklist item',
475
+ result.error?.code,
476
+ result.error?.status
477
+ );
478
+ }
479
+ return graphResult(result.data);
480
+ }
481
+
482
+ export async function deleteChecklistItem(
483
+ token: string,
484
+ listId: string,
485
+ taskId: string,
486
+ checklistItemId: string,
487
+ user?: string
488
+ ): Promise<GraphResponse<void>> {
489
+ try {
490
+ return await callGraph<void>(
491
+ token,
492
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems/${encodeURIComponent(checklistItemId)}`,
493
+ { method: 'DELETE' },
494
+ false
495
+ );
496
+ } catch (err) {
497
+ if (err instanceof GraphApiError) {
498
+ return graphError(err.message, err.code, err.status);
499
+ }
500
+ return graphError(err instanceof Error ? err.message : 'Failed to delete checklist item');
501
+ }
502
+ }
503
+
504
+ export async function updateChecklistItem(
505
+ token: string,
506
+ listId: string,
507
+ taskId: string,
508
+ checklistItemId: string,
509
+ patch: { displayName?: string; isChecked?: boolean },
510
+ user?: string
511
+ ): Promise<GraphResponse<TodoChecklistItem>> {
512
+ let result: GraphResponse<TodoChecklistItem>;
513
+ try {
514
+ result = await callGraph<TodoChecklistItem>(
515
+ token,
516
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems/${encodeURIComponent(checklistItemId)}`,
517
+ { method: 'PATCH', body: JSON.stringify(patch) }
518
+ );
519
+ } catch (err) {
520
+ if (err instanceof GraphApiError) {
521
+ return graphError(err.message, err.code, err.status);
522
+ }
523
+ return graphError(err instanceof Error ? err.message : 'Failed to update checklist item');
524
+ }
525
+ if (!result.ok || !result.data) {
526
+ return graphError(
527
+ result.error?.message || 'Failed to update checklist item',
528
+ result.error?.code,
529
+ result.error?.status
530
+ );
531
+ }
532
+ return graphResult(result.data);
533
+ }
534
+
535
+ export async function listAttachments(
536
+ token: string,
537
+ listId: string,
538
+ taskId: string,
539
+ user?: string
540
+ ): Promise<GraphResponse<TodoAttachment[]>> {
541
+ return fetchAllPages<TodoAttachment>(
542
+ token,
543
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/attachments`,
544
+ 'Failed to list attachments'
545
+ );
546
+ }
547
+
548
+ export async function createTaskFileAttachment(
549
+ token: string,
550
+ listId: string,
551
+ taskId: string,
552
+ name: string,
553
+ contentBytesBase64: string,
554
+ contentType: string,
555
+ user?: string
556
+ ): Promise<GraphResponse<TodoAttachment>> {
557
+ const body = {
558
+ '@odata.type': '#microsoft.graph.taskFileAttachment',
559
+ name,
560
+ contentBytes: contentBytesBase64,
561
+ contentType
562
+ };
563
+ let result: GraphResponse<TodoAttachment>;
564
+ try {
565
+ result = await callGraph<TodoAttachment>(
566
+ token,
567
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/attachments`,
568
+ { method: 'POST', body: JSON.stringify(body) }
569
+ );
570
+ } catch (err) {
571
+ if (err instanceof GraphApiError) {
572
+ return graphError(err.message, err.code, err.status);
573
+ }
574
+ return graphError(err instanceof Error ? err.message : 'Failed to add attachment');
575
+ }
576
+ if (!result.ok || !result.data) {
577
+ return graphError(result.error?.message || 'Failed to add attachment', result.error?.code, result.error?.status);
578
+ }
579
+ return graphResult(result.data);
580
+ }
581
+
582
+ export async function deleteAttachment(
583
+ token: string,
584
+ listId: string,
585
+ taskId: string,
586
+ attachmentId: string,
587
+ user?: string
588
+ ): Promise<GraphResponse<void>> {
589
+ try {
590
+ return await callGraph<void>(
591
+ token,
592
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/attachments/${encodeURIComponent(attachmentId)}`,
593
+ { method: 'DELETE' },
594
+ false
595
+ );
596
+ } catch (err) {
597
+ if (err instanceof GraphApiError) {
598
+ return graphError(err.message, err.code, err.status);
599
+ }
600
+ return graphError(err instanceof Error ? err.message : 'Failed to delete attachment');
601
+ }
602
+ }
603
+
604
+ export async function getTaskAttachment(
605
+ token: string,
606
+ listId: string,
607
+ taskId: string,
608
+ attachmentId: string,
609
+ user?: string
610
+ ): Promise<GraphResponse<TodoAttachment>> {
611
+ try {
612
+ const result = await callGraph<TodoAttachment>(
613
+ token,
614
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/attachments/${encodeURIComponent(attachmentId)}`
615
+ );
616
+ if (!result.ok || !result.data) {
617
+ return graphError(result.error?.message || 'Failed to get attachment', result.error?.code, result.error?.status);
618
+ }
619
+ return graphResult(result.data);
620
+ } catch (err) {
621
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
622
+ return graphError(err instanceof Error ? err.message : 'Failed to get attachment');
623
+ }
624
+ }
625
+
626
+ /** Link attachment (URL reference), not file bytes. */
627
+ export async function createTaskReferenceAttachment(
628
+ token: string,
629
+ listId: string,
630
+ taskId: string,
631
+ name: string,
632
+ url: string,
633
+ user?: string
634
+ ): Promise<GraphResponse<TodoAttachment>> {
635
+ const body = {
636
+ '@odata.type': '#microsoft.graph.taskReferenceAttachment',
637
+ name,
638
+ url
639
+ };
640
+ let result: GraphResponse<TodoAttachment>;
641
+ try {
642
+ result = await callGraph<TodoAttachment>(
643
+ token,
644
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/attachments`,
645
+ { method: 'POST', body: JSON.stringify(body) }
646
+ );
647
+ } catch (err) {
648
+ if (err instanceof GraphApiError) {
649
+ return graphError(err.message, err.code, err.status);
650
+ }
651
+ return graphError(err instanceof Error ? err.message : 'Failed to add reference attachment');
652
+ }
653
+ if (!result.ok || !result.data) {
654
+ return graphError(
655
+ result.error?.message || 'Failed to add reference attachment',
656
+ result.error?.code,
657
+ result.error?.status
658
+ );
659
+ }
660
+ return graphResult(result.data);
661
+ }
662
+
663
+ export async function addLinkedResource(
664
+ token: string,
665
+ listId: string,
666
+ taskId: string,
667
+ resource: TodoLinkedResource,
668
+ user?: string
669
+ ): Promise<GraphResponse<TodoTask>> {
670
+ const tr = await getTask(token, listId, taskId, user);
671
+ if (!tr.ok || !tr.data) {
672
+ return graphError(tr.error?.message || 'Failed to get task', tr.error?.code, tr.error?.status);
673
+ }
674
+ const existing = (tr.data.linkedResources || []) as TodoLinkedResource[];
675
+ const merged = [...existing, resource];
676
+ return updateTask(token, listId, taskId, { linkedResources: merged }, user);
677
+ }
678
+
679
+ export async function removeLinkedResourceByWebUrl(
680
+ token: string,
681
+ listId: string,
682
+ taskId: string,
683
+ webUrl: string,
684
+ user?: string
685
+ ): Promise<GraphResponse<TodoTask>> {
686
+ const tr = await getTask(token, listId, taskId, user);
687
+ if (!tr.ok || !tr.data) {
688
+ return graphError(tr.error?.message || 'Failed to get task', tr.error?.code, tr.error?.status);
689
+ }
690
+ const merged = (tr.data.linkedResources || []).filter((r) => r.webUrl !== webUrl);
691
+ return updateTask(token, listId, taskId, { linkedResources: merged as TodoLinkedResource[] }, user);
692
+ }
693
+
694
+ function linkedResourcesCollectionPath(
695
+ listId: string,
696
+ taskId: string,
697
+ user: string | undefined,
698
+ linkedResourceId?: string
699
+ ): string {
700
+ const b = `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/linkedResources`;
701
+ return linkedResourceId ? `${b}/${encodeURIComponent(linkedResourceId)}` : b;
702
+ }
703
+
704
+ /** List linked resources via the task navigation (same data as `linkedResources` on todoTask; supports paging). */
705
+ export async function listTaskLinkedResources(
706
+ token: string,
707
+ listId: string,
708
+ taskId: string,
709
+ user?: string
710
+ ): Promise<GraphResponse<TodoLinkedResource[]>> {
711
+ return fetchAllPages<TodoLinkedResource>(
712
+ token,
713
+ linkedResourcesCollectionPath(listId, taskId, user),
714
+ 'Failed to list linked resources'
715
+ );
716
+ }
717
+
718
+ export async function createTaskLinkedResource(
719
+ token: string,
720
+ listId: string,
721
+ taskId: string,
722
+ resource: TodoLinkedResource,
723
+ user?: string
724
+ ): Promise<GraphResponse<TodoLinkedResource>> {
725
+ let result: GraphResponse<TodoLinkedResource>;
726
+ try {
727
+ result = await callGraph<TodoLinkedResource>(token, linkedResourcesCollectionPath(listId, taskId, user), {
728
+ method: 'POST',
729
+ body: JSON.stringify(linkedResourceToGraphPayload(resource))
730
+ });
731
+ } catch (err) {
732
+ if (err instanceof GraphApiError) {
733
+ return graphError(err.message, err.code, err.status);
734
+ }
735
+ return graphError(err instanceof Error ? err.message : 'Failed to create linked resource');
736
+ }
737
+ if (!result.ok || !result.data) {
738
+ return graphError(
739
+ result.error?.message || 'Failed to create linked resource',
740
+ result.error?.code,
741
+ result.error?.status
742
+ );
743
+ }
744
+ return graphResult(result.data);
745
+ }
746
+
747
+ export async function getTaskLinkedResource(
748
+ token: string,
749
+ listId: string,
750
+ taskId: string,
751
+ linkedResourceId: string,
752
+ user?: string
753
+ ): Promise<GraphResponse<TodoLinkedResource>> {
754
+ try {
755
+ const result = await callGraph<TodoLinkedResource>(
756
+ token,
757
+ linkedResourcesCollectionPath(listId, taskId, user, linkedResourceId)
758
+ );
759
+ if (!result.ok || !result.data) {
760
+ return graphError(
761
+ result.error?.message || 'Failed to get linked resource',
762
+ result.error?.code,
763
+ result.error?.status
764
+ );
765
+ }
766
+ return graphResult(result.data);
767
+ } catch (err) {
768
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
769
+ return graphError(err instanceof Error ? err.message : 'Failed to get linked resource');
770
+ }
771
+ }
772
+
773
+ export async function updateTaskLinkedResource(
774
+ token: string,
775
+ listId: string,
776
+ taskId: string,
777
+ linkedResourceId: string,
778
+ patch: Partial<Pick<TodoLinkedResource, 'webUrl' | 'displayName' | 'description' | 'applicationName' | 'externalId'>>,
779
+ user?: string
780
+ ): Promise<GraphResponse<TodoLinkedResource>> {
781
+ const body: Record<string, unknown> = {};
782
+ if (patch.webUrl !== undefined) body.webUrl = patch.webUrl;
783
+ if (patch.applicationName !== undefined) body.applicationName = patch.applicationName;
784
+ if (patch.externalId !== undefined) body.externalId = patch.externalId;
785
+ const displayName = patch.displayName ?? patch.description;
786
+ if (displayName !== undefined) body.displayName = displayName;
787
+ body['@odata.type'] = '#microsoft.graph.linkedResource';
788
+ let result: GraphResponse<TodoLinkedResource>;
789
+ try {
790
+ result = await callGraph<TodoLinkedResource>(
791
+ token,
792
+ linkedResourcesCollectionPath(listId, taskId, user, linkedResourceId),
793
+ { method: 'PATCH', body: JSON.stringify(body) }
794
+ );
795
+ } catch (err) {
796
+ if (err instanceof GraphApiError) {
797
+ return graphError(err.message, err.code, err.status);
798
+ }
799
+ return graphError(err instanceof Error ? err.message : 'Failed to update linked resource');
800
+ }
801
+ if (!result.ok || !result.data) {
802
+ return graphError(
803
+ result.error?.message || 'Failed to update linked resource',
804
+ result.error?.code,
805
+ result.error?.status
806
+ );
807
+ }
808
+ return graphResult(result.data);
809
+ }
810
+
811
+ export async function deleteTaskLinkedResource(
812
+ token: string,
813
+ listId: string,
814
+ taskId: string,
815
+ linkedResourceId: string,
816
+ user?: string
817
+ ): Promise<GraphResponse<void>> {
818
+ try {
819
+ return await callGraph<void>(
820
+ token,
821
+ linkedResourcesCollectionPath(listId, taskId, user, linkedResourceId),
822
+ { method: 'DELETE' },
823
+ false
824
+ );
825
+ } catch (err) {
826
+ if (err instanceof GraphApiError) {
827
+ return graphError(err.message, err.code, err.status);
828
+ }
829
+ return graphError(err instanceof Error ? err.message : 'Failed to delete linked resource');
830
+ }
831
+ }
832
+
833
+ export async function listTaskChecklistItems(
834
+ token: string,
835
+ listId: string,
836
+ taskId: string,
837
+ user?: string
838
+ ): Promise<GraphResponse<TodoChecklistItem[]>> {
839
+ return fetchAllPages<TodoChecklistItem>(
840
+ token,
841
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems`,
842
+ 'Failed to list checklist items'
843
+ );
844
+ }
845
+
846
+ /** `GET .../tasks/{taskId}/checklistItems/{checklistItemId}` (see Graph checklistItem). */
847
+ export async function getChecklistItem(
848
+ token: string,
849
+ listId: string,
850
+ taskId: string,
851
+ checklistItemId: string,
852
+ user?: string
853
+ ): Promise<GraphResponse<TodoChecklistItem>> {
854
+ try {
855
+ const result = await callGraph<TodoChecklistItem>(
856
+ token,
857
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/checklistItems/${encodeURIComponent(checklistItemId)}`
858
+ );
859
+ if (!result.ok || !result.data) {
860
+ return graphError(
861
+ result.error?.message || 'Failed to get checklist item',
862
+ result.error?.code,
863
+ result.error?.status
864
+ );
865
+ }
866
+ return graphResult(result.data);
867
+ } catch (err) {
868
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
869
+ return graphError(err instanceof Error ? err.message : 'Failed to get checklist item');
870
+ }
871
+ }
872
+
873
+ /**
874
+ * Raw file bytes for a task file attachment (`GET .../attachments/{id}/$value`).
875
+ * Reference attachments do not support this; use metadata from {@link getTaskAttachment} instead.
876
+ */
877
+ export async function getTaskAttachmentContent(
878
+ token: string,
879
+ listId: string,
880
+ taskId: string,
881
+ attachmentId: string,
882
+ user?: string
883
+ ): Promise<GraphResponse<Uint8Array>> {
884
+ const path = `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/attachments/${encodeURIComponent(attachmentId)}/$value`;
885
+ try {
886
+ const res = await fetchGraphRaw(token, path);
887
+ const buf = new Uint8Array(await res.arrayBuffer());
888
+ if (!res.ok) {
889
+ try {
890
+ const text = new TextDecoder().decode(buf);
891
+ const json = JSON.parse(text) as { error?: { code?: string; message?: string } };
892
+ return graphError(json.error?.message || `HTTP ${res.status}`, json.error?.code, res.status);
893
+ } catch {
894
+ return graphError(`Failed to download attachment: HTTP ${res.status}`, undefined, res.status);
895
+ }
896
+ }
897
+ return graphResult(buf);
898
+ } catch (err) {
899
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
900
+ return graphError(err instanceof Error ? err.message : 'Failed to download attachment');
901
+ }
902
+ }
903
+
904
+ function listListExtensionsPath(listId: string, user: string | undefined, extensionName?: string): string {
905
+ const base = `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/extensions`;
906
+ return extensionName ? `${base}/${encodeURIComponent(extensionName)}` : base;
907
+ }
908
+
909
+ export async function listTodoListOpenExtensions(
910
+ token: string,
911
+ listId: string,
912
+ user?: string
913
+ ): Promise<GraphResponse<Array<Record<string, unknown>>>> {
914
+ try {
915
+ const result = await callGraph<{ value: Array<Record<string, unknown>> }>(
916
+ token,
917
+ listListExtensionsPath(listId, user)
918
+ );
919
+ if (!result.ok || !result.data) {
920
+ return graphError(
921
+ result.error?.message || 'Failed to list list extensions',
922
+ result.error?.code,
923
+ result.error?.status
924
+ );
925
+ }
926
+ return graphResult(result.data.value || []);
927
+ } catch (err) {
928
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
929
+ return graphError(err instanceof Error ? err.message : 'Failed to list list extensions');
930
+ }
931
+ }
932
+
933
+ export async function getTodoListOpenExtension(
934
+ token: string,
935
+ listId: string,
936
+ extensionName: string,
937
+ user?: string
938
+ ): Promise<GraphResponse<Record<string, unknown>>> {
939
+ try {
940
+ const result = await callGraph<Record<string, unknown>>(token, listListExtensionsPath(listId, user, extensionName));
941
+ if (!result.ok || !result.data) {
942
+ return graphError(
943
+ result.error?.message || 'Failed to get list extension',
944
+ result.error?.code,
945
+ result.error?.status
946
+ );
947
+ }
948
+ return graphResult(result.data);
949
+ } catch (err) {
950
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
951
+ return graphError(err instanceof Error ? err.message : 'Failed to get list extension');
952
+ }
953
+ }
954
+
955
+ export async function setTodoListOpenExtension(
956
+ token: string,
957
+ listId: string,
958
+ extensionName: string,
959
+ extensionData: Record<string, unknown>,
960
+ user?: string
961
+ ): Promise<GraphResponse<Record<string, unknown>>> {
962
+ const body = {
963
+ '@odata.type': 'microsoft.graph.openTypeExtension',
964
+ extensionName,
965
+ ...extensionData
966
+ };
967
+ let result: GraphResponse<Record<string, unknown>>;
968
+ try {
969
+ result = await callGraph<Record<string, unknown>>(token, listListExtensionsPath(listId, user), {
970
+ method: 'POST',
971
+ body: JSON.stringify(body)
972
+ });
973
+ } catch (err) {
974
+ if (err instanceof GraphApiError) {
975
+ return graphError(err.message, err.code, err.status);
976
+ }
977
+ return graphError(err instanceof Error ? err.message : 'Failed to set list extension');
978
+ }
979
+ if (!result.ok || !result.data) {
980
+ return graphError(
981
+ result.error?.message || 'Failed to set list extension',
982
+ result.error?.code,
983
+ result.error?.status
984
+ );
985
+ }
986
+ return graphResult(result.data);
987
+ }
988
+
989
+ export async function updateTodoListOpenExtension(
990
+ token: string,
991
+ listId: string,
992
+ extensionName: string,
993
+ patch: Record<string, unknown>,
994
+ user?: string
995
+ ): Promise<GraphResponse<void>> {
996
+ try {
997
+ const result = await callGraph<void>(
998
+ token,
999
+ listListExtensionsPath(listId, user, extensionName),
1000
+ {
1001
+ method: 'PATCH',
1002
+ body: JSON.stringify(patch)
1003
+ },
1004
+ false
1005
+ );
1006
+ if (!result.ok) {
1007
+ return graphError(
1008
+ result.error?.message || 'Failed to update list extension',
1009
+ result.error?.code,
1010
+ result.error?.status
1011
+ );
1012
+ }
1013
+ return graphResult(undefined as undefined);
1014
+ } catch (err) {
1015
+ if (err instanceof GraphApiError) {
1016
+ return graphError(err.message, err.code, err.status);
1017
+ }
1018
+ return graphError(err instanceof Error ? err.message : 'Failed to update list extension');
1019
+ }
1020
+ }
1021
+
1022
+ export async function deleteTodoListOpenExtension(
1023
+ token: string,
1024
+ listId: string,
1025
+ extensionName: string,
1026
+ user?: string
1027
+ ): Promise<GraphResponse<void>> {
1028
+ try {
1029
+ return await callGraph<void>(
1030
+ token,
1031
+ listListExtensionsPath(listId, user, extensionName),
1032
+ { method: 'DELETE' },
1033
+ false
1034
+ );
1035
+ } catch (err) {
1036
+ if (err instanceof GraphApiError) {
1037
+ return graphError(err.message, err.code, err.status);
1038
+ }
1039
+ return graphError(err instanceof Error ? err.message : 'Failed to delete list extension');
1040
+ }
1041
+ }
1042
+
1043
+ export interface TodoTaskDeltaPage {
1044
+ value?: TodoTask[];
1045
+ '@odata.nextLink'?: string;
1046
+ '@odata.deltaLink'?: string;
1047
+ }
1048
+
1049
+ export async function getTodoTasksDeltaPage(
1050
+ token: string,
1051
+ listId: string,
1052
+ fullUrl?: string,
1053
+ user?: string
1054
+ ): Promise<GraphResponse<TodoTaskDeltaPage>> {
1055
+ try {
1056
+ if (fullUrl) {
1057
+ return await callGraphAbsolute<TodoTaskDeltaPage>(token, fullUrl);
1058
+ }
1059
+ return await callGraph<TodoTaskDeltaPage>(
1060
+ token,
1061
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/delta`
1062
+ );
1063
+ } catch (err) {
1064
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
1065
+ return graphError(err instanceof Error ? err.message : 'Failed to get todo delta');
1066
+ }
1067
+ }
1068
+
1069
+ export interface UploadSessionResult {
1070
+ uploadUrl: string;
1071
+ expirationDateTime?: string;
1072
+ nextExpectedRanges?: string[];
1073
+ }
1074
+
1075
+ async function createTaskAttachmentUploadSession(
1076
+ token: string,
1077
+ listId: string,
1078
+ taskId: string,
1079
+ attachmentName: string,
1080
+ size: number,
1081
+ user?: string
1082
+ ): Promise<GraphResponse<UploadSessionResult>> {
1083
+ try {
1084
+ const result = await callGraph<UploadSessionResult>(
1085
+ token,
1086
+ `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/attachments/createUploadSession`,
1087
+ {
1088
+ method: 'POST',
1089
+ body: JSON.stringify({
1090
+ attachmentInfo: {
1091
+ attachmentType: 'file',
1092
+ name: attachmentName,
1093
+ size
1094
+ }
1095
+ })
1096
+ }
1097
+ );
1098
+ if (!result.ok || !result.data) {
1099
+ return graphError(
1100
+ result.error?.message || 'Failed to create upload session',
1101
+ result.error?.code,
1102
+ result.error?.status
1103
+ );
1104
+ }
1105
+ return graphResult(result.data);
1106
+ } catch (err) {
1107
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
1108
+ return graphError(err instanceof Error ? err.message : 'Failed to create upload session');
1109
+ }
1110
+ }
1111
+
1112
+ /**
1113
+ * Upload file bytes via session (no Bearer on PUT; Graph upload URL is pre-authorized).
1114
+ * Returns the final attachment object from the last chunk response when JSON.
1115
+ */
1116
+ async function uploadFileViaTodoAttachmentSession(
1117
+ uploadUrl: string,
1118
+ filePath: string,
1119
+ chunkSize = 4 * 1024 * 1024
1120
+ ): Promise<GraphResponse<TodoAttachment>> {
1121
+ const buf = await readFile(filePath);
1122
+ const total = buf.byteLength;
1123
+ let start = 0;
1124
+ let lastJson: TodoAttachment | undefined;
1125
+ while (start < total) {
1126
+ const end = Math.min(start + chunkSize, total);
1127
+ const slice = buf.subarray(start, end);
1128
+ const contentRange = `bytes ${start}-${end - 1}/${total}`;
1129
+ let response: Response;
1130
+ try {
1131
+ // codeql[js/file-access-to-http]: chunked upload of a user-selected attachment file to Graph (pre-authorized uploadUrl).
1132
+ response = await fetch(uploadUrl, {
1133
+ method: 'PUT',
1134
+ headers: {
1135
+ 'Content-Length': String(slice.byteLength),
1136
+ 'Content-Range': contentRange
1137
+ },
1138
+ body: slice
1139
+ });
1140
+ } catch (err) {
1141
+ return graphError(err instanceof Error ? err.message : 'Upload chunk failed');
1142
+ }
1143
+ const text = await response.text();
1144
+ if (!response.ok) {
1145
+ return graphError(text || `Upload failed: HTTP ${response.status}`, undefined, response.status);
1146
+ }
1147
+ if (text) {
1148
+ try {
1149
+ const parsed = JSON.parse(text) as TodoAttachment & { value?: unknown };
1150
+ if (parsed.id) lastJson = parsed;
1151
+ } catch {
1152
+ // non-JSON success body
1153
+ }
1154
+ }
1155
+ start = end;
1156
+ }
1157
+ if (lastJson) return graphResult(lastJson);
1158
+ return graphError('Upload completed but attachment body was not returned', 'UPLOAD_PARSE', 500);
1159
+ }
1160
+
1161
+ export async function uploadLargeFileAttachment(
1162
+ token: string,
1163
+ listId: string,
1164
+ taskId: string,
1165
+ filePath: string,
1166
+ attachmentName?: string,
1167
+ user?: string
1168
+ ): Promise<GraphResponse<TodoAttachment>> {
1169
+ const name = attachmentName?.trim() || basename(filePath);
1170
+ const st = await stat(filePath);
1171
+ if (!st.isFile()) return graphError(`Not a file: ${filePath}`, 'NOT_FILE', 400);
1172
+ const session = await createTaskAttachmentUploadSession(token, listId, taskId, name, st.size, user);
1173
+ if (!session.ok || !session.data?.uploadUrl) {
1174
+ return graphError(session.error?.message || 'No upload session', session.error?.code, session.error?.status);
1175
+ }
1176
+ return uploadFileViaTodoAttachmentSession(session.data.uploadUrl, filePath);
1177
+ }
1178
+
1179
+ function extensionsPath(listId: string, taskId: string, user: string | undefined, extensionName?: string): string {
1180
+ const base = `${todoRoot(user)}/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}/extensions`;
1181
+ return extensionName ? `${base}/${encodeURIComponent(extensionName)}` : base;
1182
+ }
1183
+
1184
+ export async function listTaskOpenExtensions(
1185
+ token: string,
1186
+ listId: string,
1187
+ taskId: string,
1188
+ user?: string
1189
+ ): Promise<GraphResponse<Array<Record<string, unknown>>>> {
1190
+ try {
1191
+ const result = await callGraph<{ value: Array<Record<string, unknown>> }>(
1192
+ token,
1193
+ extensionsPath(listId, taskId, user)
1194
+ );
1195
+ if (!result.ok || !result.data) {
1196
+ return graphError(result.error?.message || 'Failed to list extensions', result.error?.code, result.error?.status);
1197
+ }
1198
+ return graphResult(result.data.value || []);
1199
+ } catch (err) {
1200
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
1201
+ return graphError(err instanceof Error ? err.message : 'Failed to list extensions');
1202
+ }
1203
+ }
1204
+
1205
+ export async function getTaskOpenExtension(
1206
+ token: string,
1207
+ listId: string,
1208
+ taskId: string,
1209
+ extensionName: string,
1210
+ user?: string
1211
+ ): Promise<GraphResponse<Record<string, unknown>>> {
1212
+ try {
1213
+ const result = await callGraph<Record<string, unknown>>(token, extensionsPath(listId, taskId, user, extensionName));
1214
+ if (!result.ok || !result.data) {
1215
+ return graphError(result.error?.message || 'Failed to get extension', result.error?.code, result.error?.status);
1216
+ }
1217
+ return graphResult(result.data);
1218
+ } catch (err) {
1219
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
1220
+ return graphError(err instanceof Error ? err.message : 'Failed to get extension');
1221
+ }
1222
+ }
1223
+
1224
+ export async function setTaskOpenExtension(
1225
+ token: string,
1226
+ listId: string,
1227
+ taskId: string,
1228
+ extensionName: string,
1229
+ extensionData: Record<string, unknown>,
1230
+ user?: string
1231
+ ): Promise<GraphResponse<Record<string, unknown>>> {
1232
+ const body = {
1233
+ '@odata.type': 'microsoft.graph.openTypeExtension',
1234
+ extensionName,
1235
+ ...extensionData
1236
+ };
1237
+ let result: GraphResponse<Record<string, unknown>>;
1238
+ try {
1239
+ result = await callGraph<Record<string, unknown>>(token, extensionsPath(listId, taskId, user), {
1240
+ method: 'POST',
1241
+ body: JSON.stringify(body)
1242
+ });
1243
+ } catch (err) {
1244
+ if (err instanceof GraphApiError) {
1245
+ return graphError(err.message, err.code, err.status);
1246
+ }
1247
+ return graphError(err instanceof Error ? err.message : 'Failed to set extension');
1248
+ }
1249
+ if (!result.ok || !result.data) {
1250
+ return graphError(result.error?.message || 'Failed to set extension', result.error?.code, result.error?.status);
1251
+ }
1252
+ return graphResult(result.data);
1253
+ }
1254
+
1255
+ export async function updateTaskOpenExtension(
1256
+ token: string,
1257
+ listId: string,
1258
+ taskId: string,
1259
+ extensionName: string,
1260
+ patch: Record<string, unknown>,
1261
+ user?: string
1262
+ ): Promise<GraphResponse<void>> {
1263
+ try {
1264
+ const result = await callGraph<void>(
1265
+ token,
1266
+ extensionsPath(listId, taskId, user, extensionName),
1267
+ {
1268
+ method: 'PATCH',
1269
+ body: JSON.stringify(patch)
1270
+ },
1271
+ false
1272
+ );
1273
+ if (!result.ok) {
1274
+ return graphError(
1275
+ result.error?.message || 'Failed to update extension',
1276
+ result.error?.code,
1277
+ result.error?.status
1278
+ );
1279
+ }
1280
+ return graphResult(undefined as undefined);
1281
+ } catch (err) {
1282
+ if (err instanceof GraphApiError) {
1283
+ return graphError(err.message, err.code, err.status);
1284
+ }
1285
+ return graphError(err instanceof Error ? err.message : 'Failed to update extension');
1286
+ }
1287
+ }
1288
+
1289
+ export async function deleteTaskOpenExtension(
1290
+ token: string,
1291
+ listId: string,
1292
+ taskId: string,
1293
+ extensionName: string,
1294
+ user?: string
1295
+ ): Promise<GraphResponse<void>> {
1296
+ try {
1297
+ return await callGraph<void>(
1298
+ token,
1299
+ extensionsPath(listId, taskId, user, extensionName),
1300
+ { method: 'DELETE' },
1301
+ false
1302
+ );
1303
+ } catch (err) {
1304
+ if (err instanceof GraphApiError) {
1305
+ return graphError(err.message, err.code, err.status);
1306
+ }
1307
+ return graphError(err instanceof Error ? err.message : 'Failed to delete extension');
1308
+ }
1309
+ }