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,1226 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ callGraph,
4
+ callGraphAbsolute,
5
+ callGraphAt,
6
+ fetchAllPages,
7
+ GraphApiError,
8
+ type GraphResponse,
9
+ graphError,
10
+ graphResult
11
+ } from './graph-client.js';
12
+ import { GRAPH_BASE_URL, GRAPH_BETA_URL } from './graph-constants.js';
13
+
14
+ export interface PlannerPlanContainer {
15
+ '@odata.type'?: string;
16
+ url?: string;
17
+ containerId?: string;
18
+ type?: string;
19
+ }
20
+
21
+ export interface PlannerPlan {
22
+ id: string;
23
+ title: string;
24
+ /** @deprecated Prefer `container`; Graph may still return this for group-backed plans. */
25
+ owner?: string;
26
+ container?: PlannerPlanContainer;
27
+ '@odata.etag'?: string;
28
+ }
29
+
30
+ export interface PlannerBucket {
31
+ id: string;
32
+ name: string;
33
+ planId: string;
34
+ orderHint?: string;
35
+ '@odata.etag'?: string;
36
+ }
37
+
38
+ /** Planner label slots (plan defines display names in plan details). */
39
+ export type PlannerCategorySlot = 'category1' | 'category2' | 'category3' | 'category4' | 'category5' | 'category6';
40
+
41
+ export type PlannerAppliedCategories = Partial<Record<PlannerCategorySlot, boolean>>;
42
+
43
+ export interface PlannerTask {
44
+ id: string;
45
+ planId: string;
46
+ bucketId: string;
47
+ title: string;
48
+ orderHint: string;
49
+ assigneePriority: string;
50
+ percentComplete: number;
51
+ hasDescription: boolean;
52
+ createdDateTime: string;
53
+ /** Planner due/start (ISO date-time strings per Graph). */
54
+ dueDateTime?: string;
55
+ startDateTime?: string;
56
+ /** Teams / Outlook thread id when linked. */
57
+ conversationThreadId?: string;
58
+ /** 0 (highest) .. 10 (lowest). */
59
+ priority?: number;
60
+ /** Card preview on task: automatic, noPreview, checklist, description, reference. */
61
+ previewType?: string;
62
+ assignments?: Record<string, any>;
63
+ /** Label slots (boolean per category1..category6); names come from plan details. */
64
+ appliedCategories?: PlannerAppliedCategories;
65
+ '@odata.etag'?: string;
66
+ }
67
+
68
+ /** Checklist entry on Planner task details (keyed by client-generated id in PATCH body). */
69
+ export interface PlannerTaskDetailsChecklistItem {
70
+ '@odata.type'?: string;
71
+ isChecked: boolean;
72
+ title: string;
73
+ orderHint: string;
74
+ lastModifiedDateTime?: string;
75
+ lastModifiedBy?: { user?: { id?: string } };
76
+ }
77
+
78
+ export interface PlannerTaskDetails {
79
+ id: string;
80
+ description?: string;
81
+ checklist?: Record<string, PlannerTaskDetailsChecklistItem>;
82
+ references?: Record<string, unknown>;
83
+ previewType?: string;
84
+ '@odata.etag'?: string;
85
+ }
86
+
87
+ export interface PlannerPlanDetails {
88
+ id: string;
89
+ categoryDescriptions?: Partial<Record<PlannerCategorySlot, string>>;
90
+ sharedWith?: Record<string, boolean>;
91
+ '@odata.etag'?: string;
92
+ }
93
+
94
+ const PLANNER_SLOTS: PlannerCategorySlot[] = [
95
+ 'category1',
96
+ 'category2',
97
+ 'category3',
98
+ 'category4',
99
+ 'category5',
100
+ 'category6'
101
+ ];
102
+
103
+ /** Accept `1`..`6` or `category1`..`category6` (case-insensitive). */
104
+ export function parsePlannerLabelKey(input: string): PlannerCategorySlot | null {
105
+ const t = input.trim().toLowerCase();
106
+ const m = t.match(/^category([1-6])$/);
107
+ if (m) return `category${m[1]}` as PlannerCategorySlot;
108
+ if (/^[1-6]$/.test(t)) return `category${t}` as PlannerCategorySlot;
109
+ return null;
110
+ }
111
+
112
+ /** Build a full slot map for PATCH (Planner expects explicit booleans per slot). */
113
+ export function normalizeAppliedCategories(
114
+ current: PlannerAppliedCategories | undefined,
115
+ patch: { clearAll?: boolean; setTrue?: PlannerCategorySlot[]; setFalse?: PlannerCategorySlot[] }
116
+ ): PlannerAppliedCategories {
117
+ const out: PlannerAppliedCategories = {};
118
+ for (const s of PLANNER_SLOTS) {
119
+ if (patch.clearAll) {
120
+ out[s] = false;
121
+ continue;
122
+ }
123
+ let v = current?.[s] === true;
124
+ for (const u of patch.setTrue ?? []) if (u === s) v = true;
125
+ for (const u of patch.setFalse ?? []) if (u === s) v = false;
126
+ out[s] = v;
127
+ }
128
+ return out;
129
+ }
130
+
131
+ /** Build `assignments` for create/update from user object IDs. */
132
+ export function buildPlannerAssignments(assigneeUserIds: string[]): Record<string, unknown> {
133
+ const out: Record<string, unknown> = {};
134
+ for (const id of assigneeUserIds) {
135
+ out[id] = {
136
+ '@odata.type': '#microsoft.graph.plannerAssignment',
137
+ orderHint: ' !'
138
+ };
139
+ }
140
+ return out;
141
+ }
142
+
143
+ /** Merge assignees: add and remove user IDs from current `assignments` map. */
144
+ export function mergePlannerAssignments(
145
+ current: Record<string, unknown> | undefined,
146
+ addUserIds: string[],
147
+ removeUserIds: string[]
148
+ ): Record<string, unknown> {
149
+ const out: Record<string, unknown> = { ...(current || {}) };
150
+ for (const r of removeUserIds) delete out[r];
151
+ for (const a of addUserIds) {
152
+ out[a] = {
153
+ '@odata.type': '#microsoft.graph.plannerAssignment',
154
+ orderHint: ' !'
155
+ };
156
+ }
157
+ return out;
158
+ }
159
+
160
+ export async function listUserTasks(token: string): Promise<GraphResponse<PlannerTask[]>> {
161
+ return fetchAllPages<PlannerTask>(token, '/me/planner/tasks', 'Failed to list tasks');
162
+ }
163
+
164
+ export async function listUserPlans(token: string): Promise<GraphResponse<PlannerPlan[]>> {
165
+ return fetchAllPages<PlannerPlan>(token, '/me/planner/plans', 'Failed to list plans');
166
+ }
167
+
168
+ /**
169
+ * List Planner tasks for a user (`GET /users/{id}/planner/tasks`).
170
+ * Graph may return 403 for users other than the signed-in user depending on tenant and token.
171
+ */
172
+ export async function listPlannerTasksForUser(token: string, userId: string): Promise<GraphResponse<PlannerTask[]>> {
173
+ return fetchAllPages<PlannerTask>(
174
+ token,
175
+ `/users/${encodeURIComponent(userId)}/planner/tasks`,
176
+ 'Failed to list tasks for user'
177
+ );
178
+ }
179
+
180
+ /**
181
+ * List Planner plans for a user (`GET /users/{id}/planner/plans`).
182
+ * Same permission caveats as {@link listPlannerTasksForUser}.
183
+ */
184
+ export async function listPlannerPlansForUser(token: string, userId: string): Promise<GraphResponse<PlannerPlan[]>> {
185
+ return fetchAllPages<PlannerPlan>(
186
+ token,
187
+ `/users/${encodeURIComponent(userId)}/planner/plans`,
188
+ 'Failed to list plans for user'
189
+ );
190
+ }
191
+
192
+ export async function listGroupPlans(token: string, groupId: string): Promise<GraphResponse<PlannerPlan[]>> {
193
+ return fetchAllPages<PlannerPlan>(
194
+ token,
195
+ `/groups/${encodeURIComponent(groupId)}/planner/plans`,
196
+ 'Failed to list group plans'
197
+ );
198
+ }
199
+
200
+ export async function listPlanBuckets(token: string, planId: string): Promise<GraphResponse<PlannerBucket[]>> {
201
+ return fetchAllPages<PlannerBucket>(
202
+ token,
203
+ `/planner/plans/${encodeURIComponent(planId)}/buckets`,
204
+ 'Failed to list buckets'
205
+ );
206
+ }
207
+
208
+ export async function listPlanTasks(token: string, planId: string): Promise<GraphResponse<PlannerTask[]>> {
209
+ return fetchAllPages<PlannerTask>(
210
+ token,
211
+ `/planner/plans/${encodeURIComponent(planId)}/tasks`,
212
+ 'Failed to list plan tasks'
213
+ );
214
+ }
215
+
216
+ export async function getPlanDetails(token: string, planId: string): Promise<GraphResponse<PlannerPlanDetails>> {
217
+ try {
218
+ const result = await callGraph<PlannerPlanDetails>(token, `/planner/plans/${encodeURIComponent(planId)}/details`);
219
+ if (!result.ok || !result.data) {
220
+ return graphError(
221
+ result.error?.message || 'Failed to get plan details',
222
+ result.error?.code,
223
+ result.error?.status
224
+ );
225
+ }
226
+ return graphResult(result.data);
227
+ } catch (err) {
228
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
229
+ return graphError(err instanceof Error ? err.message : 'Failed to get plan details');
230
+ }
231
+ }
232
+
233
+ export async function getTask(token: string, taskId: string): Promise<GraphResponse<PlannerTask>> {
234
+ try {
235
+ const result = await callGraph<PlannerTask>(token, `/planner/tasks/${encodeURIComponent(taskId)}`);
236
+ if (!result.ok || !result.data) {
237
+ return graphError(result.error?.message || 'Failed to get task', result.error?.code, result.error?.status);
238
+ }
239
+ return graphResult(result.data);
240
+ } catch (err) {
241
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
242
+ return graphError(err instanceof Error ? err.message : 'Failed to get task');
243
+ }
244
+ }
245
+
246
+ export interface CreatePlannerTaskExtras {
247
+ /** ISO date-time after creation (PATCH). */
248
+ dueDateTime?: string | null;
249
+ startDateTime?: string | null;
250
+ conversationThreadId?: string | null;
251
+ orderHint?: string | null;
252
+ assigneePriority?: string | null;
253
+ priority?: number | null;
254
+ previewType?: string | null;
255
+ }
256
+
257
+ export async function createTask(
258
+ token: string,
259
+ planId: string,
260
+ title: string,
261
+ bucketId?: string,
262
+ assignments?: Record<string, any>,
263
+ appliedCategories?: PlannerAppliedCategories,
264
+ extras?: CreatePlannerTaskExtras
265
+ ): Promise<GraphResponse<PlannerTask>> {
266
+ try {
267
+ const body: Record<string, unknown> = { planId, title };
268
+ if (bucketId) body.bucketId = bucketId;
269
+ if (assignments) body.assignments = assignments;
270
+
271
+ const result = await callGraph<PlannerTask>(token, '/planner/tasks', {
272
+ method: 'POST',
273
+ body: JSON.stringify(body)
274
+ });
275
+ if (!result.ok || !result.data) {
276
+ return graphError(result.error?.message || 'Failed to create task', result.error?.code, result.error?.status);
277
+ }
278
+ let task = result.data;
279
+ const etag1 = task['@odata.etag'];
280
+ const patchPayload: Record<string, unknown> = {};
281
+ if (appliedCategories && Object.keys(appliedCategories).length > 0) {
282
+ patchPayload.appliedCategories = appliedCategories;
283
+ }
284
+ if (extras?.dueDateTime !== undefined && extras.dueDateTime !== null) {
285
+ patchPayload.dueDateTime = extras.dueDateTime;
286
+ }
287
+ if (extras?.startDateTime !== undefined && extras.startDateTime !== null) {
288
+ patchPayload.startDateTime = extras.startDateTime;
289
+ }
290
+ if (extras?.conversationThreadId !== undefined && extras.conversationThreadId !== null) {
291
+ patchPayload.conversationThreadId = extras.conversationThreadId;
292
+ }
293
+ if (extras?.orderHint !== undefined && extras.orderHint !== null) {
294
+ patchPayload.orderHint = extras.orderHint;
295
+ }
296
+ if (extras?.assigneePriority !== undefined && extras.assigneePriority !== null) {
297
+ patchPayload.assigneePriority = extras.assigneePriority;
298
+ }
299
+ if (extras?.priority !== undefined && extras.priority !== null) {
300
+ patchPayload.priority = extras.priority;
301
+ }
302
+ if (extras?.previewType !== undefined && extras.previewType !== null) {
303
+ patchPayload.previewType = extras.previewType;
304
+ }
305
+ if (Object.keys(patchPayload).length > 0) {
306
+ if (!etag1) {
307
+ return graphError('Created task missing ETag; cannot set fields', 'MISSING_ETAG', 500);
308
+ }
309
+ const patch = await callGraph<void>(token, `/planner/tasks/${encodeURIComponent(task.id)}`, {
310
+ method: 'PATCH',
311
+ headers: { 'If-Match': etag1 },
312
+ body: JSON.stringify(patchPayload)
313
+ });
314
+ if (!patch.ok) {
315
+ return graphError(patch.error?.message || 'Failed to update new task', patch.error?.code, patch.error?.status);
316
+ }
317
+ const again = await getTask(token, task.id);
318
+ if (again.ok && again.data) task = again.data;
319
+ }
320
+ return graphResult(task);
321
+ } catch (err) {
322
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
323
+ return graphError(err instanceof Error ? err.message : 'Failed to create task');
324
+ }
325
+ }
326
+
327
+ export async function updateTask(
328
+ token: string,
329
+ taskId: string,
330
+ etag: string,
331
+ updates: {
332
+ title?: string;
333
+ bucketId?: string;
334
+ assignments?: Record<string, any> | null;
335
+ percentComplete?: number;
336
+ appliedCategories?: PlannerAppliedCategories;
337
+ dueDateTime?: string | null;
338
+ startDateTime?: string | null;
339
+ orderHint?: string | null;
340
+ conversationThreadId?: string | null;
341
+ assigneePriority?: string | null;
342
+ priority?: number | null;
343
+ previewType?: string | null;
344
+ }
345
+ ): Promise<GraphResponse<void>> {
346
+ try {
347
+ const result = await callGraph<void>(token, `/planner/tasks/${encodeURIComponent(taskId)}`, {
348
+ method: 'PATCH',
349
+ headers: {
350
+ 'If-Match': etag
351
+ },
352
+ body: JSON.stringify(updates)
353
+ });
354
+ if (!result.ok) {
355
+ return graphError(result.error?.message || 'Failed to update task', result.error?.code, result.error?.status);
356
+ }
357
+ return graphResult(undefined as undefined);
358
+ } catch (err) {
359
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
360
+ return graphError(err instanceof Error ? err.message : 'Failed to update task');
361
+ }
362
+ }
363
+
364
+ export async function getPlannerPlan(token: string, planId: string): Promise<GraphResponse<PlannerPlan>> {
365
+ try {
366
+ const result = await callGraph<PlannerPlan>(token, `/planner/plans/${encodeURIComponent(planId)}`);
367
+ if (!result.ok || !result.data) {
368
+ return graphError(result.error?.message || 'Failed to get plan', result.error?.code, result.error?.status);
369
+ }
370
+ return graphResult(result.data);
371
+ } catch (err) {
372
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
373
+ return graphError(err instanceof Error ? err.message : 'Failed to get plan');
374
+ }
375
+ }
376
+
377
+ export async function createPlannerPlan(
378
+ token: string,
379
+ ownerGroupId: string,
380
+ title: string
381
+ ): Promise<GraphResponse<PlannerPlan>> {
382
+ try {
383
+ const base = GRAPH_BASE_URL.replace(/\/$/, '');
384
+ const result = await callGraph<PlannerPlan>(token, '/planner/plans', {
385
+ method: 'POST',
386
+ body: JSON.stringify({
387
+ title,
388
+ container: {
389
+ url: `${base}/groups/${encodeURIComponent(ownerGroupId)}`
390
+ }
391
+ })
392
+ });
393
+ if (!result.ok || !result.data) {
394
+ return graphError(result.error?.message || 'Failed to create plan', result.error?.code, result.error?.status);
395
+ }
396
+ return graphResult(result.data);
397
+ } catch (err) {
398
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
399
+ return graphError(err instanceof Error ? err.message : 'Failed to create plan');
400
+ }
401
+ }
402
+
403
+ export async function updatePlannerPlan(
404
+ token: string,
405
+ planId: string,
406
+ etag: string,
407
+ title: string
408
+ ): Promise<GraphResponse<void>> {
409
+ try {
410
+ const result = await callGraph<void>(token, `/planner/plans/${encodeURIComponent(planId)}`, {
411
+ method: 'PATCH',
412
+ headers: { 'If-Match': etag },
413
+ body: JSON.stringify({ title })
414
+ });
415
+ if (!result.ok) {
416
+ return graphError(result.error?.message || 'Failed to update plan', result.error?.code, result.error?.status);
417
+ }
418
+ return graphResult(undefined as undefined);
419
+ } catch (err) {
420
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
421
+ return graphError(err instanceof Error ? err.message : 'Failed to update plan');
422
+ }
423
+ }
424
+
425
+ export async function deletePlannerPlan(token: string, planId: string, etag: string): Promise<GraphResponse<void>> {
426
+ try {
427
+ const result = await callGraph<void>(
428
+ token,
429
+ `/planner/plans/${encodeURIComponent(planId)}`,
430
+ {
431
+ method: 'DELETE',
432
+ headers: { 'If-Match': etag }
433
+ },
434
+ false
435
+ );
436
+ if (!result.ok) {
437
+ return graphError(result.error?.message || 'Failed to delete plan', result.error?.code, result.error?.status);
438
+ }
439
+ return graphResult(undefined as undefined);
440
+ } catch (err) {
441
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
442
+ return graphError(err instanceof Error ? err.message : 'Failed to delete plan');
443
+ }
444
+ }
445
+
446
+ export async function getPlannerBucket(token: string, bucketId: string): Promise<GraphResponse<PlannerBucket>> {
447
+ try {
448
+ const result = await callGraph<PlannerBucket>(token, `/planner/buckets/${encodeURIComponent(bucketId)}`);
449
+ if (!result.ok || !result.data) {
450
+ return graphError(result.error?.message || 'Failed to get bucket', result.error?.code, result.error?.status);
451
+ }
452
+ return graphResult(result.data);
453
+ } catch (err) {
454
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
455
+ return graphError(err instanceof Error ? err.message : 'Failed to get bucket');
456
+ }
457
+ }
458
+
459
+ export async function createPlannerBucket(
460
+ token: string,
461
+ planId: string,
462
+ name: string
463
+ ): Promise<GraphResponse<PlannerBucket>> {
464
+ try {
465
+ const result = await callGraph<PlannerBucket>(token, '/planner/buckets', {
466
+ method: 'POST',
467
+ body: JSON.stringify({ planId, name, orderHint: ' !' })
468
+ });
469
+ if (!result.ok || !result.data) {
470
+ return graphError(result.error?.message || 'Failed to create bucket', result.error?.code, result.error?.status);
471
+ }
472
+ return graphResult(result.data);
473
+ } catch (err) {
474
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
475
+ return graphError(err instanceof Error ? err.message : 'Failed to create bucket');
476
+ }
477
+ }
478
+
479
+ export async function updatePlannerBucket(
480
+ token: string,
481
+ bucketId: string,
482
+ etag: string,
483
+ updates: { name?: string; orderHint?: string }
484
+ ): Promise<GraphResponse<void>> {
485
+ const body: Record<string, unknown> = {};
486
+ if (updates.name !== undefined) body.name = updates.name;
487
+ if (updates.orderHint !== undefined) body.orderHint = updates.orderHint;
488
+ if (Object.keys(body).length === 0) {
489
+ return graphError('No bucket updates', 'NO_UPDATES', 400);
490
+ }
491
+ try {
492
+ const result = await callGraph<void>(token, `/planner/buckets/${encodeURIComponent(bucketId)}`, {
493
+ method: 'PATCH',
494
+ headers: { 'If-Match': etag },
495
+ body: JSON.stringify(body)
496
+ });
497
+ if (!result.ok) {
498
+ return graphError(result.error?.message || 'Failed to update bucket', result.error?.code, result.error?.status);
499
+ }
500
+ return graphResult(undefined as undefined);
501
+ } catch (err) {
502
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
503
+ return graphError(err instanceof Error ? err.message : 'Failed to update bucket');
504
+ }
505
+ }
506
+
507
+ export async function deletePlannerBucket(token: string, bucketId: string, etag: string): Promise<GraphResponse<void>> {
508
+ try {
509
+ const result = await callGraph<void>(
510
+ token,
511
+ `/planner/buckets/${encodeURIComponent(bucketId)}`,
512
+ {
513
+ method: 'DELETE',
514
+ headers: { 'If-Match': etag }
515
+ },
516
+ false
517
+ );
518
+ if (!result.ok) {
519
+ return graphError(result.error?.message || 'Failed to delete bucket', result.error?.code, result.error?.status);
520
+ }
521
+ return graphResult(undefined as undefined);
522
+ } catch (err) {
523
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
524
+ return graphError(err instanceof Error ? err.message : 'Failed to delete bucket');
525
+ }
526
+ }
527
+
528
+ export async function deletePlannerTask(token: string, taskId: string, etag: string): Promise<GraphResponse<void>> {
529
+ try {
530
+ const result = await callGraph<void>(
531
+ token,
532
+ `/planner/tasks/${encodeURIComponent(taskId)}`,
533
+ {
534
+ method: 'DELETE',
535
+ headers: { 'If-Match': etag }
536
+ },
537
+ false
538
+ );
539
+ if (!result.ok) {
540
+ return graphError(result.error?.message || 'Failed to delete task', result.error?.code, result.error?.status);
541
+ }
542
+ return graphResult(undefined as undefined);
543
+ } catch (err) {
544
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
545
+ return graphError(err instanceof Error ? err.message : 'Failed to delete task');
546
+ }
547
+ }
548
+
549
+ export async function getPlannerTaskDetails(token: string, taskId: string): Promise<GraphResponse<PlannerTaskDetails>> {
550
+ try {
551
+ const result = await callGraph<PlannerTaskDetails>(token, `/planner/tasks/${encodeURIComponent(taskId)}/details`);
552
+ if (!result.ok || !result.data) {
553
+ return graphError(
554
+ result.error?.message || 'Failed to get task details',
555
+ result.error?.code,
556
+ result.error?.status
557
+ );
558
+ }
559
+ return graphResult(result.data);
560
+ } catch (err) {
561
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
562
+ return graphError(err instanceof Error ? err.message : 'Failed to get task details');
563
+ }
564
+ }
565
+
566
+ export interface UpdatePlannerTaskDetailsParams {
567
+ description?: string | null;
568
+ checklist?: Record<string, PlannerTaskDetailsChecklistItem> | null;
569
+ references?: Record<string, unknown> | null;
570
+ previewType?: string;
571
+ }
572
+
573
+ export async function updatePlannerTaskDetails(
574
+ token: string,
575
+ taskDetailsId: string,
576
+ etag: string,
577
+ updates: UpdatePlannerTaskDetailsParams
578
+ ): Promise<GraphResponse<void>> {
579
+ try {
580
+ const result = await callGraph<void>(token, `/planner/taskDetails/${encodeURIComponent(taskDetailsId)}`, {
581
+ method: 'PATCH',
582
+ headers: { 'If-Match': etag },
583
+ body: JSON.stringify(updates)
584
+ });
585
+ if (!result.ok) {
586
+ return graphError(
587
+ result.error?.message || 'Failed to update task details',
588
+ result.error?.code,
589
+ result.error?.status
590
+ );
591
+ }
592
+ return graphResult(undefined as undefined);
593
+ } catch (err) {
594
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
595
+ return graphError(err instanceof Error ? err.message : 'Failed to update task details');
596
+ }
597
+ }
598
+
599
+ export interface UpdatePlannerPlanDetailsParams {
600
+ categoryDescriptions?: Partial<Record<PlannerCategorySlot, string | null>>;
601
+ sharedWith?: Record<string, boolean>;
602
+ }
603
+
604
+ /** PATCH `/planner/plans/{id}/details` (label names, sharedWith). */
605
+ export async function updatePlannerPlanDetails(
606
+ token: string,
607
+ planId: string,
608
+ etag: string,
609
+ updates: UpdatePlannerPlanDetailsParams
610
+ ): Promise<GraphResponse<void>> {
611
+ try {
612
+ const result = await callGraph<void>(
613
+ token,
614
+ `/planner/plans/${encodeURIComponent(planId)}/details`,
615
+ {
616
+ method: 'PATCH',
617
+ headers: { 'If-Match': etag },
618
+ body: JSON.stringify(updates)
619
+ },
620
+ false
621
+ );
622
+ if (!result.ok) {
623
+ return graphError(
624
+ result.error?.message || 'Failed to update plan details',
625
+ result.error?.code,
626
+ result.error?.status
627
+ );
628
+ }
629
+ return graphResult(undefined as undefined);
630
+ } catch (err) {
631
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
632
+ return graphError(err instanceof Error ? err.message : 'Failed to update plan details');
633
+ }
634
+ }
635
+
636
+ /** Beta: plans marked favorite by the current user. */
637
+ export async function listFavoritePlans(token: string): Promise<GraphResponse<PlannerPlan[]>> {
638
+ return fetchAllPages<PlannerPlan>(
639
+ token,
640
+ '/me/planner/favoritePlans',
641
+ 'Failed to list favorite plans',
642
+ GRAPH_BETA_URL
643
+ );
644
+ }
645
+
646
+ /** Beta: plans from rosters the user belongs to. */
647
+ export async function listRosterPlans(token: string): Promise<GraphResponse<PlannerPlan[]>> {
648
+ return fetchAllPages<PlannerPlan>(token, '/me/planner/rosterPlans', 'Failed to list roster plans', GRAPH_BETA_URL);
649
+ }
650
+
651
+ export interface PlannerDeltaPage {
652
+ value?: unknown[];
653
+ '@odata.nextLink'?: string;
654
+ '@odata.deltaLink'?: string;
655
+ }
656
+
657
+ /** Beta: one page of `/me/planner/all/delta` (pass `nextLink` or `deltaLink` URL from a prior response). */
658
+ export async function getPlannerDeltaPage(
659
+ token: string,
660
+ nextOrDeltaUrl?: string
661
+ ): Promise<GraphResponse<PlannerDeltaPage>> {
662
+ try {
663
+ if (nextOrDeltaUrl) {
664
+ return await callGraphAbsolute<PlannerDeltaPage>(token, nextOrDeltaUrl);
665
+ }
666
+ return await callGraphAt<PlannerDeltaPage>(GRAPH_BETA_URL, token, '/me/planner/all/delta');
667
+ } catch (err) {
668
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
669
+ return graphError(err instanceof Error ? err.message : 'Failed to get Planner delta');
670
+ }
671
+ }
672
+
673
+ /** Add a checklist row (client-generated id) and PATCH task details. */
674
+ export async function addPlannerChecklistItem(
675
+ token: string,
676
+ taskId: string,
677
+ title: string,
678
+ checklistItemId?: string
679
+ ): Promise<GraphResponse<void>> {
680
+ const id = checklistItemId || randomUUID();
681
+ const dr = await getPlannerTaskDetails(token, taskId);
682
+ if (!dr.ok || !dr.data) {
683
+ return graphError(dr.error?.message || 'Failed to get task details', dr.error?.code, dr.error?.status);
684
+ }
685
+ const etag = dr.data['@odata.etag'];
686
+ if (!etag) return graphError('Task details missing ETag', 'MISSING_ETAG', 500);
687
+ const checklist = { ...(dr.data.checklist || {}) };
688
+ checklist[id] = {
689
+ '@odata.type': '#microsoft.graph.plannerChecklistItem',
690
+ isChecked: false,
691
+ title,
692
+ orderHint: ' !'
693
+ };
694
+ return updatePlannerTaskDetails(token, dr.data.id, etag, { checklist });
695
+ }
696
+
697
+ /** Remove a checklist item by id. */
698
+ export async function removePlannerChecklistItem(
699
+ token: string,
700
+ taskId: string,
701
+ checklistItemId: string
702
+ ): Promise<GraphResponse<void>> {
703
+ const dr = await getPlannerTaskDetails(token, taskId);
704
+ if (!dr.ok || !dr.data) {
705
+ return graphError(dr.error?.message || 'Failed to get task details', dr.error?.code, dr.error?.status);
706
+ }
707
+ const etag = dr.data['@odata.etag'];
708
+ if (!etag) return graphError('Task details missing ETag', 'MISSING_ETAG', 500);
709
+ const checklist = { ...(dr.data.checklist || {}) };
710
+ delete checklist[checklistItemId];
711
+ return updatePlannerTaskDetails(token, dr.data.id, etag, { checklist });
712
+ }
713
+
714
+ /** Add or replace a reference URL entry on task details. */
715
+ export async function addPlannerReference(
716
+ token: string,
717
+ taskId: string,
718
+ resourceUrl: string,
719
+ alias: string,
720
+ type?: string
721
+ ): Promise<GraphResponse<void>> {
722
+ const dr = await getPlannerTaskDetails(token, taskId);
723
+ if (!dr.ok || !dr.data) {
724
+ return graphError(dr.error?.message || 'Failed to get task details', dr.error?.code, dr.error?.status);
725
+ }
726
+ const etag = dr.data['@odata.etag'];
727
+ if (!etag) return graphError('Task details missing ETag', 'MISSING_ETAG', 500);
728
+ const references = { ...(dr.data.references || {}) };
729
+ references[resourceUrl] = {
730
+ '@odata.type': '#microsoft.graph.plannerExternalReference',
731
+ alias,
732
+ ...(type ? { type } : {}),
733
+ previewPriority: ' !'
734
+ };
735
+ return updatePlannerTaskDetails(token, dr.data.id, etag, { references });
736
+ }
737
+
738
+ /** Remove a reference by key URL. */
739
+ export async function removePlannerReference(
740
+ token: string,
741
+ taskId: string,
742
+ resourceUrl: string
743
+ ): Promise<GraphResponse<void>> {
744
+ const dr = await getPlannerTaskDetails(token, taskId);
745
+ if (!dr.ok || !dr.data) {
746
+ return graphError(dr.error?.message || 'Failed to get task details', dr.error?.code, dr.error?.status);
747
+ }
748
+ const etag = dr.data['@odata.etag'];
749
+ if (!etag) return graphError('Task details missing ETag', 'MISSING_ETAG', 500);
750
+ const references = { ...(dr.data.references || {}) };
751
+ delete references[resourceUrl];
752
+ return updatePlannerTaskDetails(token, dr.data.id, etag, { references });
753
+ }
754
+
755
+ /** Update one checklist row (title, checked state, orderHint). */
756
+ export async function updatePlannerChecklistItem(
757
+ token: string,
758
+ taskId: string,
759
+ checklistItemId: string,
760
+ patch: { title?: string; isChecked?: boolean; orderHint?: string }
761
+ ): Promise<GraphResponse<void>> {
762
+ const dr = await getPlannerTaskDetails(token, taskId);
763
+ if (!dr.ok || !dr.data) {
764
+ return graphError(dr.error?.message || 'Failed to get task details', dr.error?.code, dr.error?.status);
765
+ }
766
+ const etag = dr.data['@odata.etag'];
767
+ if (!etag) return graphError('Task details missing ETag', 'MISSING_ETAG', 500);
768
+ const checklist = { ...(dr.data.checklist || {}) };
769
+ const cur = checklist[checklistItemId];
770
+ if (!cur) {
771
+ return graphError(`Checklist item not found: ${checklistItemId}`, 'NOT_FOUND', 404);
772
+ }
773
+ checklist[checklistItemId] = {
774
+ ...cur,
775
+ '@odata.type': cur['@odata.type'] ?? '#microsoft.graph.plannerChecklistItem',
776
+ ...(patch.title !== undefined ? { title: patch.title } : {}),
777
+ ...(patch.isChecked !== undefined ? { isChecked: patch.isChecked } : {}),
778
+ ...(patch.orderHint !== undefined ? { orderHint: patch.orderHint } : {})
779
+ };
780
+ return updatePlannerTaskDetails(token, dr.data.id, etag, { checklist });
781
+ }
782
+
783
+ export interface PlannerAssignedToTaskBoardFormat {
784
+ id: string;
785
+ unassignedOrderHint?: string;
786
+ orderHintsByAssignee?: Record<string, string>;
787
+ '@odata.etag'?: string;
788
+ }
789
+
790
+ export interface PlannerBucketTaskBoardFormat {
791
+ id: string;
792
+ orderHint?: string;
793
+ '@odata.etag'?: string;
794
+ }
795
+
796
+ export interface PlannerProgressTaskBoardFormat {
797
+ id: string;
798
+ orderHint?: string;
799
+ '@odata.etag'?: string;
800
+ }
801
+
802
+ export async function getAssignedToTaskBoardFormat(
803
+ token: string,
804
+ taskId: string
805
+ ): Promise<GraphResponse<PlannerAssignedToTaskBoardFormat>> {
806
+ try {
807
+ const result = await callGraph<PlannerAssignedToTaskBoardFormat>(
808
+ token,
809
+ `/planner/tasks/${encodeURIComponent(taskId)}/assignedToTaskBoardFormat`
810
+ );
811
+ if (!result.ok || !result.data) {
812
+ return graphError(
813
+ result.error?.message || 'Failed to get assignedTo task board format',
814
+ result.error?.code,
815
+ result.error?.status
816
+ );
817
+ }
818
+ return graphResult(result.data);
819
+ } catch (err) {
820
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
821
+ return graphError(err instanceof Error ? err.message : 'Failed to get assignedTo task board format');
822
+ }
823
+ }
824
+
825
+ export async function updateAssignedToTaskBoardFormat(
826
+ token: string,
827
+ taskId: string,
828
+ etag: string,
829
+ updates: Partial<{ orderHintsByAssignee: Record<string, string> | null; unassignedOrderHint: string | null }>
830
+ ): Promise<GraphResponse<void>> {
831
+ try {
832
+ const result = await callGraph<void>(
833
+ token,
834
+ `/planner/tasks/${encodeURIComponent(taskId)}/assignedToTaskBoardFormat`,
835
+ {
836
+ method: 'PATCH',
837
+ headers: { 'If-Match': etag },
838
+ body: JSON.stringify(updates)
839
+ },
840
+ false
841
+ );
842
+ if (!result.ok) {
843
+ return graphError(
844
+ result.error?.message || 'Failed to update assignedTo task board format',
845
+ result.error?.code,
846
+ result.error?.status
847
+ );
848
+ }
849
+ return graphResult(undefined as undefined);
850
+ } catch (err) {
851
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
852
+ return graphError(err instanceof Error ? err.message : 'Failed to update assignedTo task board format');
853
+ }
854
+ }
855
+
856
+ export async function getBucketTaskBoardFormat(
857
+ token: string,
858
+ taskId: string
859
+ ): Promise<GraphResponse<PlannerBucketTaskBoardFormat>> {
860
+ try {
861
+ const result = await callGraph<PlannerBucketTaskBoardFormat>(
862
+ token,
863
+ `/planner/tasks/${encodeURIComponent(taskId)}/bucketTaskBoardFormat`
864
+ );
865
+ if (!result.ok || !result.data) {
866
+ return graphError(
867
+ result.error?.message || 'Failed to get bucket task board format',
868
+ result.error?.code,
869
+ result.error?.status
870
+ );
871
+ }
872
+ return graphResult(result.data);
873
+ } catch (err) {
874
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
875
+ return graphError(err instanceof Error ? err.message : 'Failed to get bucket task board format');
876
+ }
877
+ }
878
+
879
+ export async function updateBucketTaskBoardFormat(
880
+ token: string,
881
+ taskId: string,
882
+ etag: string,
883
+ orderHint: string
884
+ ): Promise<GraphResponse<void>> {
885
+ try {
886
+ const result = await callGraph<void>(
887
+ token,
888
+ `/planner/tasks/${encodeURIComponent(taskId)}/bucketTaskBoardFormat`,
889
+ {
890
+ method: 'PATCH',
891
+ headers: { 'If-Match': etag },
892
+ body: JSON.stringify({ orderHint })
893
+ },
894
+ false
895
+ );
896
+ if (!result.ok) {
897
+ return graphError(
898
+ result.error?.message || 'Failed to update bucket task board format',
899
+ result.error?.code,
900
+ result.error?.status
901
+ );
902
+ }
903
+ return graphResult(undefined as undefined);
904
+ } catch (err) {
905
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
906
+ return graphError(err instanceof Error ? err.message : 'Failed to update bucket task board format');
907
+ }
908
+ }
909
+
910
+ export async function getProgressTaskBoardFormat(
911
+ token: string,
912
+ taskId: string
913
+ ): Promise<GraphResponse<PlannerProgressTaskBoardFormat>> {
914
+ try {
915
+ const result = await callGraph<PlannerProgressTaskBoardFormat>(
916
+ token,
917
+ `/planner/tasks/${encodeURIComponent(taskId)}/progressTaskBoardFormat`
918
+ );
919
+ if (!result.ok || !result.data) {
920
+ return graphError(
921
+ result.error?.message || 'Failed to get progress task board format',
922
+ result.error?.code,
923
+ result.error?.status
924
+ );
925
+ }
926
+ return graphResult(result.data);
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 get progress task board format');
930
+ }
931
+ }
932
+
933
+ export async function updateProgressTaskBoardFormat(
934
+ token: string,
935
+ taskId: string,
936
+ etag: string,
937
+ orderHint: string
938
+ ): Promise<GraphResponse<void>> {
939
+ try {
940
+ const result = await callGraph<void>(
941
+ token,
942
+ `/planner/tasks/${encodeURIComponent(taskId)}/progressTaskBoardFormat`,
943
+ {
944
+ method: 'PATCH',
945
+ headers: { 'If-Match': etag },
946
+ body: JSON.stringify({ orderHint })
947
+ },
948
+ false
949
+ );
950
+ if (!result.ok) {
951
+ return graphError(
952
+ result.error?.message || 'Failed to update progress task board format',
953
+ result.error?.code,
954
+ result.error?.status
955
+ );
956
+ }
957
+ return graphResult(undefined as undefined);
958
+ } catch (err) {
959
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
960
+ return graphError(err instanceof Error ? err.message : 'Failed to update progress task board format');
961
+ }
962
+ }
963
+
964
+ /** Beta: current user's Planner preferences (favorites, recents). */
965
+ export interface PlannerUser {
966
+ id: string;
967
+ '@odata.etag'?: string;
968
+ favoritePlanReferences?: Record<string, unknown>;
969
+ recentPlanReferences?: Record<string, unknown>;
970
+ }
971
+
972
+ export async function getPlannerUser(token: string): Promise<GraphResponse<PlannerUser>> {
973
+ try {
974
+ const result = await callGraphAt<PlannerUser>(GRAPH_BETA_URL, token, '/me/planner');
975
+ if (!result.ok || !result.data) {
976
+ return graphError(
977
+ result.error?.message || 'Failed to get planner user',
978
+ result.error?.code,
979
+ result.error?.status
980
+ );
981
+ }
982
+ return graphResult(result.data);
983
+ } catch (err) {
984
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
985
+ return graphError(err instanceof Error ? err.message : 'Failed to get planner user');
986
+ }
987
+ }
988
+
989
+ async function patchPlannerUser(
990
+ token: string,
991
+ etag: string,
992
+ body: {
993
+ favoritePlanReferences?: Record<string, unknown> | null;
994
+ recentPlanReferences?: Record<string, unknown> | null;
995
+ }
996
+ ): Promise<GraphResponse<PlannerUser | undefined>> {
997
+ try {
998
+ const result = await callGraphAt<PlannerUser>(
999
+ GRAPH_BETA_URL,
1000
+ token,
1001
+ '/me/planner',
1002
+ {
1003
+ method: 'PATCH',
1004
+ headers: {
1005
+ 'If-Match': etag,
1006
+ Prefer: 'return=representation'
1007
+ },
1008
+ body: JSON.stringify(body)
1009
+ },
1010
+ true
1011
+ );
1012
+ if (!result.ok) {
1013
+ return graphError(
1014
+ result.error?.message || 'Failed to update planner user',
1015
+ result.error?.code,
1016
+ result.error?.status
1017
+ );
1018
+ }
1019
+ return graphResult(result.data);
1020
+ } catch (err) {
1021
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
1022
+ return graphError(err instanceof Error ? err.message : 'Failed to update planner user');
1023
+ }
1024
+ }
1025
+
1026
+ /** Beta: add or update a favorite plan entry (PATCH /me/planner merge). */
1027
+ export async function addPlannerFavoritePlan(
1028
+ token: string,
1029
+ planId: string,
1030
+ planTitle: string
1031
+ ): Promise<GraphResponse<PlannerUser | undefined>> {
1032
+ const ur = await getPlannerUser(token);
1033
+ if (!ur.ok || !ur.data) {
1034
+ return graphError(ur.error?.message || 'Failed to get planner user', ur.error?.code, ur.error?.status);
1035
+ }
1036
+ const etag = ur.data['@odata.etag'];
1037
+ if (!etag) return graphError('plannerUser missing ETag', 'MISSING_ETAG', 500);
1038
+ return patchPlannerUser(token, etag, {
1039
+ favoritePlanReferences: {
1040
+ [planId]: {
1041
+ '@odata.type': '#microsoft.graph.plannerFavoritePlanReference',
1042
+ orderHint: ' !',
1043
+ planTitle
1044
+ }
1045
+ }
1046
+ });
1047
+ }
1048
+
1049
+ /** Beta: remove a plan from favorites (set reference to null). */
1050
+ export async function removePlannerFavoritePlan(
1051
+ token: string,
1052
+ planId: string
1053
+ ): Promise<GraphResponse<PlannerUser | undefined>> {
1054
+ const ur = await getPlannerUser(token);
1055
+ if (!ur.ok || !ur.data) {
1056
+ return graphError(ur.error?.message || 'Failed to get planner user', ur.error?.code, ur.error?.status);
1057
+ }
1058
+ const etag = ur.data['@odata.etag'];
1059
+ if (!etag) return graphError('plannerUser missing ETag', 'MISSING_ETAG', 500);
1060
+ const favoritePlanReferences: Record<string, unknown> = { [planId]: null };
1061
+ return patchPlannerUser(token, etag, { favoritePlanReferences });
1062
+ }
1063
+
1064
+ /** Beta: security container for roster-backed plans (see Graph `plannerRoster`). */
1065
+ export interface PlannerRoster {
1066
+ id: string;
1067
+ '@odata.type'?: string;
1068
+ }
1069
+
1070
+ /** Beta: one member of a Planner roster. */
1071
+ export interface PlannerRosterMember {
1072
+ id: string;
1073
+ userId: string;
1074
+ roles?: string[];
1075
+ '@odata.type'?: string;
1076
+ }
1077
+
1078
+ /** Beta: `POST /planner/rosters` — create an empty roster (then add members and add a plan). */
1079
+ export async function createPlannerRoster(token: string): Promise<GraphResponse<PlannerRoster>> {
1080
+ try {
1081
+ const result = await callGraphAt<PlannerRoster>(GRAPH_BETA_URL, token, '/planner/rosters', {
1082
+ method: 'POST',
1083
+ body: JSON.stringify({ '@odata.type': '#microsoft.graph.plannerRoster' })
1084
+ });
1085
+ if (!result.ok || !result.data) {
1086
+ return graphError(result.error?.message || 'Failed to create roster', result.error?.code, result.error?.status);
1087
+ }
1088
+ return graphResult(result.data);
1089
+ } catch (err) {
1090
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
1091
+ return graphError(err instanceof Error ? err.message : 'Failed to create roster');
1092
+ }
1093
+ }
1094
+
1095
+ /** Beta: `GET /planner/rosters/{id}`. */
1096
+ export async function getPlannerRoster(token: string, rosterId: string): Promise<GraphResponse<PlannerRoster>> {
1097
+ try {
1098
+ const result = await callGraphAt<PlannerRoster>(
1099
+ GRAPH_BETA_URL,
1100
+ token,
1101
+ `/planner/rosters/${encodeURIComponent(rosterId)}`
1102
+ );
1103
+ if (!result.ok || !result.data) {
1104
+ return graphError(result.error?.message || 'Failed to get roster', result.error?.code, result.error?.status);
1105
+ }
1106
+ return graphResult(result.data);
1107
+ } catch (err) {
1108
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
1109
+ return graphError(err instanceof Error ? err.message : 'Failed to get roster');
1110
+ }
1111
+ }
1112
+
1113
+ /** Beta: `GET /planner/rosters/{id}/members`. */
1114
+ export async function listPlannerRosterMembers(
1115
+ token: string,
1116
+ rosterId: string
1117
+ ): Promise<GraphResponse<PlannerRosterMember[]>> {
1118
+ return fetchAllPages<PlannerRosterMember>(
1119
+ token,
1120
+ `/planner/rosters/${encodeURIComponent(rosterId)}/members`,
1121
+ 'Failed to list roster members',
1122
+ GRAPH_BETA_URL
1123
+ );
1124
+ }
1125
+
1126
+ /** Beta: `POST /planner/rosters/{id}/members`. */
1127
+ export async function addPlannerRosterMember(
1128
+ token: string,
1129
+ rosterId: string,
1130
+ userId: string,
1131
+ options?: { tenantId?: string; roles?: string[] }
1132
+ ): Promise<GraphResponse<PlannerRosterMember>> {
1133
+ try {
1134
+ const body: Record<string, unknown> = {
1135
+ '@odata.type': '#microsoft.graph.plannerRosterMember',
1136
+ userId
1137
+ };
1138
+ if (options?.tenantId !== undefined) body.tenantId = options.tenantId;
1139
+ if (options?.roles !== undefined && options.roles.length > 0) body.roles = options.roles;
1140
+ const result = await callGraphAt<PlannerRosterMember>(
1141
+ GRAPH_BETA_URL,
1142
+ token,
1143
+ `/planner/rosters/${encodeURIComponent(rosterId)}/members`,
1144
+ {
1145
+ method: 'POST',
1146
+ body: JSON.stringify(body)
1147
+ }
1148
+ );
1149
+ if (!result.ok || !result.data) {
1150
+ return graphError(
1151
+ result.error?.message || 'Failed to add roster member',
1152
+ result.error?.code,
1153
+ result.error?.status
1154
+ );
1155
+ }
1156
+ return graphResult(result.data);
1157
+ } catch (err) {
1158
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
1159
+ return graphError(err instanceof Error ? err.message : 'Failed to add roster member');
1160
+ }
1161
+ }
1162
+
1163
+ /** Beta: `DELETE /planner/rosters/{rosterId}/members/{memberId}` (member id is the roster member resource id). */
1164
+ export async function removePlannerRosterMember(
1165
+ token: string,
1166
+ rosterId: string,
1167
+ memberId: string
1168
+ ): Promise<GraphResponse<void>> {
1169
+ try {
1170
+ const result = await callGraphAt<void>(
1171
+ GRAPH_BETA_URL,
1172
+ token,
1173
+ `/planner/rosters/${encodeURIComponent(rosterId)}/members/${encodeURIComponent(memberId)}`,
1174
+ { method: 'DELETE' },
1175
+ false
1176
+ );
1177
+ if (!result.ok) {
1178
+ return graphError(
1179
+ result.error?.message || 'Failed to remove roster member',
1180
+ result.error?.code,
1181
+ result.error?.status
1182
+ );
1183
+ }
1184
+ return graphResult(undefined as undefined);
1185
+ } catch (err) {
1186
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
1187
+ return graphError(err instanceof Error ? err.message : 'Failed to remove roster member');
1188
+ }
1189
+ }
1190
+
1191
+ /**
1192
+ * Beta: create a plan contained by a roster (`POST /planner/plans` on beta with `container.type` roster).
1193
+ * @see https://learn.microsoft.com/en-us/graph/api/resources/plannerplancontainer
1194
+ */
1195
+ export async function createPlannerPlanInRoster(
1196
+ token: string,
1197
+ rosterId: string,
1198
+ title: string
1199
+ ): Promise<GraphResponse<PlannerPlan>> {
1200
+ try {
1201
+ const base = GRAPH_BETA_URL.replace(/\/$/, '');
1202
+ const result = await callGraphAt<PlannerPlan>(GRAPH_BETA_URL, token, '/planner/plans', {
1203
+ method: 'POST',
1204
+ headers: { Prefer: 'include-unknown-enum-members' },
1205
+ body: JSON.stringify({
1206
+ title,
1207
+ container: {
1208
+ '@odata.type': '#microsoft.graph.plannerPlanContainer',
1209
+ url: `${base}/planner/rosters/${encodeURIComponent(rosterId)}`,
1210
+ type: 'roster'
1211
+ }
1212
+ })
1213
+ });
1214
+ if (!result.ok || !result.data) {
1215
+ return graphError(
1216
+ result.error?.message || 'Failed to create plan in roster',
1217
+ result.error?.code,
1218
+ result.error?.status
1219
+ );
1220
+ }
1221
+ return graphResult(result.data);
1222
+ } catch (err) {
1223
+ if (err instanceof GraphApiError) return graphError(err.message, err.code, err.status);
1224
+ return graphError(err instanceof Error ? err.message : 'Failed to create plan in roster');
1225
+ }
1226
+ }