这是indexloc提供的服务,不要输入任何密码
Skip to content

Conversation

@JeanMeijer
Copy link
Collaborator

@JeanMeijer JeanMeijer commented Aug 28, 2025

Description

Briefly describe what you did and why.

Screenshots / Recordings

Add screenshots or recordings here to help reviewers understand your changes.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • UI/UX update
  • Docs update
  • Refactor / Cleanup

Related Areas

  • Authentication
  • Calendar UI
  • Data/API
  • Docs

Testing

  • Manual testing performed
  • Cross-browser testing (if UI changes)
  • Mobile responsiveness verified (if UI changes)

Checklist

  • I’ve read the CONTRIBUTING guide
  • My code works and is understandable and follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in complex areas
  • I have updated the documentation
  • Any dependent changes are merged and published

Notes

(Optional) Add anything else you'd like to share.

By submitting, I confirm I understand and stand behind this code. If AI was used, I’ve reviewed and verified everything myself.


Summary by cubic

Adds Google Calendar change channels (push notifications) and incremental sync for calendar lists and events via new webhook endpoints. Expands the data model to store channels/resources and richer event data, and updates providers for reliable token-based sync.

  • New Features

    • New webhook routes: POST /api/google/channels/calendars and POST /api/google/channels/events using a shared channel handler.
    • Channel helpers: subscribe/unsubscribe for calendar list and events, plus syncCalendarList/syncEvents with 410 (invalid sync token) recovery.
    • Webhooks validate channel headers/token, match resource IDs, refresh access tokens, and upsert or delete calendars/events accordingly.
    • Providers now pass calendarId/readOnly to parsers; Google and Microsoft event parsers updated; Google read-only detection preserved.
    • Auth adds Redis-backed SecondaryStorage (Upstash) for token storage.
  • Migration

    • Run DB migrations: add channel and resource tables; add recurrence and attendees tables; extend calendars/events (readOnly, metadata, response, recurrenceId, JSONB fields) and account (calendarListSyncToken).
    • Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN env vars.
    • Point Google push notifications to the new webhook routes and create watch subscriptions using providers/google-calendar/subscribe.ts.

@coderabbitai
Copy link

coderabbitai bot commented Aug 28, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel
Copy link

vercel bot commented Aug 28, 2025

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

Project Deployment Preview Comments Updated (UTC)
analog Ready Ready Preview Comment Aug 29, 2025 5:43pm

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.

22 issues found across 22 files

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.

@@ -0,0 +1,27 @@
import { Redis } from "@upstash/redis";
import { SecondaryStorage } from "better-auth";
Copy link
Contributor

Choose a reason for hiding this comment

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

Use a type-only import for SecondaryStorage to avoid adding an unnecessary runtime dependency to the bundle.

(This reflects your team's feedback about using type-only imports for types to keep bundles lean.)

Prompt for AI agents
Address the following comment on packages/auth/src/storage.ts at line 2:

<comment>Use a type-only import for SecondaryStorage to avoid adding an unnecessary runtime dependency to the bundle.

(This reflects your team&#39;s feedback about using type-only imports for types to keep bundles lean.)</comment>

<file context>
@@ -0,0 +1,27 @@
+import { Redis } from &quot;@upstash/redis&quot;;
+import { SecondaryStorage } from &quot;better-auth&quot;;
+
+import { env } from &quot;@repo/env/server&quot;;
</file context>
Suggested change
import { SecondaryStorage } from "better-auth";
import type { SecondaryStorage } from "better-auth";

get: async (key) => {
const value = await redis.get<string>(key);

return value ?? null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Ensure get always returns a string or null; stringify non-string values to prevent downstream type/parse errors.

(Based on your team's feedback about strict adherence to SecondaryStorage string contract to avoid runtime inconsistencies.)

Prompt for AI agents
Address the following comment on packages/auth/src/storage.ts at line 15:

<comment>Ensure get always returns a string or null; stringify non-string values to prevent downstream type/parse errors.

(Based on your team&#39;s feedback about strict adherence to SecondaryStorage string contract to avoid runtime inconsistencies.)</comment>

<file context>
@@ -0,0 +1,27 @@
+  get: async (key) =&gt; {
+    const value = await redis.get&lt;string&gt;(key);
+
+    return value ?? null;
+  },
+  set: async (key, value, ttl) =&gt; {
</file context>

return new Response("Channel not found", { status: 404 });
}

if (headers.token && headers.token !== channel.token) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Channel token not enforced when missing, allowing spoofed notifications without token

Prompt for AI agents
Address the following comment on packages/api/src/providers/google-calendar/channel.ts at line 67:

<comment>Channel token not enforced when missing, allowing spoofed notifications without token</comment>

<file context>
@@ -0,0 +1,115 @@
+      return new Response(&quot;Channel not found&quot;, { status: 404 });
+    }
+
+    if (headers.token &amp;&amp; headers.token !== channel.token) {
+      return new Response(&quot;Invalid channel token&quot;, { status: 401 });
+    }
</file context>

refreshTokenExpiresAt: timestamp(),
scope: text(),
password: text(),
calendarListSyncToken: text("calendar_list_sync_token"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Schema change without migration: added account.calendarListSyncToken but no DB migration adds the column, causing runtime query errors

Prompt for AI agents
Address the following comment on packages/db/src/schema/auth.ts at line 80:

<comment>Schema change without migration: added account.calendarListSyncToken but no DB migration adds the column, causing runtime query errors</comment>

<file context>
@@ -77,6 +77,7 @@ export const account = pgTable(
     refreshTokenExpiresAt: timestamp(),
     scope: text(),
     password: text(),
+    calendarListSyncToken: text(&quot;calendar_list_sync_token&quot;),
     createdAt: timestamp().notNull(),
     updatedAt: timestamp().notNull(),
</file context>

type: "google.calendar" as const,
subscriptionId,
resourceId: response.resourceId!,
expiresAt: new Date(response.expiration!),
Copy link
Contributor

Choose a reason for hiding this comment

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

expiration is a string millisecond timestamp; constructing Date with a numeric string yields Invalid Date, causing incorrect expiresAt and potential runtime errors.

Prompt for AI agents
Address the following comment on packages/api/src/providers/google-calendar/subscribe.ts at line 29:

<comment>expiration is a string millisecond timestamp; constructing Date with a numeric string yields Invalid Date, causing incorrect expiresAt and potential runtime errors.</comment>

<file context>
@@ -0,0 +1,79 @@
+    type: &quot;google.calendar&quot; as const,
+    subscriptionId,
+    resourceId: response.resourceId!,
+    expiresAt: new Date(response.expiration!),
+  };
+}
</file context>

.$default(() => newId("channel")),

// TODO: when an account is deleted, we should first stop channel subscriptions and then delete all channels associated with it
accountId: text()
Copy link
Contributor

Choose a reason for hiding this comment

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

Add indexes for accountId and resourceId to avoid slow lookups and joins; consider a composite index if queries filter by both.

Prompt for AI agents
Address the following comment on packages/db/src/schema/channel.ts at line 13:

<comment>Add indexes for accountId and resourceId to avoid slow lookups and joins; consider a composite index if queries filter by both.</comment>

<file context>
@@ -0,0 +1,31 @@
+    .$default(() =&gt; newId(&quot;channel&quot;)),
+
+  // TODO: when an account is deleted, we should first stop channel subscriptions and then delete all channels associated with it
+  accountId: text()
+    .notNull()
+    .references(() =&gt; account.id, { onDelete: &quot;cascade&quot; }),
</file context>

@@ -0,0 +1,40 @@
import { z } from "zod";

import { channel } from "@repo/db/schema";
Copy link
Contributor

Choose a reason for hiding this comment

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

Import is used only in a type position; prefer a type-only import to avoid an unnecessary runtime dependency and improve tree-shaking.

Prompt for AI agents
Address the following comment on packages/api/src/providers/google-calendar/channels/headers.ts at line 3:

<comment>Import is used only in a type position; prefer a type-only import to avoid an unnecessary runtime dependency and improve tree-shaking.</comment>

<file context>
@@ -0,0 +1,40 @@
+import { z } from &quot;zod&quot;;
+
+import { channel } from &quot;@repo/db/schema&quot;;
+
+export type ChannelHeaders = z.infer&lt;typeof headersSchema&gt;;
</file context>

return parseGoogleCalendarEvent({
calendar: destinationCalendar,
calendarId: destinationCalendar.id,
readOnly: false,
Copy link
Contributor

Choose a reason for hiding this comment

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

Hard-coding readOnly to false misrepresents destination calendar permissions; use destinationCalendar.readOnly for correctness and consistency.

Prompt for AI agents
Address the following comment on packages/api/src/providers/calendars/google-calendar.ts at line 281:

<comment>Hard-coding readOnly to false misrepresents destination calendar permissions; use destinationCalendar.readOnly for correctness and consistency.</comment>

<file context>
@@ -273,7 +277,8 @@ export class GoogleCalendarProvider implements CalendarProvider {
       return parseGoogleCalendarEvent({
-        calendar: destinationCalendar,
+        calendarId: destinationCalendar.id,
+        readOnly: false,
         accountId: this.accountId,
         event: moved,
</file context>
Suggested change
readOnly: false,
readOnly: destinationCalendar.readOnly,

calendarId: calendar.id,
})
.onConflictDoUpdate({
target: [calendars.id],
Copy link
Contributor

Choose a reason for hiding this comment

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

Primary key conflates calendars across accounts; upsert updates cross-tenant due to conflict on id only.

Prompt for AI agents
Address the following comment on packages/api/src/providers/google-calendar/channels/calendars.ts at line 21:

<comment>Primary key conflates calendars across accounts; upsert updates cross-tenant due to conflict on id only.</comment>

<file context>
@@ -0,0 +1,74 @@
+      calendarId: calendar.id,
+    })
+    .onConflictDoUpdate({
+      target: [calendars.id],
+      set: calendar,
+    });
</file context>

export const resource = pgTable("resource", {
id: text()
.primaryKey()
.$default(() => newId("resource")),
Copy link
Contributor

Choose a reason for hiding this comment

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

Use $defaultFn for function-based default so newId("resource") is evaluated per insert.

Prompt for AI agents
Address the following comment on packages/db/src/schema/resource.ts at line 8:

<comment>Use $defaultFn for function-based default so newId(&quot;resource&quot;) is evaluated per insert.</comment>

<file context>
@@ -0,0 +1,16 @@
+export const resource = pgTable(&quot;resource&quot;, {
+  id: text()
+    .primaryKey()
+    .$default(() =&gt; newId(&quot;resource&quot;)),
+  providerId: text({ enum: [&quot;google&quot;] }).notNull(),
+
</file context>

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