Skip to content

Comments

Add Microsoft Calendar support#978

Closed
elie222 wants to merge 14 commits intomainfrom
feat/microsoft-calendar
Closed

Add Microsoft Calendar support#978
elie222 wants to merge 14 commits intomainfrom
feat/microsoft-calendar

Conversation

@elie222
Copy link
Owner

@elie222 elie222 commented Nov 16, 2025

Summary by CodeRabbit

  • New Features

    • Added Outlook (Microsoft) calendar support and a unified calendar availability across Google + Outlook
    • New UI to connect Google or Outlook calendars and manage connections independently
  • Bug Fixes

    • OAuth redirects now use a consistent environment-based base URL
  • Documentation

    • Updated README with Microsoft Calendar setup and clarified independent calendar OAuth flows

@vercel
Copy link

vercel bot commented Nov 16, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
inbox-zero Error Error Nov 16, 2025 3:40pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 16, 2025

Walkthrough

Adds Microsoft (Outlook) calendar support alongside Google: new OAuth routes, provider implementations, unified availability aggregation, shared callback/helpers, UI updates for multi-provider connection, and migration of redirect base URLs to env.NEXT_PUBLIC_BASE_URL.

Changes

Cohort / File(s) Summary
Docs & Versioning
README.md, package.json, version.txt
README updated with Microsoft Calendar registration and OAuth steps; bumped version and updated dev tooling deps.
Calendar UI
apps/web/app/(app)/[emailAccountId]/calendars/page.tsx, apps/web/app/(app)/[emailAccountId]/calendars/CalendarConnections.tsx, apps/web/app/(app)/[emailAccountId]/calendars/CalendarConnectionCard.tsx, apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx, apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendarButton.tsx
Replaced single-provider connect button with a dual-provider ConnectCalendar component; ConnectionCard now renders provider-specific icon/text; empty-state messaging updated; removed old ConnectCalendarButton.
OAuth Routes — Google & Outlook (calendar)
apps/web/app/api/google/calendar/callback/route.ts, apps/web/app/api/outlook/calendar/callback/route.ts, apps/web/app/api/outlook/calendar/auth-url/route.ts
Google callback now delegates to shared handler; new Outlook auth-url and callback routes added, using state cookies and unified callback processing.
Linking / MCP callbacks base URL migration
apps/web/app/api/google/linking/callback/route.ts, apps/web/app/api/outlook/linking/callback/route.ts, apps/web/app/api/mcp/[integration]/callback/route.ts
Replaced request-origin-based redirect construction with env.NEXT_PUBLIC_BASE_URL and removed baseUrl parameter where previously passed.
OAuth helper infrastructure
apps/web/utils/calendar/oauth-types.ts, apps/web/utils/calendar/oauth-callback-helpers.ts, apps/web/utils/calendar/handle-calendar-callback.ts
New calendar OAuth types, state/validation helpers, RedirectError, and a unified handleCalendarCallback that orchestrates token exchange, verification, connection creation, and calendar sync via provider abstraction.
Providers — OAuth & Availability
apps/web/utils/calendar/providers/google.ts, apps/web/utils/calendar/providers/microsoft.ts, apps/web/utils/calendar/providers/google-availability.ts, apps/web/utils/calendar/providers/microsoft-availability.ts
Added Google and Microsoft CalendarOAuthProvider implementations (token exchange, syncCalendars) and provider-specific availability fetchers implementing CalendarAvailabilityProvider.
Availability — unify & types
apps/web/utils/calendar/availability-types.ts, apps/web/utils/calendar/unified-availability.ts, apps/web/utils/calendar/availability.ts
Introduced BusyPeriod type and CalendarAvailabilityProvider interface; added getUnifiedCalendarAvailability aggregating busy periods across providers; removed legacy Google-only availability module.
AI tool & usage
apps/web/utils/ai/calendar/availability.ts
Switched tool to call getUnifiedCalendarAvailability and updated description to reflect cross-provider scope; added consolidated error handling.
Outlook client & scopes
apps/web/utils/outlook/calendar-client.ts, apps/web/utils/outlook/scopes.ts
New Outlook calendar client with token refresh, OAuth URL builder, calendar fetcher; exported CALENDAR_SCOPES for calendar permissions.
OAuth utilities
apps/web/utils/oauth/account-linking.ts, apps/web/utils/oauth/callback-validation.ts
Removed baseUrl parameter from interfaces/functions and migrated to env.NEXT_PUBLIC_BASE_URL for redirect construction.
E2E Tests
apps/web/__tests__/e2e/calendar/google-calendar.test.ts, apps/web/__tests__/e2e/calendar/microsoft-calendar.test.ts
Added gated end-to-end tests for Google and Microsoft availability providers (require env and test gating).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant UI as Client UI
    participant API as /api/outlook/calendar/auth-url
    participant OAuth as Microsoft OAuth
    participant Callback as /api/outlook/calendar/callback
    participant Handler as handleCalendarCallback
    participant Provider as microsoftCalendarProvider
    participant DB as Database

    User->>UI: Click "Add Outlook Calendar"
    UI->>API: GET /api/outlook/calendar/auth-url (scoped)
    API->>API: create state cookie (emailAccountId, nonce)
    API-->>UI: { url }
    UI->>OAuth: Redirect to provider URL
    OAuth->>Callback: Redirect with code & state
    Callback->>Handler: Delegate(request, microsoftCalendarProvider, logger)
    Handler->>Handler: validate callback & parse state
    Handler->>DB: verify email account ownership
    Handler->>Provider: exchangeCodeForTokens(code)
    Provider-->>Handler: tokens + email
    Handler->>DB: check/create calendarConnection
    Handler->>Provider: syncCalendars(connectionId, tokens...)
    Provider->>DB: upsert calendars
    Handler-->>UI: Redirect to /calendars?connected=1
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing focused review:
    • handleCalendarCallback and oauth-callback-helpers: redirect semantics, cookie/header propagation, RedirectError handling.
    • microsoftCalendarProvider & googleCalendarProvider: token exchange, refresh handling, required scope checks, and error paths.
    • unified availability aggregation: timezone boundary calculations and normalization across provider responses.
    • Removal of legacy availability module: ensure no residual imports or callsites expect old API.
    • Base-URL migration: verify no regressions for environments relying on dynamic origin.

Possibly related PRs

  • Release calendar feature #835 —Touches calendar UI/components and connection flow; likely overlaps with multi-provider UI changes.
  • Timezone calendar #812 —Related availability logic refactor; intersects with unified availability and timezone handling.
  • Update migrations #638 —Changes redirect/base-URL handling for OAuth flows; related to migration to NEXT_PUBLIC_BASE_URL.

Suggested reviewers

  • anakarentorosserrano-star

"I hopped through code with nimble paws,
Google and Outlook, now sharing the laws.
States, tokens, and calendars all in a row,
Busy periods merged — watch my tail glow! 🐇📅"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Microsoft Calendar support' clearly and concisely summarizes the main change: introducing Microsoft Calendar integration alongside existing Google Calendar support.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/microsoft-calendar

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (13)
apps/web/app/(app)/[emailAccountId]/calendars/CalendarConnectionCard.tsx (1)

31-46: Consider logging unrecognized providers.

The fallback to Google for unrecognized providers provides graceful degradation, but silently defaulting could mask configuration issues or bugs if an unexpected provider value appears in the database.

Consider adding a console warning:

 const getProviderInfo = (provider: string) => {
   const providers = {
     microsoft: {
       name: "Microsoft Calendar",
       icon: "/images/product/outlook-calendar.svg",
       alt: "Microsoft Calendar",
     },
     google: {
       name: "Google Calendar",
       icon: "/images/product/google-calendar.svg",
       alt: "Google Calendar",
     },
   };
 
+  if (!(provider in providers)) {
+    console.warn(`Unknown calendar provider: ${provider}, falling back to Google`);
+  }
+
   return providers[provider as keyof typeof providers] || providers.google;
 };
apps/web/utils/calendar/availability-types.ts (1)

12-20: Consider simplifying the accessToken type.

The accessToken parameter is both optional (?) and nullable (| null), which is redundant. Consider using just accessToken: string | null to simplify the type signature, unless there's a semantic distinction between undefined and null in your usage.

   fetchBusyPeriods(params: {
-    accessToken?: string | null;
+    accessToken: string | null;
     refreshToken: string | null;
     expiresAt: number | null;
     emailAccountId: string;
     calendarIds: string[];
     timeMin: string;
     timeMax: string;
   }): Promise<BusyPeriod[]>;
apps/web/utils/ai/calendar/availability.ts (1)

112-142: Validate tool date inputs before calling unified availability

execute relies on new Date(timeMin/timeMax) from unconstrained string inputs, which can yield Invalid Date or timezone‑dependent results if the model sends non‑ISO or ambiguous values. Consider tightening the inputSchema (e.g., ISO 8601 with z.string().datetime()) or explicitly validating and short‑circuiting on invalid dates before calling getUnifiedCalendarAvailability, returning an empty result or an error shape instead.

apps/web/utils/calendar/unified-availability.ts (2)

25-31: Confirm widening the requested window to full days is intentional

startDate/endDate are expanded to startOfDay/endOfDay in the user’s timezone before querying providers. This effectively turns any narrower caller window into whole‑day (or multi‑day) availability checks. If upstream callers intend to respect more precise time ranges, consider either:

  • Honoring the exact startDate/endDate without day expansion, or
  • Renaming the parameters / documenting that this API always works on full days so callers pass only dates.

124-136: Optionally sort (and later dedupe) unified busy periods for downstream consumers

The merged allBusyPeriods = results.flat() is returned in provider/connection iteration order. For easier downstream handling (especially UI or LLM tools), consider sorting by start (and, if needed, merging or deduping overlapping periods) before returning.

apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx (1)

7-8: Centralize GetCalendarAuthUrlResponse instead of importing from a specific route

This client component uses GetCalendarAuthUrlResponse for both Google and Outlook but imports the type from the Google route module. To avoid coupling client code to a specific route file (and to keep Google/Outlook in sync), consider moving this shared response type to a common calendar types module and importing it from there in both routes and ConnectCalendar.

apps/web/utils/calendar/handle-calendar-callback.ts (1)

117-136: Preserve granular error codes from RedirectError instead of always forcing connection_failed.

RedirectError instances are thrown from helpers that already set specific error query params on redirectUrl (missing_code, invalid_state, unauthorized, etc.), but the catch block always overwrites them with "connection_failed". This can reduce debuggability and surface less useful messages to the UI.

Consider reusing any existing error query param when handling RedirectError, falling back to "connection_failed" only if none is present:

-    if (error instanceof RedirectError) {
-      return redirectWithError(
-        error.redirectUrl,
-        "connection_failed",
-        error.responseHeaders,
-      );
-    }
+    if (error instanceof RedirectError) {
+      const existingError =
+        error.redirectUrl.searchParams.get("error") ?? "connection_failed";
+      return redirectWithError(
+        error.redirectUrl,
+        existingError,
+        error.responseHeaders,
+      );
+    }

This keeps the shared handler generic while preserving the more specific semantics encoded by the lower-level helpers.

apps/web/utils/calendar/providers/microsoft-availability.ts (1)

11-61: Verify that calendarIds map correctly to Microsoft Graph schedules identifiers.

fetchMicrosoftCalendarBusyPeriods calls:

await calendarClient.api("/me/calendar/getSchedule").post({
  schedules: calendarIds,
  ...
});

The Graph getSchedule API typically expects mailbox identifiers (often email addresses or user IDs) in schedules. Upstream, these are named calendarIds, which might instead be calendar resource IDs.

Please double‑check that the values passed in calendarIds are valid schedules identifiers for Graph (e.g., mailbox addresses). If they are mailbox addresses, consider renaming to make that intent clearer; if they are calendar IDs, you may need to adapt the call or mapping.

apps/web/utils/calendar/providers/microsoft.ts (1)

20-77: Add a defensive check for tokens.access_token before using it.

After exchanging the authorization code, the code assumes tokens.access_token is present and immediately uses it to call https://graph.microsoft.com/v1.0/me. While this is expected in normal responses, a malformed or partial token response would result in an Authorization header with Bearer undefined, leading to a harder‑to‑diagnose failure.

A small guard would make this more robust and yield a clearer error:

-    const tokens = await tokenResponse.json();
-
-    if (!tokenResponse.ok) {
-      throw new Error(
-        tokens.error_description || "Failed to exchange code for tokens",
-      );
-    }
+    const tokens = await tokenResponse.json();
+
+    if (!tokenResponse.ok) {
+      throw new Error(
+        tokens.error_description || "Failed to exchange code for tokens",
+      );
+    }
+
+    if (!tokens.access_token) {
+      throw new Error("Token response missing access_token");
+    }

This keeps failures in the token step clearly separated from Graph profile issues.

apps/web/utils/outlook/calendar-client.ts (1)

96-113: Clarify or tighten the lookup used when saving refreshed tokens.

In getCalendarClientWithRefresh, the refreshed tokens are persisted to the first calendarConnection found by:

const calendarConnection = await prisma.calendarConnection.findFirst({
  where: {
    emailAccountId,
    provider: "microsoft",
  },
  select: { id: true },
});

This assumes there is at most one Microsoft calendar connection per emailAccountId. If that assumption ever changes (e.g., multiple Microsoft connections per email account), this would update an arbitrary one.

Either:

  • Document this 1:1 assumption in a short comment, or
  • Tighten the lookup (e.g., use a unique constraint and findUnique, or include additional fields such as email) so the target connection is unambiguous.

This will make the refresh behavior more future‑proof and easier to reason about.

apps/web/utils/calendar/oauth-callback-helpers.ts (3)

16-20: Consider extracting validation bounds as named constants.

The validation bounds (max 64 for emailAccountId, min 8/max 128 for nonce) are magic numbers. Consider extracting these as named constants to improve maintainability and ensure consistency with database constraints.

const EMAIL_ACCOUNT_ID_MAX_LENGTH = 64;
const NONCE_MIN_LENGTH = 8;
const NONCE_MAX_LENGTH = 128;

const calendarOAuthStateSchema = z.object({
  emailAccountId: z.string().min(1).max(EMAIL_ACCOUNT_ID_MAX_LENGTH),
  type: z.literal("calendar"),
  nonce: z.string().min(NONCE_MIN_LENGTH).max(NONCE_MAX_LENGTH),
});

69-94: Simplify the type assertion in parseOAuthState call.

The type assertion parseOAuthState<Omit<CalendarOAuthState, "nonce">>(storedState) is unnecessarily complex. Since parseOAuthState returns T & { nonce: string }, the result is already CalendarOAuthState. You can simplify this to improve readability.

   let rawState: unknown;
   try {
-    rawState = parseOAuthState<Omit<CalendarOAuthState, "nonce">>(storedState);
+    rawState = parseOAuthState(storedState);
   } catch (error) {
     logger.error("Failed to decode state", { error });
     redirectUrl.searchParams.set("error", "invalid_state_format");
     throw new RedirectError(redirectUrl, responseHeaders);
   }

205-218: Consider adding JSDoc to document the RedirectError pattern.

The custom RedirectError class is a useful pattern for propagating redirect responses through error handling. Adding JSDoc would help other developers understand when and how to use this pattern.

 /**
  * Custom error class for redirect responses
+ * 
+ * Thrown when an OAuth callback validation error occurs and the user should
+ * be redirected back to the calendar page with appropriate error information.
+ * The error carries the redirect URL and response headers needed to construct
+ * the redirect response.
+ * 
+ * @example
+ * ```typescript
+ * throw new RedirectError(redirectUrl, response.headers);
+ * ```
  */
 export class RedirectError extends Error {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a48330 and f1c7bd8.

⛔ Files ignored due to path filters (2)
  • apps/web/public/images/product/outlook-calendar.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (29)
  • README.md (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/calendars/CalendarConnectionCard.tsx (3 hunks)
  • apps/web/app/(app)/[emailAccountId]/calendars/CalendarConnections.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendarButton.tsx (0 hunks)
  • apps/web/app/(app)/[emailAccountId]/calendars/page.tsx (1 hunks)
  • apps/web/app/api/google/calendar/callback/route.ts (1 hunks)
  • apps/web/app/api/google/linking/callback/route.ts (5 hunks)
  • apps/web/app/api/mcp/[integration]/callback/route.ts (2 hunks)
  • apps/web/app/api/outlook/calendar/auth-url/route.ts (1 hunks)
  • apps/web/app/api/outlook/calendar/callback/route.ts (1 hunks)
  • apps/web/app/api/outlook/linking/callback/route.ts (5 hunks)
  • apps/web/utils/ai/calendar/availability.ts (3 hunks)
  • apps/web/utils/calendar/availability-types.ts (1 hunks)
  • apps/web/utils/calendar/availability.ts (0 hunks)
  • apps/web/utils/calendar/handle-calendar-callback.ts (1 hunks)
  • apps/web/utils/calendar/oauth-callback-helpers.ts (1 hunks)
  • apps/web/utils/calendar/oauth-types.ts (1 hunks)
  • apps/web/utils/calendar/providers/google-availability.ts (1 hunks)
  • apps/web/utils/calendar/providers/google.ts (1 hunks)
  • apps/web/utils/calendar/providers/microsoft-availability.ts (1 hunks)
  • apps/web/utils/calendar/providers/microsoft.ts (1 hunks)
  • apps/web/utils/calendar/unified-availability.ts (1 hunks)
  • apps/web/utils/oauth/account-linking.ts (2 hunks)
  • apps/web/utils/oauth/callback-validation.ts (2 hunks)
  • apps/web/utils/outlook/calendar-client.ts (1 hunks)
  • apps/web/utils/outlook/scopes.ts (1 hunks)
  • package.json (1 hunks)
  • version.txt (1 hunks)
💤 Files with no reviewable changes (2)
  • apps/web/utils/calendar/availability.ts
  • apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendarButton.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-08T13:14:07.449Z
Learnt from: elie222
Repo: elie222/inbox-zero PR: 537
File: apps/web/app/(app)/[emailAccountId]/clean/onboarding/page.tsx:30-34
Timestamp: 2025-07-08T13:14:07.449Z
Learning: The clean onboarding page in apps/web/app/(app)/[emailAccountId]/clean/onboarding/page.tsx is intentionally Gmail-specific and should show an error for non-Google email accounts rather than attempting to support multiple providers.

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx
  • apps/web/app/(app)/[emailAccountId]/calendars/CalendarConnections.tsx
🧬 Code graph analysis (19)
apps/web/utils/calendar/unified-availability.ts (4)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-81)
apps/web/utils/calendar/availability-types.ts (1)
  • BusyPeriod (1-4)
apps/web/utils/calendar/providers/google-availability.ts (1)
  • googleAvailabilityProvider (63-89)
apps/web/utils/calendar/providers/microsoft-availability.ts (1)
  • microsoftAvailabilityProvider (68-94)
apps/web/utils/oauth/account-linking.ts (1)
apps/web/env.ts (1)
  • env (16-244)
apps/web/utils/calendar/providers/google-availability.ts (2)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-81)
apps/web/utils/calendar/availability-types.ts (2)
  • BusyPeriod (1-4)
  • CalendarAvailabilityProvider (6-21)
apps/web/utils/calendar/providers/google.ts (3)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-81)
apps/web/utils/calendar/oauth-types.ts (2)
  • CalendarOAuthProvider (10-28)
  • CalendarTokens (3-8)
apps/web/utils/calendar/client.ts (2)
  • getCalendarOAuth2Client (31-37)
  • fetchGoogleCalendars (117-127)
apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx (4)
apps/web/providers/EmailAccountProvider.tsx (1)
  • useAccount (79-89)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-81)
apps/web/utils/fetch.ts (1)
  • fetchWithAccount (7-25)
apps/web/app/api/outlook/calendar/auth-url/route.ts (1)
  • GetCalendarAuthUrlResponse (10-10)
apps/web/app/api/outlook/calendar/callback/route.ts (4)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-81)
apps/web/utils/middleware.ts (1)
  • withError (327-345)
apps/web/utils/calendar/handle-calendar-callback.ts (1)
  • handleCalendarCallback (20-138)
apps/web/utils/calendar/providers/microsoft.ts (1)
  • microsoftCalendarProvider (12-131)
apps/web/utils/oauth/callback-validation.ts (1)
apps/web/env.ts (1)
  • env (16-244)
apps/web/app/api/outlook/calendar/auth-url/route.ts (5)
apps/web/utils/oauth/state.ts (2)
  • generateOAuthState (10-18)
  • oauthStateCookieOptions (34-40)
apps/web/utils/outlook/calendar-client.ts (1)
  • getCalendarOAuth2Url (25-42)
apps/web/app/api/outlook/calendar/callback/route.ts (1)
  • GET (8-10)
apps/web/utils/middleware.ts (1)
  • withEmailAccount (373-391)
apps/web/utils/calendar/constants.ts (1)
  • CALENDAR_STATE_COOKIE_NAME (1-1)
apps/web/app/api/google/linking/callback/route.ts (1)
apps/web/env.ts (1)
  • env (16-244)
apps/web/utils/calendar/providers/microsoft-availability.ts (2)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-81)
apps/web/utils/calendar/availability-types.ts (2)
  • BusyPeriod (1-4)
  • CalendarAvailabilityProvider (6-21)
apps/web/app/api/mcp/[integration]/callback/route.ts (1)
apps/web/env.ts (1)
  • env (16-244)
apps/web/utils/calendar/providers/microsoft.ts (4)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-81)
apps/web/utils/calendar/oauth-types.ts (2)
  • CalendarOAuthProvider (10-28)
  • CalendarTokens (3-8)
apps/web/env.ts (1)
  • env (16-244)
apps/web/utils/outlook/calendar-client.ts (2)
  • getCalendarClientWithRefresh (44-135)
  • fetchMicrosoftCalendars (137-153)
apps/web/utils/calendar/handle-calendar-callback.ts (4)
apps/web/utils/calendar/oauth-types.ts (1)
  • CalendarOAuthProvider (10-28)
apps/web/utils/logger.ts (1)
  • Logger (5-5)
apps/web/utils/calendar/oauth-callback-helpers.ts (9)
  • validateOAuthCallback (25-64)
  • parseAndValidateCalendarState (69-94)
  • buildCalendarRedirectUrl (99-104)
  • verifyEmailAccountAccess (109-138)
  • checkExistingConnection (143-155)
  • redirectWithMessage (184-191)
  • createCalendarConnection (160-179)
  • RedirectError (208-218)
  • redirectWithError (196-203)
apps/web/env.ts (1)
  • env (16-244)
apps/web/utils/outlook/calendar-client.ts (4)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-81)
apps/web/env.ts (1)
  • env (16-244)
apps/web/utils/outlook/scopes.ts (1)
  • CALENDAR_SCOPES (20-28)
apps/web/utils/error.ts (1)
  • SafeError (59-69)
apps/web/app/(app)/[emailAccountId]/calendars/page.tsx (3)
apps/web/components/PageWrapper.tsx (1)
  • PageWrapper (3-20)
apps/web/components/PageHeader.tsx (1)
  • PageHeader (14-36)
apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx (1)
  • ConnectCalendar (12-111)
apps/web/utils/ai/calendar/availability.ts (1)
apps/web/utils/calendar/unified-availability.ts (1)
  • getUnifiedCalendarAvailability (14-137)
apps/web/utils/calendar/oauth-callback-helpers.ts (6)
apps/web/utils/logger.ts (1)
  • Logger (5-5)
apps/web/utils/calendar/oauth-types.ts (2)
  • OAuthCallbackValidation (30-34)
  • CalendarOAuthState (36-40)
apps/web/utils/calendar/constants.ts (1)
  • CALENDAR_STATE_COOKIE_NAME (1-1)
apps/web/env.ts (1)
  • env (16-244)
apps/web/utils/oauth/state.ts (1)
  • parseOAuthState (25-29)
apps/web/utils/auth.ts (1)
  • auth (495-496)
apps/web/app/api/google/calendar/callback/route.ts (2)
apps/web/utils/calendar/handle-calendar-callback.ts (1)
  • handleCalendarCallback (20-138)
apps/web/utils/calendar/providers/google.ts (1)
  • googleCalendarProvider (13-99)
apps/web/app/api/outlook/linking/callback/route.ts (1)
apps/web/env.ts (1)
  • env (16-244)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Software Component Analysis Js
  • GitHub Check: Static Code Analysis Js
  • GitHub Check: Software Component Analysis Js
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Jit Security
  • GitHub Check: test
  • GitHub Check: Static Code Analysis Js
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Jit Security
🔇 Additional comments (28)
version.txt (1)

1-1: LGTM! Appropriate version bump for feature release.

The minor version increment to v2.20.0 correctly signals the addition of Microsoft Calendar support as a new feature.

apps/web/utils/oauth/callback-validation.ts (1)

1-32: LGTM! Consistent refactoring to use environment-based base URL.

The removal of the baseUrl parameter and the use of env.NEXT_PUBLIC_BASE_URL simplifies the function signature and centralizes redirect URL configuration. This change is consistent with the broader refactoring across OAuth and callback handling in this PR.

README.md (1)

414-431: LGTM! Clear and comprehensive Microsoft Calendar setup documentation.

The documentation provides all necessary steps for configuring Microsoft Calendar integration, including Azure app registration, redirect URI setup, and required permissions. The note about the calendar OAuth flow being separate from the email OAuth is particularly helpful.

apps/web/utils/oauth/account-linking.ts (1)

1-30: LGTM! Consistent refactoring pattern applied.

The removal of the baseUrl parameter and the use of env.NEXT_PUBLIC_BASE_URL aligns with the environment-based redirect URL pattern introduced across the PR. This simplifies the function signature and centralizes configuration.

apps/web/app/api/google/linking/callback/route.ts (1)

46-46: LGTM! Consistent application of environment-based redirect URLs.

All redirect URL constructions in this file have been updated to use env.NEXT_PUBLIC_BASE_URL instead of request.nextUrl.origin. This ensures consistent redirect behavior across all code paths in the Google linking callback.

Also applies to: 60-60, 203-203, 241-241, 250-250

apps/web/utils/outlook/scopes.ts (1)

20-28: LGTM! Appropriate Microsoft Graph calendar scopes defined.

The CALENDAR_SCOPES constant includes the necessary permissions for calendar integration:

  • Baseline authentication scopes (openid, profile, email, User.Read)
  • offline_access for refresh tokens
  • Calendars.Read and Calendars.ReadWrite for calendar access

This follows the established pattern of the existing SCOPES constant and aligns with Microsoft Graph API requirements.

apps/web/app/api/mcp/[integration]/callback/route.ts (1)

53-53: Environment variable properly configured and verified.

The refactoring is sound: NEXT_PUBLIC_BASE_URL is consistently applied across all OAuth and callback routes. Verification confirms:

  • Required field: Defined as z.string() in env.ts (no optional flag), enforcing configuration across all environments
  • Documented: .env.example provides the development default; README explicitly documents values for dev, Docker, and production deployments
  • Build-time requirement: Docker deployment instructions clearly state it must be passed as --build-arg (correctly handled by Next.js build process)
  • No redirect URL vulnerabilities: All redirect URLs construct from the required environment variable; validation checks remain intact

No actionable items remain.

package.json (1)

21-21: Turbo 2.6.1 and pnpm 10.22.0 are compatible—no breaking changes detected.

Turbo 2.6.1 contains bug fixes and documentation updates with no advertised breaking changes compared to 2.5.8, and pnpm 10.22.0 has no breaking changes listed. Both updates introduce new features without compromising existing functionality. The versions are safe to use.

apps/web/utils/calendar/oauth-types.ts (1)

1-40: LGTM!

The type definitions are well-structured and provide a clean abstraction for calendar OAuth providers. The interfaces clearly separate concerns between token exchange, calendar synchronization, and OAuth state management.

apps/web/app/(app)/[emailAccountId]/calendars/CalendarConnections.tsx (1)

17-17: LGTM!

The updated message accurately reflects the new multi-provider calendar support introduced in this PR.

apps/web/app/(app)/[emailAccountId]/calendars/CalendarConnectionCard.tsx (1)

57-57: LGTM!

The dynamic provider rendering correctly uses the computed provider information to display the appropriate icon, alt text, and title for each calendar connection.

Also applies to: 125-132

apps/web/app/(app)/[emailAccountId]/calendars/page.tsx (1)

4-4: LGTM!

The component replacement and responsive layout adjustments properly support the new multi-provider calendar connection flow. The layout will stack vertically on smaller screens and arrange horizontally on larger screens, providing a better user experience.

Also applies to: 9-15

apps/web/app/api/outlook/calendar/callback/route.ts (1)

1-10: LGTM!

The route handler follows a clean, consistent pattern by delegating to the unified handleCalendarCallback handler with the Microsoft provider. The implementation is well-scoped with proper logging and error handling.

apps/web/app/api/outlook/calendar/auth-url/route.ts (2)

12-21: LGTM!

The getAuthUrl helper properly generates OAuth state with the email account context and delegates URL construction to the shared Outlook calendar client utility.


23-37: LGTM!

The route handler correctly:

  • Uses withEmailAccount middleware to ensure proper authentication
  • Generates the OAuth URL with appropriate state
  • Sets the state cookie with secure options
  • Returns a properly typed response
apps/web/app/api/outlook/linking/callback/route.ts (1)

49-49: All environment variable configurations verified and properly implemented.

The changes have been verified across all mentioned lines (49, 63, 274, 310, 319). The environment variable is properly configured in env.ts with required string validation (z.string()), correctly imported in the route handler, and consistently used for all redirect URL constructions. No remaining uses of request.nextUrl.origin exist in the OAuth callbacks.

apps/web/utils/calendar/providers/google-availability.ts (1)

11-88: Google FreeBusy wrapper and provider wiring look correct

The FreeBusy query, BusyPeriod extraction, and integration with getCalendarClientWithRefresh align with the provider contract and unified availability usage. Error handling and trace logging are appropriate.

apps/web/utils/calendar/providers/google.ts (1)

16-97: Google OAuth token exchange and calendar sync flow look solid

The provider correctly validates the ID token/email, enforces presence of a refresh token, and syncs calendars via upsert while marking the connection disconnected on failure. The integration with getCalendarClientWithRefresh and Prisma schema appears coherent.

apps/web/app/api/google/calendar/callback/route.ts (1)

3-9: Thin delegation to shared calendar callback handler looks good.

Routing Google calendar callbacks through handleCalendarCallback via googleCalendarProvider keeps this route minimal and centralizes the OAuth and syncing flow in one place. The logger scope remains specific to this route, which is helpful for tracing.

apps/web/utils/calendar/oauth-callback-helpers.ts (9)

1-14: LGTM!

Imports are well-organized and all appear to be used appropriately throughout the file.


34-41: LGTM! Strong origin validation.

Good security practice to validate the OAuth callback origin against the expected base URL. This prevents redirect attacks and ensures callbacks come from the expected domain.


54-61: LGTM! Proper CSRF protection via state validation.

Excellent implementation of OAuth state validation to prevent CSRF attacks. The comparison ensures the state parameter matches the stored cookie value.


96-104: LGTM!

Clean implementation that properly constructs account-specific calendar URLs using the utility function and environment configuration.


106-138: LGTM! Robust authorization checks.

Excellent implementation of authorization verification:

  1. Validates active session exists
  2. Confirms user owns the email account via database lookup
  3. Uses minimal select for efficiency
  4. Provides appropriate error handling and logging

140-155: LGTM!

Straightforward implementation that correctly checks for existing calendar connections using all relevant identifying fields.


181-191: LGTM!

Clean helper function for success redirects with proper query parameter handling.


193-203: LGTM!

Clean helper function for error redirects with proper query parameter handling.


157-179: The concern about plaintext token storage is incorrect.

The tokens are automatically encrypted before storage via Prisma extensions. The calendarConnection model is explicitly covered by the encryptedTokens extension (line 21 of apps/web/utils/prisma-extensions.ts), which:

  • Encrypts accessToken and refreshToken on all create/update operations using AES-256-GCM with random IVs and authentication tags
  • Automatically decrypts them when retrieved

The code already implements secure token storage by design.

Likely an incorrect or invalid review comment.

Comment on lines +43 to +44
const redirectUrl = new URL("/calendars", env.NEXT_PUBLIC_BASE_URL);
const response = NextResponse.redirect(redirectUrl);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent redirect URL construction.

Line 43 hardcodes the redirect to /calendars, while buildCalendarRedirectUrl (lines 99-104) uses prefixPath(emailAccountId, "/calendars") to construct a more specific path. This inconsistency could lead to users being redirected to the wrong location.

Consider using buildCalendarRedirectUrl in validateOAuthCallback once the emailAccountId is parsed from state, or document why the generic /calendars redirect is used initially (perhaps as a fallback before state parsing).

Also applies to: 99-104

🤖 Prompt for AI Agents
In apps/web/utils/calendar/oauth-callback-helpers.ts around lines 43-44 (and
also affecting 99-104), the redirect is hardcoded to "/calendars" which is
inconsistent with buildCalendarRedirectUrl that prefixes the path with the
emailAccountId; parse the emailAccountId from the OAuth state before issuing the
redirect and call buildCalendarRedirectUrl(emailAccountId, "/calendars") to
construct the target URL, and if the state is missing or parsing fails keep a
documented fallback to new URL("/calendars", env.NEXT_PUBLIC_BASE_URL) so users
aren’t left without a redirect.

Comment on lines +48 to +52
if (!code || code.length < 10) {
logger.warn("Missing or invalid code in calendar callback");
redirectUrl.searchParams.set("error", "missing_code");
throw new RedirectError(redirectUrl, response.headers);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace magic number with a more lenient or provider-aware validation.

The hardcoded 10 character minimum for the authorization code is arbitrary. OAuth authorization codes vary significantly in length across providers (Google, Microsoft, etc.). This could reject valid codes or fail to catch malformed ones.

Consider either:

  1. Removing the length check entirely (presence check is sufficient)
  2. Using a more lenient minimum (e.g., 5)
  3. Making it provider-specific if needed
-  if (!code || code.length < 10) {
+  if (!code || code.trim().length === 0) {
     logger.warn("Missing or invalid code in calendar callback");
     redirectUrl.searchParams.set("error", "missing_code");
     throw new RedirectError(redirectUrl, response.headers);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!code || code.length < 10) {
logger.warn("Missing or invalid code in calendar callback");
redirectUrl.searchParams.set("error", "missing_code");
throw new RedirectError(redirectUrl, response.headers);
}
if (!code || code.trim().length === 0) {
logger.warn("Missing or invalid code in calendar callback");
redirectUrl.searchParams.set("error", "missing_code");
throw new RedirectError(redirectUrl, response.headers);
}
🤖 Prompt for AI Agents
In apps/web/utils/calendar/oauth-callback-helpers.ts around lines 48 to 52, the
authorization-code validation uses a hardcoded minimum length of 10 which is
arbitrary and may reject valid provider codes; remove the strict length check
and only validate presence (i.e., ensure code is truthy), or if you prefer a
lenient guard use a much smaller minimum like 5 or implement provider-specific
checks based on the current provider; update the logger/redirect to only treat
missing/empty codes as errors and adjust tests accordingly.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 31 files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
apps/web/utils/outlook/calendar-client.ts (2)

110-110: Improve clarity of expiry calculation.

While JavaScript operator precedence makes this work correctly, applying Math.floor to the final sum can cause precision issues. It's clearer and more precise to floor the milliseconds-to-seconds conversion first.

Apply this diff for better precision and clarity:

-          expires_at: Math.floor(Date.now() / 1000 + Number(tokens.expires_in)),
+          expires_at: Math.floor(Date.now() / 1000) + Number(tokens.expires_in),

86-90: Avoid exposing raw error messages from external services.

The error description from Microsoft's token endpoint might contain sensitive information or internal details that shouldn't be directly exposed.

Apply this diff to use a generic error message:

     if (!response.ok) {
-      throw new Error(tokens.error_description || "Failed to refresh token");
+      logger.error("Token refresh failed", { error: tokens.error_description });
+      throw new Error("Failed to refresh token");
     }
apps/web/__tests__/e2e/calendar/google-calendar.test.ts (1)

149-178: Avoid hard‑coding “at least one busy period” to reduce flakiness

The assertion that there must be at least one busy period tomorrow makes this test depend on personal calendar contents. If the test account has no events tomorrow, you’ll get a failing test even though the integration is working.

Consider treating “no busy periods” as non‑fatal, and still validating shape when data exists. For example:

-      // Verify at least one busy period was found
-      expect(busyPeriods.length).toBeGreaterThan(0);
-
-      // Verify busy periods have correct structure
-      if (busyPeriods.length > 0) {
-        expect(busyPeriods[0]).toHaveProperty("start");
-        expect(busyPeriods[0]).toHaveProperty("end");
-        expect(typeof busyPeriods[0].start).toBe("string");
-        expect(typeof busyPeriods[0].end).toBe("string");
-      }
+      // If there are no busy periods, we already logged a warning above; treat as non‑fatal
+      if (busyPeriods.length === 0) {
+        return;
+      }
+
+      // Verify at least one busy period was found
+      expect(busyPeriods.length).toBeGreaterThan(0);
+
+      // Verify busy periods have correct structure
+      expect(busyPeriods[0]).toHaveProperty("start");
+      expect(busyPeriods[0]).toHaveProperty("end");
+      expect(typeof busyPeriods[0].start).toBe("string");
+      expect(typeof busyPeriods[0].end).toBe("string");

This keeps the assertions meaningful while avoiding spurious failures when the calendar is legitimately empty.

apps/web/__tests__/e2e/calendar/microsoft-calendar.test.ts (1)

153-183: Relax “at least one busy period” assumption to make the test less brittle

Like the Google test, this assumes there is at least one event tomorrow:

// (Assuming your calendar actually has events on tomorrow)
expect(busyPeriods.length).toBeGreaterThan(0);

If the test account is legitimately free tomorrow, the integration is still healthy but the test fails.

You can use the same pattern as the Google test suggestion and early‑return when there are no busy periods, while still asserting structure when data exists:

-      // Verify at least one busy period was found
-      // (Assuming your calendar actually has events on tomorrow)
-      expect(busyPeriods.length).toBeGreaterThan(0);
-
-      // Verify busy periods have correct structure
-      if (busyPeriods.length > 0) {
-        expect(busyPeriods[0]).toHaveProperty("start");
-        expect(busyPeriods[0]).toHaveProperty("end");
-        expect(typeof busyPeriods[0].start).toBe("string");
-        expect(typeof busyPeriods[0].end).toBe("string");
-      }
+      // If there are no busy periods, we already logged a warning above; treat as non‑fatal
+      if (busyPeriods.length === 0) {
+        return;
+      }
+
+      // Verify at least one busy period was found
+      expect(busyPeriods.length).toBeGreaterThan(0);
+
+      // Verify busy periods have correct structure
+      expect(busyPeriods[0]).toHaveProperty("start");
+      expect(busyPeriods[0]).toHaveProperty("end");
+      expect(typeof busyPeriods[0].start).toBe("string");
+      expect(typeof busyPeriods[0].end).toBe("string");

That keeps the e2e useful without tying it to a specific calendar state.

apps/web/utils/calendar/unified-availability.ts (1)

72-108: Consider logging when Google connections have no enabled calendars (for parity with Microsoft)

For Google connections you silently continue when there are no enabled calendars, while for Microsoft you log a warning. For troubleshooting, it can be helpful to have consistent observability across providers.

You could mirror the Microsoft behavior like this:

-  // Fetch Google calendar availability
-  for (const connection of googleConnections) {
-    const calendarIds = connection.calendars.map((cal) => cal.calendarId);
-    if (!calendarIds.length) continue;
+  // Fetch Google calendar availability
+  for (const connection of googleConnections) {
+    const calendarIds = connection.calendars.map((cal) => cal.calendarId);
+    if (!calendarIds.length) {
+      logger.warn("No enabled calendars for Google connection", {
+        connectionId: connection.id,
+      });
+      continue;
+    }

Not required for correctness, but it will make debugging “no data” scenarios much easier.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1c7bd8 and 717bb7d.

📒 Files selected for processing (5)
  • apps/web/__tests__/e2e/calendar/google-calendar.test.ts (1 hunks)
  • apps/web/__tests__/e2e/calendar/microsoft-calendar.test.ts (1 hunks)
  • apps/web/utils/calendar/providers/microsoft-availability.ts (1 hunks)
  • apps/web/utils/calendar/unified-availability.ts (1 hunks)
  • apps/web/utils/outlook/calendar-client.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/utils/calendar/providers/microsoft-availability.ts
🧰 Additional context used
🧬 Code graph analysis (4)
apps/web/utils/outlook/calendar-client.ts (4)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-81)
apps/web/env.ts (1)
  • env (16-244)
apps/web/utils/outlook/scopes.ts (1)
  • CALENDAR_SCOPES (20-28)
apps/web/utils/error.ts (1)
  • SafeError (59-69)
apps/web/__tests__/e2e/calendar/google-calendar.test.ts (1)
apps/web/utils/calendar/providers/google-availability.ts (1)
  • googleAvailabilityProvider (63-89)
apps/web/utils/calendar/unified-availability.ts (4)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-81)
apps/web/utils/calendar/availability-types.ts (1)
  • BusyPeriod (1-4)
apps/web/utils/calendar/providers/google-availability.ts (1)
  • googleAvailabilityProvider (63-89)
apps/web/utils/calendar/providers/microsoft-availability.ts (1)
  • microsoftAvailabilityProvider (65-91)
apps/web/__tests__/e2e/calendar/microsoft-calendar.test.ts (1)
apps/web/utils/calendar/providers/microsoft-availability.ts (1)
  • microsoftAvailabilityProvider (65-91)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Software Component Analysis Js
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Jit Security
  • GitHub Check: test
🔇 Additional comments (8)
apps/web/utils/outlook/calendar-client.ts (5)

1-11: LGTM!

The imports and logger setup follow the established patterns correctly.


13-23: LGTM!

The CalendarAuthProvider implementation correctly implements the AuthenticationProvider interface required by Microsoft Graph Client.


25-42: LGTM!

The OAuth2 URL generation correctly constructs the Microsoft authorization endpoint with proper parameters.


44-135: Well-structured token refresh flow.

The overall token refresh implementation correctly handles the Microsoft OAuth2 flow, including token validation, refresh, and persistence. The error handling for invalid_grant is appropriate.


158-193: LGTM!

The token persistence logic correctly handles optional fields and properly converts timestamp units from seconds to milliseconds for the Date object.

apps/web/__tests__/e2e/calendar/google-calendar.test.ts (1)

11-105: Overall e2e flow and safeguards look solid

Conditional execution via environment flags, defensive checks for missing account/connection/tokens, and the use of a real provider to exercise the full stack all look well thought out and safe to run only when explicitly configured.

apps/web/__tests__/e2e/calendar/microsoft-calendar.test.ts (1)

11-108: Symmetric Outlook e2e wiring looks good

The setup/guard clauses, database queries, and provider invocation mirror the Google flow cleanly and give you a good end‑to‑end signal when a Microsoft calendar is connected.

apps/web/utils/calendar/unified-availability.ts (1)

14-143: Unified availability aggregation and error isolation look correct

The function correctly:

  • Computes a single time window in the requested timezone.
  • Loads all connected calendar connections with only enabled calendars.
  • Fans out per connection, isolating failures by catching and returning empty arrays.
  • Flattens results and traces both request metadata and aggregate counts.

This is a clean, composable abstraction for multi‑provider availability.

Comment on lines +37 to +41
if (!testEmail) {
console.warn("\n⚠️ Set TEST_OUTLOOK_EMAIL env var to run these tests");
console.warn(
" Example: TEST_OUTLOOK_EMAIL=your@email.com pnpm test-e2e outlook-calendar\n",
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align example command with documented usage to avoid confusion

The usage block at the top shows pnpm test-e2e microsoft-calendar, but the warning example uses outlook-calendar. That mismatch can confuse anyone trying to run the test.

You can align them like this:

-      console.warn(
-        "   Example: TEST_OUTLOOK_EMAIL=your@email.com pnpm test-e2e outlook-calendar\n",
-      );
+      console.warn(
+        "   Example: TEST_OUTLOOK_EMAIL=your@email.com pnpm test-e2e microsoft-calendar\n",
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!testEmail) {
console.warn("\n⚠️ Set TEST_OUTLOOK_EMAIL env var to run these tests");
console.warn(
" Example: TEST_OUTLOOK_EMAIL=your@email.com pnpm test-e2e outlook-calendar\n",
);
if (!testEmail) {
console.warn("\n⚠️ Set TEST_OUTLOOK_EMAIL env var to run these tests");
console.warn(
" Example: TEST_OUTLOOK_EMAIL=your@email.com pnpm test-e2e microsoft-calendar\n",
);
🤖 Prompt for AI Agents
In apps/web/__tests__/e2e/calendar/microsoft-calendar.test.ts around lines 37 to
41, the warning example command uses "outlook-calendar" which contradicts the
documented usage "microsoft-calendar"; update the warning to show the same
command (e.g., TEST_OUTLOOK_EMAIL=your@email.com pnpm test-e2e
microsoft-calendar) so the example matches the documented test name and avoids
confusion.

if (!refreshToken) throw new SafeError("No refresh token");

// Check if token is still valid
if (expiresAt && expiresAt > Date.now() && accessToken) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Token expiry comparison has unit mismatch.

The comparison expiresAt > Date.now() will always fail because Date.now() returns milliseconds while expiresAt is stored in seconds (as evident from line 110). This causes tokens to be considered expired on every call, forcing unnecessary refresh requests.

Apply this diff to fix the comparison:

-  if (expiresAt && expiresAt > Date.now() && accessToken) {
+  if (expiresAt && expiresAt * 1000 > Date.now() && accessToken) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (expiresAt && expiresAt > Date.now() && accessToken) {
if (expiresAt && expiresAt * 1000 > Date.now() && accessToken) {
🤖 Prompt for AI Agents
In apps/web/utils/outlook/calendar-client.ts around line 58, the token expiry
comparison uses expiresAt (seconds) against Date.now() (milliseconds), causing
tokens to be treated as expired; fix by normalizing units before comparing —
either convert expiresAt to milliseconds (expiresAt * 1000) and compare to
Date.now(), or convert Date.now() to seconds (Math.floor(Date.now() / 1000)) and
compare to expiresAt, so the if condition correctly detects unexpired tokens.

Comment on lines +137 to +156
export async function fetchMicrosoftCalendars(calendarClient: Client): Promise<
Array<{
id?: string;
name?: string;
description?: string;
timeZone?: string;
}>
> {
try {
const response = await calendarClient
.api("/me/calendars")
.select("id,name,color,isDefaultCalendar,canEdit,owner,timeZone")
.get();

return response.value || [];
} catch (error) {
logger.error("Error fetching Microsoft calendars", { error });
throw new SafeError("Failed to fetch calendars");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix mismatch between selected fields and return type.

The function selects fields that don't match the declared return type:

  • Selected but not in return type: color, isDefaultCalendar, canEdit, owner
  • In return type but not selected: description

This means description will always be undefined, and extra fields are fetched unnecessarily.

Apply this diff to align the select with the return type:

     const response = await calendarClient
       .api("/me/calendars")
-      .select("id,name,color,isDefaultCalendar,canEdit,owner,timeZone")
+      .select("id,name,description,timeZone")
       .get();

Alternatively, if you need the additional fields, update the return type accordingly.

🤖 Prompt for AI Agents
In apps/web/utils/outlook/calendar-client.ts around lines 137 to 156, the
selected fields from the Microsoft Graph call don’t match the declared return
type: the select includes color, isDefaultCalendar, canEdit, owner (which aren’t
in the return type) and omits description (which is in the return type). Fix by
making the select and return type consistent — either add description to the
.select(...) and remove any unneeded fields from the select so only
id,name,description,timeZone are requested, or expand the function’s return type
to include color, isDefaultCalendar, canEdit, and owner if those fields are
required; update the .select(...) and the returned object type accordingly and
keep the logger/error behavior unchanged.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files (reviewed changes from recent commits).

Prompt for AI agents (all 2 issues)

Understand the root cause of the following 2 issues and fix them.


<file name="apps/web/__tests__/e2e/calendar/google-calendar.test.ts">

<violation number="1" location="apps/web/__tests__/e2e/calendar/google-calendar.test.ts:170">
The test unconditionally expects at least one busy period even though an empty calendar is a valid outcome, so the E2E will fail whenever no events exist for the checked date. Remove or gate this assertion so the test only verifies structure unless you explicitly seed a busy period.</violation>
</file>

<file name="apps/web/utils/calendar/providers/microsoft-availability.ts">

<violation number="1" location="apps/web/utils/calendar/providers/microsoft-availability.ts:50">
Swallowing calendar API errors here makes the provider return an empty busy list when every request fails, so scheduling proceeds with incorrect availability instead of surfacing the failure.</violation>
</file>

Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR

expect(Array.isArray(busyPeriods)).toBe(true);

// Verify at least one busy period was found
expect(busyPeriods.length).toBeGreaterThan(0);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test unconditionally expects at least one busy period even though an empty calendar is a valid outcome, so the E2E will fail whenever no events exist for the checked date. Remove or gate this assertion so the test only verifies structure unless you explicitly seed a busy period.

Prompt for AI agents
Address the following comment on apps/web/__tests__/e2e/calendar/google-calendar.test.ts at line 170:

<comment>The test unconditionally expects at least one busy period even though an empty calendar is a valid outcome, so the E2E will fail whenever no events exist for the checked date. Remove or gate this assertion so the test only verifies structure unless you explicitly seed a busy period.</comment>

<file context>
@@ -0,0 +1,181 @@
+      expect(Array.isArray(busyPeriods)).toBe(true);
+
+      // Verify at least one busy period was found
+      expect(busyPeriods.length).toBeGreaterThan(0);
+
+      // Verify busy periods have correct structure
</file context>
Fix with Cubic

}
}
}
} catch (calendarError) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swallowing calendar API errors here makes the provider return an empty busy list when every request fails, so scheduling proceeds with incorrect availability instead of surfacing the failure.

Prompt for AI agents
Address the following comment on apps/web/utils/calendar/providers/microsoft-availability.ts at line 50:

<comment>Swallowing calendar API errors here makes the provider return an empty busy list when every request fails, so scheduling proceeds with incorrect availability instead of surfacing the failure.</comment>

<file context>
@@ -20,45 +20,42 @@ async function fetchMicrosoftCalendarBusyPeriods({
             }
           }
         }
+      } catch (calendarError) {
+        logger.error(&quot;Error fetching calendar events&quot;, {
+          calendarId,
</file context>
Fix with Cubic

@elie222 elie222 closed this Nov 17, 2025
@elie222 elie222 deleted the feat/microsoft-calendar branch December 18, 2025 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants