@node2flow/google-calendar-mcp 1.0.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.
@@ -0,0 +1,641 @@
1
+ /**
2
+ * Google Calendar API v3 Client — OAuth 2.0 refresh token pattern
3
+ */
4
+
5
+ import type {
6
+ Event,
7
+ EventList,
8
+ CalendarListEntry,
9
+ CalendarListList,
10
+ Calendar,
11
+ AclRule,
12
+ AclList,
13
+ FreeBusyResponse,
14
+ Colors,
15
+ SettingsList,
16
+ } from './types.js';
17
+
18
+ export interface CalendarClientConfig {
19
+ clientId: string;
20
+ clientSecret: string;
21
+ refreshToken: string;
22
+ }
23
+
24
+ export class CalendarClient {
25
+ private config: CalendarClientConfig;
26
+ private accessToken: string | null = null;
27
+ private tokenExpiry = 0;
28
+
29
+ private static readonly BASE = 'https://www.googleapis.com/calendar/v3';
30
+ private static readonly TOKEN_URL = 'https://oauth2.googleapis.com/token';
31
+
32
+ constructor(config: CalendarClientConfig) {
33
+ this.config = config;
34
+ }
35
+
36
+ // ========== OAuth ==========
37
+
38
+ private async getAccessToken(): Promise<string> {
39
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
40
+ return this.accessToken;
41
+ }
42
+
43
+ const res = await fetch(CalendarClient.TOKEN_URL, {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
46
+ body: new URLSearchParams({
47
+ client_id: this.config.clientId,
48
+ client_secret: this.config.clientSecret,
49
+ refresh_token: this.config.refreshToken,
50
+ grant_type: 'refresh_token',
51
+ }),
52
+ });
53
+
54
+ if (!res.ok) {
55
+ const text = await res.text();
56
+ throw new Error(`Token refresh failed (${res.status}): ${text}`);
57
+ }
58
+
59
+ const data = (await res.json()) as { access_token: string; expires_in: number };
60
+ this.accessToken = data.access_token;
61
+ this.tokenExpiry = Date.now() + (data.expires_in - 60) * 1000;
62
+ return this.accessToken;
63
+ }
64
+
65
+ private async request(path: string, options: RequestInit = {}): Promise<unknown> {
66
+ const token = await this.getAccessToken();
67
+ const url = `${CalendarClient.BASE}${path}`;
68
+
69
+ const res = await fetch(url, {
70
+ ...options,
71
+ headers: {
72
+ Authorization: `Bearer ${token}`,
73
+ 'Content-Type': 'application/json',
74
+ ...options.headers,
75
+ },
76
+ });
77
+
78
+ if (!res.ok) {
79
+ const text = await res.text();
80
+ throw new Error(`Calendar API error (${res.status}): ${text}`);
81
+ }
82
+
83
+ const contentType = res.headers.get('content-type') || '';
84
+ if (contentType.includes('application/json')) {
85
+ return res.json();
86
+ }
87
+ return {};
88
+ }
89
+
90
+ // ========== Events (10) ==========
91
+
92
+ async listEvents(opts: {
93
+ calendarId: string;
94
+ timeMin?: string;
95
+ timeMax?: string;
96
+ q?: string;
97
+ maxResults?: number;
98
+ pageToken?: string;
99
+ singleEvents?: boolean;
100
+ orderBy?: string;
101
+ timeZone?: string;
102
+ showDeleted?: boolean;
103
+ }): Promise<EventList> {
104
+ const params = new URLSearchParams();
105
+ if (opts.timeMin) params.set('timeMin', opts.timeMin);
106
+ if (opts.timeMax) params.set('timeMax', opts.timeMax);
107
+ if (opts.q) params.set('q', opts.q);
108
+ if (opts.maxResults) params.set('maxResults', String(opts.maxResults));
109
+ if (opts.pageToken) params.set('pageToken', opts.pageToken);
110
+ if (opts.singleEvents !== undefined) params.set('singleEvents', String(opts.singleEvents));
111
+ if (opts.orderBy) params.set('orderBy', opts.orderBy);
112
+ if (opts.timeZone) params.set('timeZone', opts.timeZone);
113
+ if (opts.showDeleted !== undefined) params.set('showDeleted', String(opts.showDeleted));
114
+ const qs = params.toString();
115
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/events${qs ? `?${qs}` : ''}`) as Promise<EventList>;
116
+ }
117
+
118
+ async getEvent(opts: {
119
+ calendarId: string;
120
+ eventId: string;
121
+ timeZone?: string;
122
+ }): Promise<Event> {
123
+ const params = new URLSearchParams();
124
+ if (opts.timeZone) params.set('timeZone', opts.timeZone);
125
+ const qs = params.toString();
126
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/events/${encodeURIComponent(opts.eventId)}${qs ? `?${qs}` : ''}`) as Promise<Event>;
127
+ }
128
+
129
+ async createEvent(opts: {
130
+ calendarId: string;
131
+ summary?: string;
132
+ description?: string;
133
+ location?: string;
134
+ startDateTime?: string;
135
+ startDate?: string;
136
+ startTimeZone?: string;
137
+ endDateTime?: string;
138
+ endDate?: string;
139
+ endTimeZone?: string;
140
+ attendees?: string[];
141
+ recurrence?: string[];
142
+ reminders?: { useDefault: boolean; overrides?: { method: string; minutes: number }[] };
143
+ colorId?: string;
144
+ visibility?: string;
145
+ transparency?: string;
146
+ sendUpdates?: string;
147
+ conferenceDataVersion?: number;
148
+ }): Promise<Event> {
149
+ const payload: Record<string, unknown> = {};
150
+ if (opts.summary) payload.summary = opts.summary;
151
+ if (opts.description) payload.description = opts.description;
152
+ if (opts.location) payload.location = opts.location;
153
+ if (opts.colorId) payload.colorId = opts.colorId;
154
+ if (opts.visibility) payload.visibility = opts.visibility;
155
+ if (opts.transparency) payload.transparency = opts.transparency;
156
+ if (opts.recurrence) payload.recurrence = opts.recurrence;
157
+
158
+ // Start time
159
+ if (opts.startDate) {
160
+ payload.start = { date: opts.startDate };
161
+ } else if (opts.startDateTime) {
162
+ const start: Record<string, string> = { dateTime: opts.startDateTime };
163
+ if (opts.startTimeZone) start.timeZone = opts.startTimeZone;
164
+ payload.start = start;
165
+ }
166
+
167
+ // End time
168
+ if (opts.endDate) {
169
+ payload.end = { date: opts.endDate };
170
+ } else if (opts.endDateTime) {
171
+ const end: Record<string, string> = { dateTime: opts.endDateTime };
172
+ if (opts.endTimeZone) end.timeZone = opts.endTimeZone;
173
+ payload.end = end;
174
+ }
175
+
176
+ if (opts.attendees) {
177
+ payload.attendees = opts.attendees.map(email => ({ email }));
178
+ }
179
+
180
+ if (opts.reminders) {
181
+ payload.reminders = opts.reminders;
182
+ }
183
+
184
+ const params = new URLSearchParams();
185
+ if (opts.sendUpdates) params.set('sendUpdates', opts.sendUpdates);
186
+ if (opts.conferenceDataVersion !== undefined) params.set('conferenceDataVersion', String(opts.conferenceDataVersion));
187
+ const qs = params.toString();
188
+
189
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/events${qs ? `?${qs}` : ''}`, {
190
+ method: 'POST',
191
+ body: JSON.stringify(payload),
192
+ }) as Promise<Event>;
193
+ }
194
+
195
+ async updateEvent(opts: {
196
+ calendarId: string;
197
+ eventId: string;
198
+ summary?: string;
199
+ description?: string;
200
+ location?: string;
201
+ startDateTime?: string;
202
+ startDate?: string;
203
+ startTimeZone?: string;
204
+ endDateTime?: string;
205
+ endDate?: string;
206
+ endTimeZone?: string;
207
+ attendees?: string[];
208
+ recurrence?: string[];
209
+ reminders?: { useDefault: boolean; overrides?: { method: string; minutes: number }[] };
210
+ colorId?: string;
211
+ visibility?: string;
212
+ transparency?: string;
213
+ sendUpdates?: string;
214
+ }): Promise<Event> {
215
+ const payload: Record<string, unknown> = {};
216
+ if (opts.summary !== undefined) payload.summary = opts.summary;
217
+ if (opts.description !== undefined) payload.description = opts.description;
218
+ if (opts.location !== undefined) payload.location = opts.location;
219
+ if (opts.colorId !== undefined) payload.colorId = opts.colorId;
220
+ if (opts.visibility !== undefined) payload.visibility = opts.visibility;
221
+ if (opts.transparency !== undefined) payload.transparency = opts.transparency;
222
+ if (opts.recurrence !== undefined) payload.recurrence = opts.recurrence;
223
+
224
+ if (opts.startDate) {
225
+ payload.start = { date: opts.startDate };
226
+ } else if (opts.startDateTime) {
227
+ const start: Record<string, string> = { dateTime: opts.startDateTime };
228
+ if (opts.startTimeZone) start.timeZone = opts.startTimeZone;
229
+ payload.start = start;
230
+ }
231
+
232
+ if (opts.endDate) {
233
+ payload.end = { date: opts.endDate };
234
+ } else if (opts.endDateTime) {
235
+ const end: Record<string, string> = { dateTime: opts.endDateTime };
236
+ if (opts.endTimeZone) end.timeZone = opts.endTimeZone;
237
+ payload.end = end;
238
+ }
239
+
240
+ if (opts.attendees) {
241
+ payload.attendees = opts.attendees.map(email => ({ email }));
242
+ }
243
+
244
+ if (opts.reminders) {
245
+ payload.reminders = opts.reminders;
246
+ }
247
+
248
+ const params = new URLSearchParams();
249
+ if (opts.sendUpdates) params.set('sendUpdates', opts.sendUpdates);
250
+ const qs = params.toString();
251
+
252
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/events/${encodeURIComponent(opts.eventId)}${qs ? `?${qs}` : ''}`, {
253
+ method: 'PUT',
254
+ body: JSON.stringify(payload),
255
+ }) as Promise<Event>;
256
+ }
257
+
258
+ async patchEvent(opts: {
259
+ calendarId: string;
260
+ eventId: string;
261
+ summary?: string;
262
+ description?: string;
263
+ location?: string;
264
+ startDateTime?: string;
265
+ startDate?: string;
266
+ startTimeZone?: string;
267
+ endDateTime?: string;
268
+ endDate?: string;
269
+ endTimeZone?: string;
270
+ attendees?: string[];
271
+ colorId?: string;
272
+ visibility?: string;
273
+ transparency?: string;
274
+ sendUpdates?: string;
275
+ }): Promise<Event> {
276
+ const payload: Record<string, unknown> = {};
277
+ if (opts.summary !== undefined) payload.summary = opts.summary;
278
+ if (opts.description !== undefined) payload.description = opts.description;
279
+ if (opts.location !== undefined) payload.location = opts.location;
280
+ if (opts.colorId !== undefined) payload.colorId = opts.colorId;
281
+ if (opts.visibility !== undefined) payload.visibility = opts.visibility;
282
+ if (opts.transparency !== undefined) payload.transparency = opts.transparency;
283
+
284
+ if (opts.startDate) {
285
+ payload.start = { date: opts.startDate };
286
+ } else if (opts.startDateTime) {
287
+ const start: Record<string, string> = { dateTime: opts.startDateTime };
288
+ if (opts.startTimeZone) start.timeZone = opts.startTimeZone;
289
+ payload.start = start;
290
+ }
291
+
292
+ if (opts.endDate) {
293
+ payload.end = { date: opts.endDate };
294
+ } else if (opts.endDateTime) {
295
+ const end: Record<string, string> = { dateTime: opts.endDateTime };
296
+ if (opts.endTimeZone) end.timeZone = opts.endTimeZone;
297
+ payload.end = end;
298
+ }
299
+
300
+ if (opts.attendees) {
301
+ payload.attendees = opts.attendees.map(email => ({ email }));
302
+ }
303
+
304
+ const params = new URLSearchParams();
305
+ if (opts.sendUpdates) params.set('sendUpdates', opts.sendUpdates);
306
+ const qs = params.toString();
307
+
308
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/events/${encodeURIComponent(opts.eventId)}${qs ? `?${qs}` : ''}`, {
309
+ method: 'PATCH',
310
+ body: JSON.stringify(payload),
311
+ }) as Promise<Event>;
312
+ }
313
+
314
+ async deleteEvent(opts: {
315
+ calendarId: string;
316
+ eventId: string;
317
+ sendUpdates?: string;
318
+ }): Promise<void> {
319
+ const params = new URLSearchParams();
320
+ if (opts.sendUpdates) params.set('sendUpdates', opts.sendUpdates);
321
+ const qs = params.toString();
322
+ await this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/events/${encodeURIComponent(opts.eventId)}${qs ? `?${qs}` : ''}`, {
323
+ method: 'DELETE',
324
+ });
325
+ }
326
+
327
+ async quickAdd(opts: {
328
+ calendarId: string;
329
+ text: string;
330
+ sendUpdates?: string;
331
+ }): Promise<Event> {
332
+ const params = new URLSearchParams({ text: opts.text });
333
+ if (opts.sendUpdates) params.set('sendUpdates', opts.sendUpdates);
334
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/events/quickAdd?${params.toString()}`, {
335
+ method: 'POST',
336
+ }) as Promise<Event>;
337
+ }
338
+
339
+ async moveEvent(opts: {
340
+ calendarId: string;
341
+ eventId: string;
342
+ destination: string;
343
+ sendUpdates?: string;
344
+ }): Promise<Event> {
345
+ const params = new URLSearchParams({ destination: opts.destination });
346
+ if (opts.sendUpdates) params.set('sendUpdates', opts.sendUpdates);
347
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/events/${encodeURIComponent(opts.eventId)}/move?${params.toString()}`, {
348
+ method: 'POST',
349
+ }) as Promise<Event>;
350
+ }
351
+
352
+ async listInstances(opts: {
353
+ calendarId: string;
354
+ eventId: string;
355
+ timeMin?: string;
356
+ timeMax?: string;
357
+ maxResults?: number;
358
+ pageToken?: string;
359
+ timeZone?: string;
360
+ }): Promise<EventList> {
361
+ const params = new URLSearchParams();
362
+ if (opts.timeMin) params.set('timeMin', opts.timeMin);
363
+ if (opts.timeMax) params.set('timeMax', opts.timeMax);
364
+ if (opts.maxResults) params.set('maxResults', String(opts.maxResults));
365
+ if (opts.pageToken) params.set('pageToken', opts.pageToken);
366
+ if (opts.timeZone) params.set('timeZone', opts.timeZone);
367
+ const qs = params.toString();
368
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/events/${encodeURIComponent(opts.eventId)}/instances${qs ? `?${qs}` : ''}`) as Promise<EventList>;
369
+ }
370
+
371
+ async importEvent(opts: {
372
+ calendarId: string;
373
+ iCalUID: string;
374
+ summary?: string;
375
+ description?: string;
376
+ location?: string;
377
+ startDateTime?: string;
378
+ startDate?: string;
379
+ startTimeZone?: string;
380
+ endDateTime?: string;
381
+ endDate?: string;
382
+ endTimeZone?: string;
383
+ }): Promise<Event> {
384
+ const payload: Record<string, unknown> = {
385
+ iCalUID: opts.iCalUID,
386
+ };
387
+ if (opts.summary) payload.summary = opts.summary;
388
+ if (opts.description) payload.description = opts.description;
389
+ if (opts.location) payload.location = opts.location;
390
+
391
+ if (opts.startDate) {
392
+ payload.start = { date: opts.startDate };
393
+ } else if (opts.startDateTime) {
394
+ const start: Record<string, string> = { dateTime: opts.startDateTime };
395
+ if (opts.startTimeZone) start.timeZone = opts.startTimeZone;
396
+ payload.start = start;
397
+ }
398
+
399
+ if (opts.endDate) {
400
+ payload.end = { date: opts.endDate };
401
+ } else if (opts.endDateTime) {
402
+ const end: Record<string, string> = { dateTime: opts.endDateTime };
403
+ if (opts.endTimeZone) end.timeZone = opts.endTimeZone;
404
+ payload.end = end;
405
+ }
406
+
407
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/events/import`, {
408
+ method: 'POST',
409
+ body: JSON.stringify(payload),
410
+ }) as Promise<Event>;
411
+ }
412
+
413
+ // ========== CalendarList (5) ==========
414
+
415
+ async listCalendars(opts: {
416
+ maxResults?: number;
417
+ pageToken?: string;
418
+ showDeleted?: boolean;
419
+ showHidden?: boolean;
420
+ }): Promise<CalendarListList> {
421
+ const params = new URLSearchParams();
422
+ if (opts.maxResults) params.set('maxResults', String(opts.maxResults));
423
+ if (opts.pageToken) params.set('pageToken', opts.pageToken);
424
+ if (opts.showDeleted !== undefined) params.set('showDeleted', String(opts.showDeleted));
425
+ if (opts.showHidden !== undefined) params.set('showHidden', String(opts.showHidden));
426
+ const qs = params.toString();
427
+ return this.request(`/users/me/calendarList${qs ? `?${qs}` : ''}`) as Promise<CalendarListList>;
428
+ }
429
+
430
+ async getCalendarEntry(opts: {
431
+ calendarId: string;
432
+ }): Promise<CalendarListEntry> {
433
+ return this.request(`/users/me/calendarList/${encodeURIComponent(opts.calendarId)}`) as Promise<CalendarListEntry>;
434
+ }
435
+
436
+ async addCalendar(opts: {
437
+ id: string;
438
+ colorId?: string;
439
+ summaryOverride?: string;
440
+ hidden?: boolean;
441
+ selected?: boolean;
442
+ }): Promise<CalendarListEntry> {
443
+ const payload: Record<string, unknown> = { id: opts.id };
444
+ if (opts.colorId) payload.colorId = opts.colorId;
445
+ if (opts.summaryOverride) payload.summaryOverride = opts.summaryOverride;
446
+ if (opts.hidden !== undefined) payload.hidden = opts.hidden;
447
+ if (opts.selected !== undefined) payload.selected = opts.selected;
448
+ return this.request('/users/me/calendarList', {
449
+ method: 'POST',
450
+ body: JSON.stringify(payload),
451
+ }) as Promise<CalendarListEntry>;
452
+ }
453
+
454
+ async updateCalendarEntry(opts: {
455
+ calendarId: string;
456
+ colorId?: string;
457
+ summaryOverride?: string;
458
+ hidden?: boolean;
459
+ selected?: boolean;
460
+ defaultReminders?: { method: string; minutes: number }[];
461
+ }): Promise<CalendarListEntry> {
462
+ const payload: Record<string, unknown> = {};
463
+ if (opts.colorId !== undefined) payload.colorId = opts.colorId;
464
+ if (opts.summaryOverride !== undefined) payload.summaryOverride = opts.summaryOverride;
465
+ if (opts.hidden !== undefined) payload.hidden = opts.hidden;
466
+ if (opts.selected !== undefined) payload.selected = opts.selected;
467
+ if (opts.defaultReminders) payload.defaultReminders = opts.defaultReminders;
468
+ return this.request(`/users/me/calendarList/${encodeURIComponent(opts.calendarId)}`, {
469
+ method: 'PUT',
470
+ body: JSON.stringify(payload),
471
+ }) as Promise<CalendarListEntry>;
472
+ }
473
+
474
+ async removeCalendar(opts: {
475
+ calendarId: string;
476
+ }): Promise<void> {
477
+ await this.request(`/users/me/calendarList/${encodeURIComponent(opts.calendarId)}`, {
478
+ method: 'DELETE',
479
+ });
480
+ }
481
+
482
+ // ========== Calendars (5) ==========
483
+
484
+ async getCalendar(opts: {
485
+ calendarId: string;
486
+ }): Promise<Calendar> {
487
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}`) as Promise<Calendar>;
488
+ }
489
+
490
+ async createCalendar(opts: {
491
+ summary: string;
492
+ description?: string;
493
+ location?: string;
494
+ timeZone?: string;
495
+ }): Promise<Calendar> {
496
+ const payload: Record<string, unknown> = { summary: opts.summary };
497
+ if (opts.description) payload.description = opts.description;
498
+ if (opts.location) payload.location = opts.location;
499
+ if (opts.timeZone) payload.timeZone = opts.timeZone;
500
+ return this.request('/calendars', {
501
+ method: 'POST',
502
+ body: JSON.stringify(payload),
503
+ }) as Promise<Calendar>;
504
+ }
505
+
506
+ async updateCalendar(opts: {
507
+ calendarId: string;
508
+ summary?: string;
509
+ description?: string;
510
+ location?: string;
511
+ timeZone?: string;
512
+ }): Promise<Calendar> {
513
+ const payload: Record<string, unknown> = {};
514
+ if (opts.summary !== undefined) payload.summary = opts.summary;
515
+ if (opts.description !== undefined) payload.description = opts.description;
516
+ if (opts.location !== undefined) payload.location = opts.location;
517
+ if (opts.timeZone !== undefined) payload.timeZone = opts.timeZone;
518
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}`, {
519
+ method: 'PUT',
520
+ body: JSON.stringify(payload),
521
+ }) as Promise<Calendar>;
522
+ }
523
+
524
+ async deleteCalendar(opts: {
525
+ calendarId: string;
526
+ }): Promise<void> {
527
+ await this.request(`/calendars/${encodeURIComponent(opts.calendarId)}`, {
528
+ method: 'DELETE',
529
+ });
530
+ }
531
+
532
+ async clearCalendar(opts: {
533
+ calendarId: string;
534
+ }): Promise<void> {
535
+ await this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/clear`, {
536
+ method: 'POST',
537
+ });
538
+ }
539
+
540
+ // ========== ACL (5) ==========
541
+
542
+ async listAcl(opts: {
543
+ calendarId: string;
544
+ maxResults?: number;
545
+ pageToken?: string;
546
+ showDeleted?: boolean;
547
+ }): Promise<AclList> {
548
+ const params = new URLSearchParams();
549
+ if (opts.maxResults) params.set('maxResults', String(opts.maxResults));
550
+ if (opts.pageToken) params.set('pageToken', opts.pageToken);
551
+ if (opts.showDeleted !== undefined) params.set('showDeleted', String(opts.showDeleted));
552
+ const qs = params.toString();
553
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/acl${qs ? `?${qs}` : ''}`) as Promise<AclList>;
554
+ }
555
+
556
+ async getAcl(opts: {
557
+ calendarId: string;
558
+ ruleId: string;
559
+ }): Promise<AclRule> {
560
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/acl/${encodeURIComponent(opts.ruleId)}`) as Promise<AclRule>;
561
+ }
562
+
563
+ async createAcl(opts: {
564
+ calendarId: string;
565
+ role: string;
566
+ scopeType: string;
567
+ scopeValue?: string;
568
+ sendNotifications?: boolean;
569
+ }): Promise<AclRule> {
570
+ const payload: Record<string, unknown> = {
571
+ role: opts.role,
572
+ scope: { type: opts.scopeType, value: opts.scopeValue },
573
+ };
574
+ const params = new URLSearchParams();
575
+ if (opts.sendNotifications !== undefined) params.set('sendNotifications', String(opts.sendNotifications));
576
+ const qs = params.toString();
577
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/acl${qs ? `?${qs}` : ''}`, {
578
+ method: 'POST',
579
+ body: JSON.stringify(payload),
580
+ }) as Promise<AclRule>;
581
+ }
582
+
583
+ async updateAcl(opts: {
584
+ calendarId: string;
585
+ ruleId: string;
586
+ role: string;
587
+ sendNotifications?: boolean;
588
+ }): Promise<AclRule> {
589
+ const params = new URLSearchParams();
590
+ if (opts.sendNotifications !== undefined) params.set('sendNotifications', String(opts.sendNotifications));
591
+ const qs = params.toString();
592
+ return this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/acl/${encodeURIComponent(opts.ruleId)}${qs ? `?${qs}` : ''}`, {
593
+ method: 'PUT',
594
+ body: JSON.stringify({ role: opts.role }),
595
+ }) as Promise<AclRule>;
596
+ }
597
+
598
+ async deleteAcl(opts: {
599
+ calendarId: string;
600
+ ruleId: string;
601
+ }): Promise<void> {
602
+ await this.request(`/calendars/${encodeURIComponent(opts.calendarId)}/acl/${encodeURIComponent(opts.ruleId)}`, {
603
+ method: 'DELETE',
604
+ });
605
+ }
606
+
607
+ // ========== Utility (3) ==========
608
+
609
+ async queryFreeBusy(opts: {
610
+ timeMin: string;
611
+ timeMax: string;
612
+ timeZone?: string;
613
+ calendarIds: string[];
614
+ }): Promise<FreeBusyResponse> {
615
+ const payload: Record<string, unknown> = {
616
+ timeMin: opts.timeMin,
617
+ timeMax: opts.timeMax,
618
+ items: opts.calendarIds.map(id => ({ id })),
619
+ };
620
+ if (opts.timeZone) payload.timeZone = opts.timeZone;
621
+ return this.request('/freeBusy', {
622
+ method: 'POST',
623
+ body: JSON.stringify(payload),
624
+ }) as Promise<FreeBusyResponse>;
625
+ }
626
+
627
+ async getColors(): Promise<Colors> {
628
+ return this.request('/colors') as Promise<Colors>;
629
+ }
630
+
631
+ async listSettings(opts: {
632
+ maxResults?: number;
633
+ pageToken?: string;
634
+ }): Promise<SettingsList> {
635
+ const params = new URLSearchParams();
636
+ if (opts.maxResults) params.set('maxResults', String(opts.maxResults));
637
+ if (opts.pageToken) params.set('pageToken', opts.pageToken);
638
+ const qs = params.toString();
639
+ return this.request(`/users/me/settings${qs ? `?${qs}` : ''}`) as Promise<SettingsList>;
640
+ }
641
+ }