diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts
index 1b3be084..830fb594 100644
--- a/apps/web/next-env.d.ts
+++ b/apps/web/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+///
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/bun.lock b/bun.lock
index 39dfe2ec..8d028fc3 100644
--- a/bun.lock
+++ b/bun.lock
@@ -225,7 +225,7 @@
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"@types/node": "^22.9.0",
- "typescript": "^5.8.3",
+ "typescript": "^5.9.2",
},
},
"packages/google-tasks": {
diff --git a/packages/api/src/providers/calendars/google-calendar.ts b/packages/api/src/providers/calendars/google-calendar.ts
index 38ca56c0..a938eec4 100644
--- a/packages/api/src/providers/calendars/google-calendar.ts
+++ b/packages/api/src/providers/calendars/google-calendar.ts
@@ -99,42 +99,61 @@ export class GoogleCalendarProvider implements CalendarProvider {
events: CalendarEvent[];
recurringMasterEvents: CalendarEvent[];
}> {
- return this.withErrorHandler("events", async () => {
- const { items } = await this.client.calendars.events.list(calendar.id, {
- timeMin: timeMin.withTimeZone("UTC").toInstant().toString(),
- timeMax: timeMax.withTimeZone("UTC").toInstant().toString(),
- singleEvents: CALENDAR_DEFAULTS.SINGLE_EVENTS,
- orderBy: CALENDAR_DEFAULTS.ORDER_BY,
- maxResults: CALENDAR_DEFAULTS.MAX_EVENTS_PER_CALENDAR,
- });
+ return this.withErrorHandler(
+ "events",
+ async () => {
+ // Validate time range to prevent empty time range errors
+ const timeMinInstant = timeMin.withTimeZone("UTC").toInstant();
+ const timeMaxInstant = timeMax.withTimeZone("UTC").toInstant();
+
+ if (Temporal.Instant.compare(timeMinInstant, timeMaxInstant) >= 0) {
+ throw new Error(
+ `Invalid time range: timeMax (${timeMaxInstant}) must be after timeMin (${timeMinInstant})`,
+ );
+ }
- const events: CalendarEvent[] =
- items?.map((event) =>
- parseGoogleCalendarEvent({
- calendar,
- accountId: this.accountId,
- event,
- defaultTimeZone: timeZone ?? "UTC",
- }),
- ) ?? [];
-
- const instances = events.filter((e) => e.recurringEventId);
- const masters = new Set([]);
-
- for (const instance of instances) {
- masters.add(instance.recurringEventId!);
- }
+ const { items } = await this.client.calendars.events.list(calendar.id, {
+ timeMin: timeMinInstant.toString(),
+ timeMax: timeMaxInstant.toString(),
+ singleEvents: CALENDAR_DEFAULTS.SINGLE_EVENTS,
+ orderBy: CALENDAR_DEFAULTS.ORDER_BY,
+ maxResults: CALENDAR_DEFAULTS.MAX_EVENTS_PER_CALENDAR,
+ });
- if (masters.size === 0) {
- return { events, recurringMasterEvents: [] };
- }
+ const events: CalendarEvent[] =
+ items?.map((event) =>
+ parseGoogleCalendarEvent({
+ calendar,
+ accountId: this.accountId,
+ event,
+ defaultTimeZone: timeZone ?? "UTC",
+ }),
+ ) ?? [];
+
+ const instances = events.filter((e) => e.recurringEventId);
+ const masters = new Set([]);
+
+ for (const instance of instances) {
+ masters.add(instance.recurringEventId!);
+ }
- const recurringMasterEvents = await Promise.all(
- Array.from(masters).map((id) => this.event(calendar, id, timeZone)),
- );
+ if (masters.size === 0) {
+ return { events, recurringMasterEvents: [] };
+ }
- return { events, recurringMasterEvents };
- });
+ const recurringMasterEvents = await Promise.all(
+ Array.from(masters).map((id) => this.event(calendar, id, timeZone)),
+ );
+
+ return { events, recurringMasterEvents };
+ },
+ {
+ calendarId: calendar.id,
+ timeMin: timeMin.toString(),
+ timeMax: timeMax.toString(),
+ timeZone,
+ },
+ );
}
async event(
@@ -349,16 +368,34 @@ export class GoogleCalendarProvider implements CalendarProvider {
timeMin: Temporal.ZonedDateTime,
timeMax: Temporal.ZonedDateTime,
): Promise {
- return this.withErrorHandler("freeBusy", async () => {
- const response = await this.client.checkFreeBusy.checkFreeBusy({
- timeMin: timeMin.withTimeZone("UTC").toInstant().toString(),
- timeMax: timeMax.withTimeZone("UTC").toInstant().toString(),
- timeZone: "UTC",
- items: schedules.map((id) => ({ id })),
- });
+ return this.withErrorHandler(
+ "freeBusy",
+ async () => {
+ // Validate time range to prevent empty time range errors
+ const timeMinInstant = timeMin.withTimeZone("UTC").toInstant();
+ const timeMaxInstant = timeMax.withTimeZone("UTC").toInstant();
+
+ if (Temporal.Instant.compare(timeMinInstant, timeMaxInstant) >= 0) {
+ throw new Error(
+ `Invalid time range: timeMax (${timeMaxInstant}) must be after timeMin (${timeMinInstant})`,
+ );
+ }
- return parseGoogleCalendarFreeBusy(response);
- });
+ const response = await this.client.checkFreeBusy.checkFreeBusy({
+ timeMin: timeMinInstant.toString(),
+ timeMax: timeMaxInstant.toString(),
+ timeZone: "UTC",
+ items: schedules.map((id) => ({ id })),
+ });
+
+ return parseGoogleCalendarFreeBusy(response);
+ },
+ {
+ schedules,
+ timeMin: timeMin.toString(),
+ timeMax: timeMax.toString(),
+ },
+ );
}
private async withErrorHandler(
@@ -369,7 +406,28 @@ export class GoogleCalendarProvider implements CalendarProvider {
try {
return await Promise.resolve(fn());
} catch (error: unknown) {
- console.error(`Failed to ${operation}:`, error);
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+
+ // Enhanced logging for time range errors
+ if (
+ errorMessage.includes("timeRangeEmpty") ||
+ errorMessage.includes("Invalid time range")
+ ) {
+ console.error(`Time range validation failed in ${operation}:`, {
+ error: errorMessage,
+ context,
+ operation,
+ accountId: this.accountId,
+ });
+ } else {
+ console.error(`Failed to ${operation}:`, {
+ error: errorMessage,
+ context,
+ operation,
+ accountId: this.accountId,
+ });
+ }
throw new ProviderError(error as Error, operation, context);
}