@automate.ax/integration-contracts 0.55.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 (136) hide show
  1. package/dist/airtable/api.d.ts +19 -0
  2. package/dist/airtable/api.js +44 -0
  3. package/dist/airtable/index.d.ts +353 -0
  4. package/dist/airtable/index.js +32 -0
  5. package/dist/airtable/schemas.d.ts +716 -0
  6. package/dist/airtable/schemas.js +184 -0
  7. package/dist/asana/api.d.ts +70 -0
  8. package/dist/asana/api.js +119 -0
  9. package/dist/asana/index.d.ts +384 -0
  10. package/dist/asana/index.js +38 -0
  11. package/dist/asana/schemas.d.ts +1060 -0
  12. package/dist/asana/schemas.js +512 -0
  13. package/dist/brevo/api.d.ts +31 -0
  14. package/dist/brevo/api.js +64 -0
  15. package/dist/brevo/index.d.ts +822 -0
  16. package/dist/brevo/index.js +51 -0
  17. package/dist/brevo/schemas.d.ts +1217 -0
  18. package/dist/brevo/schemas.js +332 -0
  19. package/dist/github/index.d.ts +37 -0
  20. package/dist/github/index.js +106 -0
  21. package/dist/github/schemas.d.ts +8014 -0
  22. package/dist/github/schemas.js +113 -0
  23. package/dist/gmail/gmail.d.ts +172 -0
  24. package/dist/gmail/gmail.js +405 -0
  25. package/dist/gmail/index.d.ts +262 -0
  26. package/dist/gmail/index.js +23 -0
  27. package/dist/gmail/schemas.d.ts +444 -0
  28. package/dist/gmail/schemas.js +242 -0
  29. package/dist/google-calendar/event-schemas.d.ts +117 -0
  30. package/dist/google-calendar/event-schemas.js +13 -0
  31. package/dist/google-calendar/google-calendar.d.ts +147 -0
  32. package/dist/google-calendar/google-calendar.js +583 -0
  33. package/dist/google-calendar/index.d.ts +365 -0
  34. package/dist/google-calendar/index.js +24 -0
  35. package/dist/google-calendar/schemas.d.ts +592 -0
  36. package/dist/google-calendar/schemas.js +379 -0
  37. package/dist/google-forms/event-schemas.d.ts +105 -0
  38. package/dist/google-forms/event-schemas.js +20 -0
  39. package/dist/google-forms/google-forms.d.ts +243 -0
  40. package/dist/google-forms/google-forms.js +581 -0
  41. package/dist/google-forms/index.d.ts +141 -0
  42. package/dist/google-forms/index.js +20 -0
  43. package/dist/google-forms/schemas.d.ts +618 -0
  44. package/dist/google-forms/schemas.js +383 -0
  45. package/dist/linear/api.d.ts +63 -0
  46. package/dist/linear/api.js +88 -0
  47. package/dist/linear/index.d.ts +1338 -0
  48. package/dist/linear/index.js +55 -0
  49. package/dist/linear/schemas.d.ts +1795 -0
  50. package/dist/linear/schemas.js +760 -0
  51. package/dist/outlook/graph.d.ts +34 -0
  52. package/dist/outlook/graph.js +98 -0
  53. package/dist/outlook/index.d.ts +115 -0
  54. package/dist/outlook/index.js +11 -0
  55. package/dist/outlook/schemas.d.ts +529 -0
  56. package/dist/outlook/schemas.js +312 -0
  57. package/dist/resend/index.d.ts +707 -0
  58. package/dist/resend/index.js +54 -0
  59. package/dist/resend/schemas.d.ts +1033 -0
  60. package/dist/resend/schemas.js +377 -0
  61. package/dist/slack/index.d.ts +151 -0
  62. package/dist/slack/index.js +22 -0
  63. package/dist/slack/schemas.d.ts +641 -0
  64. package/dist/slack/schemas.js +581 -0
  65. package/dist/teams/graph.d.ts +33 -0
  66. package/dist/teams/graph.js +95 -0
  67. package/dist/teams/index.d.ts +164 -0
  68. package/dist/teams/index.js +21 -0
  69. package/dist/teams/schemas.d.ts +675 -0
  70. package/dist/teams/schemas.js +365 -0
  71. package/dist/trello/api.d.ts +69 -0
  72. package/dist/trello/api.js +140 -0
  73. package/dist/trello/event-schemas.d.ts +1101 -0
  74. package/dist/trello/event-schemas.js +233 -0
  75. package/dist/trello/index.d.ts +721 -0
  76. package/dist/trello/index.js +35 -0
  77. package/dist/trello/schemas.d.ts +283 -0
  78. package/dist/trello/schemas.js +401 -0
  79. package/dist/triggers.d.ts +26 -0
  80. package/dist/triggers.js +33 -0
  81. package/dist/vercel/index.d.ts +1065 -0
  82. package/dist/vercel/index.js +114 -0
  83. package/dist/vercel/schemas.d.ts +1518 -0
  84. package/dist/vercel/schemas.js +335 -0
  85. package/dist/whatsapp/api.d.ts +62 -0
  86. package/dist/whatsapp/api.js +121 -0
  87. package/dist/whatsapp/index.d.ts +614 -0
  88. package/dist/whatsapp/index.js +35 -0
  89. package/dist/whatsapp/schemas.d.ts +926 -0
  90. package/dist/whatsapp/schemas.js +326 -0
  91. package/package.json +169 -0
  92. package/src/airtable/api.ts +66 -0
  93. package/src/airtable/index.ts +41 -0
  94. package/src/airtable/schemas.ts +231 -0
  95. package/src/asana/api.ts +171 -0
  96. package/src/asana/index.ts +49 -0
  97. package/src/asana/schemas.ts +655 -0
  98. package/src/brevo/api.ts +89 -0
  99. package/src/brevo/index.ts +66 -0
  100. package/src/brevo/schemas.ts +449 -0
  101. package/src/github/index.ts +217 -0
  102. package/src/github/schemas.ts +435 -0
  103. package/src/gmail/gmail.ts +577 -0
  104. package/src/gmail/index.ts +26 -0
  105. package/src/gmail/schemas.ts +327 -0
  106. package/src/google-calendar/event-schemas.ts +16 -0
  107. package/src/google-calendar/google-calendar.ts +727 -0
  108. package/src/google-calendar/index.ts +27 -0
  109. package/src/google-calendar/schemas.ts +529 -0
  110. package/src/google-forms/event-schemas.ts +26 -0
  111. package/src/google-forms/google-forms.ts +683 -0
  112. package/src/google-forms/index.ts +23 -0
  113. package/src/google-forms/schemas.ts +499 -0
  114. package/src/linear/api.ts +141 -0
  115. package/src/linear/index.ts +71 -0
  116. package/src/linear/schemas.ts +1060 -0
  117. package/src/outlook/graph.ts +132 -0
  118. package/src/outlook/index.ts +14 -0
  119. package/src/outlook/schemas.ts +388 -0
  120. package/src/resend/index.ts +70 -0
  121. package/src/resend/schemas.ts +462 -0
  122. package/src/slack/index.ts +29 -0
  123. package/src/slack/schemas.ts +737 -0
  124. package/src/teams/graph.ts +130 -0
  125. package/src/teams/index.ts +27 -0
  126. package/src/teams/schemas.ts +463 -0
  127. package/src/trello/api.ts +190 -0
  128. package/src/trello/event-schemas.ts +290 -0
  129. package/src/trello/index.ts +45 -0
  130. package/src/trello/schemas.ts +507 -0
  131. package/src/triggers.ts +68 -0
  132. package/src/vercel/index.ts +147 -0
  133. package/src/vercel/schemas.ts +594 -0
  134. package/src/whatsapp/api.ts +171 -0
  135. package/src/whatsapp/index.ts +46 -0
  136. package/src/whatsapp/schemas.ts +341 -0
@@ -0,0 +1,727 @@
1
+ import { Buffer } from "node:buffer"
2
+ import { auth, calendar, type calendar_v3 } from "@googleapis/calendar"
3
+ import { z } from "zod"
4
+ import {
5
+ ACL_PAGE_SCHEMA,
6
+ ACL_RULE_SCHEMA,
7
+ CALENDAR_EVENT_SCHEMA,
8
+ CALENDAR_PAGE_SCHEMA,
9
+ CALENDAR_SCHEMA,
10
+ COLOR_PALETTE_SCHEMA,
11
+ EVENT_PAGE_SCHEMA,
12
+ EVENT_DETAILS_INPUT_SCHEMA,
13
+ EVENT_SCHEDULE_INPUT_SCHEMA,
14
+ FREE_BUSY_SCHEMA,
15
+ } from "./schemas"
16
+
17
+ const GOOGLE_CALENDAR_HOSTS = new Set(["calendar.google.com", "www.google.com"])
18
+ const GOOGLE_CALENDAR_SECRET_SCHEMA = z.object({
19
+ accessToken: z.string().min(1),
20
+ })
21
+
22
+ /**
23
+ * Creates the official Calendar client from a resolved Google integration
24
+ * secret.
25
+ *
26
+ * @param secret - Refreshed Google integration secret.
27
+ */
28
+ export function getGoogleCalendarApi(secret: Record<string, unknown>) {
29
+ const { accessToken } = GOOGLE_CALENDAR_SECRET_SCHEMA.parse(secret)
30
+ return calendar({
31
+ auth: new auth.OAuth2({
32
+ credentials: { access_token: accessToken },
33
+ }),
34
+ version: "v3",
35
+ })
36
+ }
37
+
38
+ /**
39
+ * Resolves an exact calendar title or ID to the provider's canonical ID.
40
+ *
41
+ * Unknown references are returned unchanged so email-address and otherwise
42
+ * unlisted calendar IDs remain usable.
43
+ *
44
+ * @param calendarApi - Authenticated Google Calendar client.
45
+ * @param reference - Calendar title or ID, defaulting to `primary`.
46
+ */
47
+ export async function resolveCalendarId(
48
+ calendarApi: calendar_v3.Calendar,
49
+ reference?: string,
50
+ ) {
51
+ return (await resolveCalendarIds(calendarApi, [reference]))[0]!
52
+ }
53
+
54
+ /**
55
+ * Resolves multiple exact calendar titles or IDs with one paginated listing.
56
+ *
57
+ * Unknown references are returned unchanged so email-address and otherwise
58
+ * unlisted calendar IDs remain usable.
59
+ *
60
+ * @param calendarApi - Authenticated Google Calendar client.
61
+ * @param references - Calendar titles or IDs, with absent values as primary.
62
+ */
63
+ export async function resolveCalendarIds(
64
+ calendarApi: calendar_v3.Calendar,
65
+ references: (string | undefined)[],
66
+ ) {
67
+ if (references.every((value) => value === undefined || value === "primary")) {
68
+ return references.map(() => "primary")
69
+ }
70
+
71
+ const calendars: calendar_v3.Schema$CalendarListEntry[] = []
72
+ let pageToken: string | undefined
73
+ do {
74
+ const { data } = await calendarApi.calendarList.list({
75
+ maxResults: 250,
76
+ pageToken,
77
+ showDeleted: false,
78
+ showHidden: true,
79
+ })
80
+ calendars.push(...(data.items ?? []))
81
+ pageToken = data.nextPageToken ?? undefined
82
+ } while (pageToken)
83
+
84
+ return references.map((reference) =>
85
+ resolveCalendarReference(calendars, reference),
86
+ )
87
+ }
88
+
89
+ /**
90
+ * Resolves one calendar reference against an already fetched calendar list.
91
+ *
92
+ * @param calendars - Visible Google Calendar list entries.
93
+ * @param reference - Calendar title or ID, defaulting to primary.
94
+ * @throws When the calendar title matches more than one visible calendar.
95
+ */
96
+ export function resolveCalendarReference(
97
+ calendars: calendar_v3.Schema$CalendarListEntry[],
98
+ reference?: string,
99
+ ) {
100
+ if (reference === undefined || reference === "primary") return "primary"
101
+
102
+ const idMatch = calendars.find(({ id }) => id === reference)
103
+ if (idMatch?.id) return idMatch.id
104
+
105
+ const normalizedReference = reference.trim().toLowerCase()
106
+ const nameMatches = calendars.filter(
107
+ ({ summary, summaryOverride }) =>
108
+ summary?.trim().toLowerCase() === normalizedReference ||
109
+ summaryOverride?.trim().toLowerCase() === normalizedReference,
110
+ )
111
+ if (nameMatches.length === 1 && nameMatches[0]!.id) {
112
+ return nameMatches[0]!.id
113
+ }
114
+ if (nameMatches.length > 1) {
115
+ throw new Error(
116
+ `Google Calendar "${reference}" is ambiguous; matching IDs: ${nameMatches.map(({ id }) => id).join(", ")}.`,
117
+ )
118
+ }
119
+ return reference
120
+ }
121
+
122
+ /**
123
+ * Accepts a raw event ID or a standard Google Calendar event URL.
124
+ *
125
+ * @param event - Event ID or URL.
126
+ * @throws When a URL is not a recognizable Google Calendar event URL.
127
+ */
128
+ export function normalizeEventId(event: string) {
129
+ const value = event.trim()
130
+ if (!URL.canParse(value)) return value
131
+
132
+ const url = new URL(value)
133
+ if (!GOOGLE_CALENDAR_HOSTS.has(url.hostname)) {
134
+ throw new Error(`Expected a Google Calendar URL, received "${value}".`)
135
+ }
136
+ const encodedEvent = url.searchParams.get("eid")
137
+ if (!encodedEvent) {
138
+ throw new Error(`Could not find an event ID in "${value}".`)
139
+ }
140
+ const [eventId] = Buffer.from(encodedEvent, "base64url")
141
+ .toString("utf8")
142
+ .split(" ")
143
+ if (!eventId) {
144
+ throw new Error(`Could not decode the event ID in "${value}".`)
145
+ }
146
+ return eventId
147
+ }
148
+
149
+ /**
150
+ * Converts an ergonomic timed or all-day schedule to Calendar API date-times.
151
+ *
152
+ * @param schedule - Timed or all-day event schedule.
153
+ */
154
+ export function toEventDateTimes(
155
+ schedule: z.infer<typeof EVENT_SCHEDULE_INPUT_SCHEMA>,
156
+ ) {
157
+ if ("startDate" in schedule) {
158
+ return {
159
+ end: {
160
+ date: schedule.endDate ?? addDays(schedule.startDate, 1),
161
+ },
162
+ start: { date: schedule.startDate },
163
+ }
164
+ }
165
+ return {
166
+ end: { dateTime: schedule.end, timeZone: schedule.timeZone },
167
+ start: { dateTime: schedule.start, timeZone: schedule.timeZone },
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Normalizes a provider event into the stable public Calendar event shape.
173
+ *
174
+ * @param event - Provider event.
175
+ * @param calendarId - Calendar from which the event was read.
176
+ * @throws When the provider omits the event ID.
177
+ */
178
+ export function normalizeCalendarEvent(
179
+ event: calendar_v3.Schema$Event,
180
+ calendarId: string,
181
+ ): z.infer<typeof CALENDAR_EVENT_SCHEMA> {
182
+ if (!event.id) {
183
+ throw new Error("Google Calendar returned an event without an ID.")
184
+ }
185
+ return {
186
+ attachments: (event.attachments ?? []).flatMap((attachment) =>
187
+ attachment.fileUrl
188
+ ? [
189
+ {
190
+ fileId: attachment.fileId ?? undefined,
191
+ fileUrl: attachment.fileUrl,
192
+ iconLink: attachment.iconLink ?? undefined,
193
+ mimeType: attachment.mimeType ?? undefined,
194
+ title: attachment.title ?? undefined,
195
+ },
196
+ ]
197
+ : [],
198
+ ),
199
+ attendees: (event.attendees ?? []).flatMap((attendee) =>
200
+ attendee.email
201
+ ? [
202
+ {
203
+ additionalGuests: attendee.additionalGuests ?? undefined,
204
+ comment: attendee.comment ?? undefined,
205
+ displayName: attendee.displayName ?? undefined,
206
+ email: attendee.email,
207
+ optional: attendee.optional ?? undefined,
208
+ organizer: attendee.organizer ?? false,
209
+ resource: attendee.resource ?? undefined,
210
+ responseStatus: normalizeResponseStatus(attendee.responseStatus),
211
+ self: attendee.self ?? false,
212
+ },
213
+ ]
214
+ : [],
215
+ ),
216
+ calendarId,
217
+ colorId: event.colorId ?? undefined,
218
+ conference: normalizeConference(event.conferenceData),
219
+ createdAt: event.created ?? undefined,
220
+ creator: normalizePrincipal(event.creator),
221
+ description: event.description ?? undefined,
222
+ end: event.end ? normalizeEventTime(event.end) : undefined,
223
+ extendedProperties: {
224
+ private: withoutNullValues(event.extendedProperties?.private),
225
+ shared: withoutNullValues(event.extendedProperties?.shared),
226
+ },
227
+ guestsCanInviteOthers: event.guestsCanInviteOthers ?? true,
228
+ guestsCanModify: event.guestsCanModify ?? false,
229
+ guestsCanSeeOtherGuests: event.guestsCanSeeOtherGuests ?? true,
230
+ hangoutLink: event.hangoutLink ?? undefined,
231
+ htmlLink: event.htmlLink ?? undefined,
232
+ id: event.id,
233
+ iCalUID: event.iCalUID ?? undefined,
234
+ location: event.location ?? undefined,
235
+ originalStart: event.originalStartTime
236
+ ? normalizeEventTime(event.originalStartTime)
237
+ : undefined,
238
+ organizer: normalizePrincipal(event.organizer),
239
+ recurrence: event.recurrence ?? [],
240
+ recurringEventId: event.recurringEventId ?? undefined,
241
+ reminders: {
242
+ overrides: (event.reminders?.overrides ?? []).flatMap((reminder) =>
243
+ reminder.method && reminder.minutes != null
244
+ ? [
245
+ {
246
+ method: normalizeReminderMethod(reminder.method),
247
+ minutes: reminder.minutes,
248
+ },
249
+ ]
250
+ : [],
251
+ ),
252
+ useDefault: event.reminders?.useDefault ?? true,
253
+ },
254
+ sequence: event.sequence ?? 0,
255
+ start: event.start ? normalizeEventTime(event.start) : undefined,
256
+ status: normalizeEventStatus(event.status),
257
+ summary: event.summary ?? "",
258
+ transparency:
259
+ event.transparency === "transparent" ? "transparent" : "opaque",
260
+ type: event.eventType ?? "default",
261
+ updatedAt: event.updated ?? undefined,
262
+ visibility: normalizeVisibility(event.visibility),
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Normalizes a provider event page while retaining pagination and sync tokens.
268
+ *
269
+ * @param events - Provider event collection.
270
+ * @param calendarId - Calendar from which events were read.
271
+ */
272
+ export function normalizeEventPage(
273
+ events: calendar_v3.Schema$Events,
274
+ calendarId: string,
275
+ ): z.infer<typeof EVENT_PAGE_SCHEMA> {
276
+ return {
277
+ events: (events.items ?? []).map((event) =>
278
+ normalizeCalendarEvent(event, calendarId),
279
+ ),
280
+ nextPageToken: events.nextPageToken ?? undefined,
281
+ nextSyncToken: events.nextSyncToken ?? undefined,
282
+ timeZone: events.timeZone ?? undefined,
283
+ updatedAt: events.updated ?? undefined,
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Normalizes a calendar or calendar-list entry.
289
+ *
290
+ * @param value - Provider calendar metadata.
291
+ * @param id - Calendar ID when not included in the provider resource.
292
+ * @throws When neither the resource nor caller supplies a calendar ID.
293
+ */
294
+ export function normalizeCalendar(
295
+ value: calendar_v3.Schema$Calendar | calendar_v3.Schema$CalendarListEntry,
296
+ id?: string,
297
+ ): z.infer<typeof CALENDAR_SCHEMA> {
298
+ const calendarId = value.id ?? id
299
+ if (!calendarId) {
300
+ throw new Error("Google Calendar returned calendar metadata without an ID.")
301
+ }
302
+ const listEntry = "accessRole" in value ? value : undefined
303
+ return {
304
+ accessRole: listEntry?.accessRole ?? undefined,
305
+ autoAcceptInvitations: value.autoAcceptInvitations ?? false,
306
+ backgroundColor: listEntry?.backgroundColor ?? undefined,
307
+ colorId: listEntry?.colorId ?? undefined,
308
+ conferenceSolutionTypes:
309
+ value.conferenceProperties?.allowedConferenceSolutionTypes ?? [],
310
+ defaultReminders:
311
+ listEntry?.defaultReminders?.flatMap((reminder) =>
312
+ reminder.method && reminder.minutes != null
313
+ ? [{ method: reminder.method, minutes: reminder.minutes }]
314
+ : [],
315
+ ) ?? [],
316
+ dataOwner: value.dataOwner ?? undefined,
317
+ deleted: listEntry?.deleted ?? false,
318
+ description: value.description ?? undefined,
319
+ foregroundColor: listEntry?.foregroundColor ?? undefined,
320
+ hidden: listEntry?.hidden ?? false,
321
+ id: calendarId,
322
+ location: value.location ?? undefined,
323
+ notifications:
324
+ listEntry?.notificationSettings?.notifications?.flatMap((notification) =>
325
+ notification.type &&
326
+ (notification.type === "eventCreation" ||
327
+ notification.type === "eventChange" ||
328
+ notification.type === "eventCancellation" ||
329
+ notification.type === "eventResponse" ||
330
+ notification.type === "agenda")
331
+ ? [{ type: notification.type }]
332
+ : [],
333
+ ) ?? [],
334
+ primary: listEntry?.primary ?? id === "primary",
335
+ selected: listEntry?.selected ?? false,
336
+ summary: value.summary ?? "",
337
+ summaryOverride: listEntry?.summaryOverride ?? undefined,
338
+ timeZone: value.timeZone ?? undefined,
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Normalizes a provider calendar-list page.
344
+ *
345
+ * @param value - Provider calendar-list collection.
346
+ */
347
+ export function normalizeCalendarPage(
348
+ value: calendar_v3.Schema$CalendarList,
349
+ ): z.infer<typeof CALENDAR_PAGE_SCHEMA> {
350
+ return {
351
+ calendars: (value.items ?? []).map((entry) => normalizeCalendar(entry)),
352
+ nextPageToken: value.nextPageToken ?? undefined,
353
+ nextSyncToken: value.nextSyncToken ?? undefined,
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Normalizes a provider free/busy response.
359
+ *
360
+ * @param value - Provider free/busy response.
361
+ * @throws When the provider omits the requested time range.
362
+ */
363
+ export function normalizeFreeBusy(
364
+ value: calendar_v3.Schema$FreeBusyResponse,
365
+ ): z.infer<typeof FREE_BUSY_SCHEMA> {
366
+ if (!value.timeMin || !value.timeMax) {
367
+ throw new Error("Google Calendar returned an incomplete free/busy range.")
368
+ }
369
+ return {
370
+ calendars: Object.entries(value.calendars ?? {}).map(
371
+ ([calendarId, result]) => ({
372
+ busy: (result.busy ?? []).flatMap((period) =>
373
+ period.start && period.end
374
+ ? [{ end: period.end, start: period.start }]
375
+ : [],
376
+ ),
377
+ calendarId,
378
+ errors: (result.errors ?? []).map((error) => ({
379
+ domain: error.domain ?? undefined,
380
+ reason: error.reason ?? undefined,
381
+ })),
382
+ }),
383
+ ),
384
+ timeMax: value.timeMax,
385
+ timeMin: value.timeMin,
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Normalizes Google Calendar color definitions.
391
+ *
392
+ * @param value - Provider color palette.
393
+ */
394
+ export function normalizeColors(
395
+ value: calendar_v3.Schema$Colors,
396
+ ): z.infer<typeof COLOR_PALETTE_SCHEMA> {
397
+ return {
398
+ calendars: normalizeColorDefinitions(value.calendar),
399
+ events: normalizeColorDefinitions(value.event),
400
+ updatedAt: value.updated ?? undefined,
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Normalizes one calendar ACL rule.
406
+ *
407
+ * @param value - Provider ACL rule.
408
+ * @throws When the provider omits the rule ID or scope.
409
+ */
410
+ export function normalizeAclRule(
411
+ value: calendar_v3.Schema$AclRule,
412
+ ): z.infer<typeof ACL_RULE_SCHEMA> {
413
+ if (!value.id || !value.scope?.type) {
414
+ throw new Error("Google Calendar returned an incomplete ACL rule.")
415
+ }
416
+ return {
417
+ id: value.id,
418
+ role: normalizeAclRole(value.role),
419
+ scope: {
420
+ type: normalizeAclScope(value.scope.type),
421
+ value: value.scope.value ?? undefined,
422
+ },
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Normalizes a page of calendar ACL rules.
428
+ *
429
+ * @param value - Provider ACL collection.
430
+ */
431
+ export function normalizeAclPage(
432
+ value: calendar_v3.Schema$Acl,
433
+ ): z.infer<typeof ACL_PAGE_SCHEMA> {
434
+ return {
435
+ nextPageToken: value.nextPageToken ?? undefined,
436
+ nextSyncToken: value.nextSyncToken ?? undefined,
437
+ rules: (value.items ?? []).map(normalizeAclRule),
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Finds common free windows of a requested duration.
443
+ *
444
+ * @param freeBusy - Normalized free/busy response.
445
+ * @param durationMinutes - Required duration.
446
+ * @param stepMinutes - Candidate-start increment.
447
+ * @param limit - Maximum returned slots.
448
+ */
449
+ export function findAvailableSlots(
450
+ freeBusy: z.infer<typeof FREE_BUSY_SCHEMA>,
451
+ durationMinutes: number,
452
+ stepMinutes: number,
453
+ limit: number,
454
+ ) {
455
+ const queryEnd = new Date(freeBusy.timeMax).getTime()
456
+ const duration = durationMinutes * 60_000
457
+ const step = stepMinutes * 60_000
458
+ const busy = mergePeriods(
459
+ freeBusy.calendars.flatMap(({ busy: periods }) =>
460
+ periods.map(({ end, start }) => ({
461
+ end: new Date(end).getTime(),
462
+ start: new Date(start).getTime(),
463
+ })),
464
+ ),
465
+ )
466
+ const slots: { durationMinutes: number; end: string; start: string }[] = []
467
+
468
+ for (
469
+ let candidate = new Date(freeBusy.timeMin).getTime();
470
+ candidate + duration <= queryEnd && slots.length < limit;
471
+ candidate += step
472
+ ) {
473
+ const candidateEnd = candidate + duration
474
+ if (
475
+ !busy.some(
476
+ (period) => candidate < period.end && candidateEnd > period.start,
477
+ )
478
+ ) {
479
+ slots.push({
480
+ durationMinutes,
481
+ end: new Date(candidateEnd).toISOString(),
482
+ start: new Date(candidate).toISOString(),
483
+ })
484
+ }
485
+ }
486
+ return slots
487
+ }
488
+
489
+ /**
490
+ * Builds the provider event body shared by create, update, and import actions.
491
+ *
492
+ * @param input - Public event fields.
493
+ */
494
+ export function toEventResource(
495
+ input: z.infer<typeof EVENT_DETAILS_INPUT_SCHEMA>,
496
+ ): calendar_v3.Schema$Event {
497
+ return {
498
+ attachments: input.attachments,
499
+ attendees: input.attendees,
500
+ colorId: input.colorId,
501
+ description: input.description,
502
+ ...(input.schedule ? toEventDateTimes(input.schedule) : {}),
503
+ extendedProperties: input.extendedProperties,
504
+ guestsCanInviteOthers: input.guestsCanInviteOthers,
505
+ guestsCanModify: input.guestsCanModify,
506
+ guestsCanSeeOtherGuests: input.guestsCanSeeOtherGuests,
507
+ location: input.location,
508
+ recurrence: input.recurrence,
509
+ reminders: input.reminders,
510
+ summary: input.summary,
511
+ transparency: input.transparency,
512
+ visibility: input.visibility,
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Adds whole UTC days to an ISO calendar date.
518
+ *
519
+ * @param date - ISO calendar date.
520
+ * @param days - Number of days to add.
521
+ */
522
+ function addDays(date: string, days: number) {
523
+ const value = new Date(`${date}T00:00:00.000Z`)
524
+ value.setUTCDate(value.getUTCDate() + days)
525
+ return value.toISOString().slice(0, 10)
526
+ }
527
+
528
+ /**
529
+ * Normalizes an all-day or timed provider event boundary.
530
+ *
531
+ * @param value - Provider event date-time.
532
+ */
533
+ function normalizeEventTime(value: calendar_v3.Schema$EventDateTime) {
534
+ return {
535
+ date: value.date ?? undefined,
536
+ dateTime: value.dateTime ?? undefined,
537
+ timeZone: value.timeZone ?? undefined,
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Normalizes an event creator or organizer.
543
+ *
544
+ * @param value - Provider event principal.
545
+ */
546
+ function normalizePrincipal(value: calendar_v3.Schema$Event["creator"]) {
547
+ return value
548
+ ? {
549
+ displayName: value.displayName ?? undefined,
550
+ email: value.email ?? undefined,
551
+ self: value.self ?? false,
552
+ }
553
+ : undefined
554
+ }
555
+
556
+ /**
557
+ * Normalizes event conference entry points and creation state.
558
+ *
559
+ * @param value - Provider conference data.
560
+ */
561
+ function normalizeConference(
562
+ value: calendar_v3.Schema$ConferenceData | null | undefined,
563
+ ) {
564
+ if (!value) return undefined
565
+ return {
566
+ conferenceId: value.conferenceId ?? undefined,
567
+ creationStatus: normalizeConferenceStatus(
568
+ value.createRequest?.status?.statusCode,
569
+ ),
570
+ entryPoints: (value.entryPoints ?? []).flatMap((entryPoint) =>
571
+ entryPoint.entryPointType && entryPoint.uri
572
+ ? [
573
+ {
574
+ label: entryPoint.label ?? undefined,
575
+ passcode:
576
+ entryPoint.passcode ??
577
+ entryPoint.password ??
578
+ entryPoint.pin ??
579
+ undefined,
580
+ type: entryPoint.entryPointType,
581
+ uri: entryPoint.uri,
582
+ },
583
+ ]
584
+ : [],
585
+ ),
586
+ name: value.conferenceSolution?.name ?? undefined,
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Narrows a provider conference creation status.
592
+ *
593
+ * @param value - Provider status.
594
+ */
595
+ function normalizeConferenceStatus(
596
+ value: string | null | undefined,
597
+ ): "pending" | "success" | "failure" | undefined {
598
+ return value === "pending" || value === "success" || value === "failure"
599
+ ? value
600
+ : undefined
601
+ }
602
+
603
+ /**
604
+ * Narrows an attendee response status.
605
+ *
606
+ * @param value - Provider response status.
607
+ */
608
+ function normalizeResponseStatus(value: string | null | undefined) {
609
+ return value === "declined" || value === "tentative" || value === "accepted"
610
+ ? value
611
+ : "needsAction"
612
+ }
613
+
614
+ /**
615
+ * Narrows a reminder delivery method.
616
+ *
617
+ * @param value - Provider reminder method.
618
+ */
619
+ function normalizeReminderMethod(value: string) {
620
+ return value === "email" ? "email" : "popup"
621
+ }
622
+
623
+ /**
624
+ * Narrows an event lifecycle status.
625
+ *
626
+ * @param value - Provider event status.
627
+ */
628
+ function normalizeEventStatus(value: string | null | undefined) {
629
+ return value === "tentative" || value === "cancelled" ? value : "confirmed"
630
+ }
631
+
632
+ /**
633
+ * Narrows an event visibility.
634
+ *
635
+ * @param value - Provider visibility.
636
+ */
637
+ function normalizeVisibility(value: string | null | undefined) {
638
+ return value === "public" || value === "private" || value === "confidential"
639
+ ? value
640
+ : "default"
641
+ }
642
+
643
+ /**
644
+ * Narrows an ACL permission role.
645
+ *
646
+ * @param value - Provider ACL role.
647
+ */
648
+ function normalizeAclRole(value: string | null | undefined) {
649
+ return value === "none" ||
650
+ value === "freeBusyReader" ||
651
+ value === "reader" ||
652
+ value === "writer"
653
+ ? value
654
+ : "owner"
655
+ }
656
+
657
+ /**
658
+ * Narrows an ACL scope type.
659
+ *
660
+ * @param value - Provider ACL scope type.
661
+ */
662
+ function normalizeAclScope(value: string) {
663
+ return value === "default" || value === "group" || value === "domain"
664
+ ? value
665
+ : "user"
666
+ }
667
+
668
+ /**
669
+ * Removes null values from a provider string record.
670
+ *
671
+ * @param value - Nullable provider record.
672
+ */
673
+ function withoutNullValues(
674
+ value: Record<string, string | null> | null | undefined,
675
+ ) {
676
+ return Object.fromEntries(
677
+ Object.entries(value ?? {}).flatMap(([key, item]) =>
678
+ item === null ? [] : [[key, item]],
679
+ ),
680
+ )
681
+ }
682
+
683
+ /**
684
+ * Normalizes complete color definitions keyed by provider ID.
685
+ *
686
+ * @param definitions - Provider color definitions.
687
+ */
688
+ function normalizeColorDefinitions(
689
+ definitions:
690
+ | Record<string, calendar_v3.Schema$ColorDefinition>
691
+ | null
692
+ | undefined,
693
+ ) {
694
+ return Object.fromEntries(
695
+ Object.entries(definitions ?? {}).flatMap(([id, definition]) =>
696
+ definition.background && definition.foreground
697
+ ? [
698
+ [
699
+ id,
700
+ {
701
+ background: definition.background,
702
+ foreground: definition.foreground,
703
+ },
704
+ ],
705
+ ]
706
+ : [],
707
+ ),
708
+ )
709
+ }
710
+
711
+ /**
712
+ * Merges overlapping numeric time periods.
713
+ *
714
+ * @param periods - Busy periods expressed as epoch milliseconds.
715
+ */
716
+ function mergePeriods(periods: { end: number; start: number }[]) {
717
+ return periods
718
+ .toSorted((left, right) => left.start - right.start)
719
+ .reduce<{ end: number; start: number }[]>((merged, period) => {
720
+ const previous = merged.at(-1)
721
+ if (!previous || period.start > previous.end) return [...merged, period]
722
+ return [
723
+ ...merged.slice(0, -1),
724
+ { end: Math.max(previous.end, period.end), start: previous.start },
725
+ ]
726
+ }, [])
727
+ }