Skip to content

security: fix missing Secure attribute on cookies#1217

Closed
elie222 wants to merge 24 commits intomainfrom
fix/secure-cookies
Closed

security: fix missing Secure attribute on cookies#1217
elie222 wants to merge 24 commits intomainfrom
fix/secure-cookies

Conversation

@elie222
Copy link
Copy Markdown
Owner

@elie222 elie222 commented Jan 6, 2026

User description

security: Add Secure attribute to all document.cookie sets

This fix ensures all cookies set via the browser have the Secure attribute, preventing them from being transmitted over unencrypted HTTP.

  • Added Secure attribute to 12 cookie instances
  • Added SameSite=Lax where missing for consistency
  • Verified no impact on localhost development

Summary by CodeRabbit

Release Notes

  • New Features

    • Added bulk archive functionality to organize and archive emails by category
    • Introduced quick bulk archive interface for faster email management
    • Added auto-categorization setup to automatically organize uncategorized senders
    • Implemented real-time progress tracking for bulk operations
    • Added confidence-based email archiving with high, medium, and low priority levels
  • Security

    • Enhanced cookie security with additional protection attributes across authentication and session management

✏️ Tip: You can customize this high-level summary in your review settings.


Generated description

Below is a concise technical summary of the changes proposed in this PR:

graph LR
BulkArchivePage_("BulkArchivePage"):::added
BulkArchiveContent_("BulkArchiveContent"):::added
PRISMA_DB_("PRISMA/DB"):::modified
BulkArchiveProgress_("BulkArchiveProgress"):::added
AutoCategorizationSetup_("AutoCategorizationSetup"):::added
BulkArchiveCards_("BulkArchiveCards"):::added
GET_("GET"):::added
getCategorizedSenders_("getCategorizedSenders"):::added
BulkArchivePage_ -- "Loads senders/categories then renders BulkArchiveContent for client UI" --> BulkArchiveContent_
BulkArchivePage_ -- "Reads newsletter rows and category metadata via Prisma" --> PRISMA_DB_
BulkArchiveContent_ -- "Renders progress panel and refreshes when categorization completes" --> BulkArchiveProgress_
BulkArchiveContent_ -- "Includes setup to trigger AI categorization when none exist" --> AutoCategorizationSetup_
BulkArchiveContent_ -- "Passes grouped senders and categories to UI card component" --> BulkArchiveCards_
BulkArchiveContent_ -- "Polls categorized senders API to refresh senders and categories" --> GET_
GET_ -- "Concurrent fetch: newsletters and user categories before responding" --> getCategorizedSenders_
getCategorizedSenders_ -- "Queries newsletter table for senders with categoryId not null" --> PRISMA_DB_
classDef added stroke:#15AA7A
classDef removed stroke:#CD5270
classDef modified stroke:#EDAC4C
linkStyle default stroke:#CBD5E1,font-size:13px
Loading

Enhances application security by adding Secure and SameSite=Lax attributes to all client-side cookies, and introduces new bulk email archiving features including AI-powered sender categorization and a user interface for managing archived senders.

TopicDetails
Cookie Security Strengthens client-side cookie security by ensuring all document.cookie operations include the Secure and SameSite=Lax attributes, preventing transmission over unencrypted HTTP and mitigating CSRF risks.
Modified files (5)
  • apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx
  • apps/web/app/utm.tsx
  • apps/web/components/ui/sidebar.tsx
  • apps/web/utils/auth-cookies.ts
  • apps/web/utils/cookies.ts
Latest Contributors(2)
UserCommitDate
elie222fixesDecember 30, 2025
eduardoleliss@gmail.comPR-feedbackOctober 10, 2025
Bulk Archive Feature Implements a new bulk email archiving feature, allowing users to categorize and archive senders based on AI-driven confidence levels, and provides a user interface to manage these operations.
Modified files (19)
  • apps/web/app/(app)/(redirects)/bulk-archive/page.tsx
  • apps/web/app/(app)/(redirects)/quick-bulk-archive/page.tsx
  • apps/web/app/(app)/[emailAccountId]/bulk-archive/AutoCategorizationSetup.tsx
  • apps/web/app/(app)/[emailAccountId]/bulk-archive/BulkArchiveContent.tsx
  • apps/web/app/(app)/[emailAccountId]/bulk-archive/BulkArchiveProgress.tsx
  • apps/web/app/(app)/[emailAccountId]/bulk-archive/page.tsx
  • apps/web/app/(app)/[emailAccountId]/quick-bulk-archive/BulkArchiveTab.tsx
  • apps/web/app/(app)/[emailAccountId]/quick-bulk-archive/page.tsx
  • apps/web/app/(app)/early-access/page.tsx
  • apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
  • apps/web/app/api/user/categorize/senders/categorized/route.ts
  • apps/web/components/BulkArchiveCards.tsx
  • apps/web/components/EmailCell.tsx
  • apps/web/components/ProgressPanel.tsx
  • apps/web/components/bulk-archive/categoryIcons.test.ts
  • apps/web/components/bulk-archive/categoryIcons.ts
  • apps/web/utils/actions/categorize.ts
  • apps/web/utils/bulk-archive/get-archive-candidates.test.ts
  • apps/web/utils/bulk-archive/get-archive-candidates.ts
Latest Contributors(0)
UserCommitDate
This pull request is reviewed by Baz. Review like a pro on (Baz).

jshwrnr and others added 24 commits January 5, 2026 11:02
- Remove unused `categories` prop from BulkArchiveTab
- Add guards for empty thread.messages arrays
- Add error handling with try/catch in archiveSelected
- Remove dead code for 'more emails' section
- Fix createMany return value to use result.count
- Use Next.js Link instead of anchor tag in creator page
- Type error boundary props properly with reset function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Memoize headings in TableOfContents to prevent IntersectionObserver recreation
- Remove nested Button in TryInboxZero, style Link directly
- Update deprecated Image props (layout/objectFit) to Next.js 13+ syntax
- Add rel="noopener" to external link in CaseStudyContent

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The marketing directory was accidentally converted from a submodule
to a regular directory during a merge, causing 100+ spurious file
changes in the PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move category icon mapping logic to bulk-archive/categoryIcons.ts
to reduce BulkArchiveCards.tsx file size.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Jan 6, 2026

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

Project Deployment Review Updated (UTC)
inbox-zero Ready Ready Preview Jan 6, 2026 8:01pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

A bulk archiving feature is introduced with new pages, components, and API routes to manage email sender categorization and archiving by confidence level. Email provider abstraction replaces direct Gmail API usage. Cookie security is enhanced across the application with SameSite and Secure attributes.

Changes

Cohort / File(s) Summary
Bulk Archive Feature — Redirect Pages
apps/web/app/(app)/(redirects)/bulk-archive/page.tsx, apps/web/app/(app)/(redirects)/quick-bulk-archive/page.tsx
New async redirect pages that invoke redirectToEmailAccountPath for bulk-archive and quick-bulk-archive routes.
Bulk Archive Feature — Main Pages & Layout Components
apps/web/app/(app)/[emailAccountId]/bulk-archive/page.tsx, apps/web/app/(app)/[emailAccountId]/quick-bulk-archive/page.tsx
Server-rendered pages that verify account ownership, fetch initial senders/categories in parallel, and render UI composition with PermissionsCheck, ArchiveProgress, PremiumAlertWithData, TopBar, and client-side content components. Exports dynamic = "force-dynamic" and maxDuration = 300.
Bulk Archive Feature — Categorization & Progress
apps/web/app/(app)/[emailAccountId]/bulk-archive/AutoCategorizationSetup.tsx, apps/web/app/(app)/[emailAccountId]/bulk-archive/BulkArchiveProgress.tsx
AutoCategorizationSetup toggles bulk categorization with action call and toast feedback. BulkArchiveProgress subscribes to SWR polling with animated fake progress, detects completion, and invokes callback after 3-second delay; includes interval/timeout cleanup.
Bulk Archive Feature — Content & Layout
apps/web/app/(app)/[emailAccountId]/bulk-archive/BulkArchiveContent.tsx, apps/web/components/BulkArchiveCards.tsx
BulkArchiveContent fetches data via SWR (2000ms interval), computes emailGroups, and wires child components. BulkArchiveCards renders categorized sender rows with per-category and per-sender selection, expandable sender details with recent emails, archive queue integration, and category management dialog.
Quick Bulk Archive UI
apps/web/app/(app)/[emailAccountId]/quick-bulk-archive/BulkArchiveTab.tsx
Client component that renders high/medium/low confidence archive sections with per-sender selection, "Select All/Deselect All" controls, collapsible sender rows showing recent emails, and async queue-based archiving with error handling.
Category Icons & Utility
apps/web/components/bulk-archive/categoryIcons.ts, apps/web/components/bulk-archive/categoryIcons.test.ts
New utility getCategoryIcon maps category names to lucide-react icons via normalized string matching with 17 category patterns and MailIcon fallback. Comprehensive test suite covers edge cases, case-insensitivity, priority ordering, and boundary conditions.
Archive Candidate Classification
apps/web/utils/bulk-archive/get-archive-candidates.ts, apps/web/utils/bulk-archive/get-archive-candidates.test.ts
New utility classifies email sender groups into high/medium/low confidence archive candidates based on category keyword matching (e.g., "Marketing"/"Newsletter" → high, "Notification"/"Alert" → medium, uncategorized/unknown → low). Test suite validates classification logic and batch behavior.
API Routes & Email Provider Abstraction
apps/web/app/api/user/categorize/senders/categorized/route.ts, apps/web/app/api/user/categorize/senders/batch/handle-batch.ts
New GET route fetches categorized senders and categories; wrapped with withEmailAccount middleware. handle-batch replaces direct Gmail client usage with createEmailProvider abstraction, removing token-specific fetching and using emailProvider.getThreadsFromSenderWithSubject. Declares explicit return type Promise.
Categorization Actions & Initialization
apps/web/utils/actions/categorize.ts
bulkCategorizeSendersAction now initializes missing default enabled categories via prisma.category.createMany with skipDuplicates before queue cleanup and categorization.
Cookie Security Enhancements
apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx, apps/web/utils/auth-cookies.ts, apps/web/utils/cookies.ts, apps/web/components/ui/sidebar.tsx, apps/web/app/utm.tsx
All set-cookie operations now include SameSite=Lax and Secure attributes alongside existing path/max-age. Affects onboarding cookie, auth-error cookie, invitation cookies, sidebar open-state cookies, and all utm cookies.
Minor Component & UI Updates
apps/web/components/EmailCell.tsx, apps/web/components/ProgressPanel.tsx, apps/web/app/(app)/early-access/page.tsx
EmailCell conditionally renders subtitle; ProgressPanel wraps status content in div instead of p; early-access page wraps Sender Categories Card in fragment with commented placeholder Cards.

Sequence Diagram

sequenceDiagram
    participant User
    participant BulkArchivePage as Server:<br/>BulkArchivePage
    participant DB as Database
    participant Client as Client:<br/>BulkArchiveContent
    participant SWR as SWR Hook
    participant API as API:<br/>/categorize/senders
    participant Queue as Archive<br/>Queue

    User->>BulkArchivePage: Visit bulk-archive page
    BulkArchivePage->>BulkArchivePage: Verify account ownership
    BulkArchivePage->>DB: Fetch senders with categories
    BulkArchivePage->>DB: Fetch user categories with rules
    DB-->>BulkArchivePage: Initial data
    BulkArchivePage-->>Client: Render with initial data

    User->>Client: Enable auto-categorization
    Client->>Client: Set isEnabling = true
    Client->>API: bulkCategorizeSendersAction(emailAccountId)
    API->>API: Initialize missing default categories
    API->>API: Queue categorization batch job
    API-->>Client: Success
    Client->>Client: Show toast: "Processing..."
    
    par Polling for Progress
        Client->>SWR: Subscribe to progress with 2s interval
        loop While categorizing
            SWR->>API: GET /progress
            API-->>SWR: {totalItems, completedItems}
            SWR-->>Client: Update progress
        end
    end

    par User Actions
        User->>Client: Select senders to archive
        User->>Client: Click "Archive Selected"
        Client->>Queue: addToArchiveSenderQueue(senders)
        Queue->>Queue: Process archive jobs
    end

    API->>API: Categorization complete
    SWR->>API: GET /progress (100% complete)
    API-->>SWR: completedItems == totalItems
    SWR-->>Client: Completion detected
    Client->>Client: After 3s: disable categorizing
    Client->>Client: Invoke onComplete callback
    Client->>SWR: Mutate to refresh data
    SWR->>API: GET /categorized
    API->>DB: Fetch updated senders
    DB-->>API: Updated data
    API-->>SWR: New data
    SWR-->>Client: Update UI
    Client-->>User: Show updated sender list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Update digest to support Outlook #612: Replaces direct Gmail API usage with email provider abstraction in handle-batch.ts, matching the code-level refactor in this PR.
  • Longer bulk categorize #257: Modifies the bulk categorization pipeline including bulkCategorizeSendersAction and batch handlers, overlapping with categorization domain logic.
  • Smart Categories #252: Adds categorization and bulk-archive code paths (API routes, actions, components, queue logic) with direct overlap in the archiving infrastructure.

Poem

🐰✨ A Warren's Archive Dream
Hop along, the categories bloom,
High, medium, low—they find their room,
With progress bars that dance and glow,
Bulk archive now, let emails go! 📧⚡

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% 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 'security: fix missing Secure attribute on cookies' accurately reflects the main objective of this PR, which is to add the Secure attribute to cookies across multiple files for enhanced security.
✨ Finishing touches
  • 📝 Generate docstrings

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.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Jan 6, 2026

Add Secure and SameSite=Lax to app cookies and introduce email-account–scoped Bulk Archive and Quick Bulk Archive pages with polling and archiving actions

Set cookie attributes to Secure and SameSite=Lax across onboarding, auth error, UTM, referral, and sidebar state. Add email-account–scoped Bulk Archive and Quick Bulk Archive pages that fetch categorized senders, poll progress via new categorize endpoints, and queue archive actions. Replace Gmail-specific batching with createEmailProvider in the categorize batch handler. Add category icon and archive-candidate heuristics with tests.

📍Where to Start

Start with the categorize batch handler refactor in handle-batch.ts, then review the new polling endpoint in route.ts and the Bulk Archive page composition in page.tsx.


📊 Macroscope summarized 2db6dfb. 22 files reviewed, 23 issues evaluated, 16 issues filtered, 2 comments posted. View details

{data.threads.slice(0, 5).map((thread) => {
const firstMessage = thread.messages[0];
if (!firstMessage) return null;
const subject = firstMessage.subject;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

subject.length can throw if firstMessage.subject is null/undefined. Consider defaulting subject to an empty string (or guarding) before checking .length.

Suggested change
const subject = firstMessage.subject;
const subject = firstMessage.subject ?? "";

🚀 Want me to fix this? Reply ex: "fix it for me".

Comment on lines +572 to +574
<span className="shrink-0 text-xs text-muted-foreground">
{formatShortDate(new Date(date))}
</span>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider guarding firstMessage.date before new Date(...)/formatShortDate. If missing/invalid, skip the timestamp or show a placeholder to avoid Invalid Date/NaN.

-                <span className="shrink-0 text-xs text-muted-foreground">
-                  {formatShortDate(new Date(date))}
-                </span>
+                {(() => {
+                  if (!date) return null;
+                  const d = new Date(date);
+                  return isNaN(d.getTime()) ? null : (
+                    <span className="shrink-0 text-xs text-muted-foreground">
+                      {formatShortDate(d)}
+                    </span>
+                  );
+                })()}

🚀 Want me to fix this? Reply ex: "fix it for me".

Copy link
Copy Markdown
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.

1 issue found across 24 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/web/app/utm.tsx">

<violation number="1" location="apps/web/app/utm.tsx:18">
P1: Cookie injection vulnerability: URL parameter values are interpolated directly into cookie strings without encoding. An attacker could craft a malicious URL (e.g., `?utm_source=x; Domain=.evil.com`) to inject arbitrary cookie attributes. Use `encodeURIComponent()` to safely encode the values.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


if (utmSource)
document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/`;
document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/; SameSite=Lax; Secure`;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 6, 2026

Choose a reason for hiding this comment

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

P1: Cookie injection vulnerability: URL parameter values are interpolated directly into cookie strings without encoding. An attacker could craft a malicious URL (e.g., ?utm_source=x; Domain=.evil.com) to inject arbitrary cookie attributes. Use encodeURIComponent() to safely encode the values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/utm.tsx, line 18:

<comment>Cookie injection vulnerability: URL parameter values are interpolated directly into cookie strings without encoding. An attacker could craft a malicious URL (e.g., `?utm_source=x; Domain=.evil.com`) to inject arbitrary cookie attributes. Use `encodeURIComponent()` to safely encode the values.</comment>

<file context>
@@ -15,17 +15,17 @@ function setUtmCookies() {
 
   if (utmSource)
-    document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/`;
+    document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/; SameSite=Lax; Secure`;
   if (utmMedium)
-    document.cookie = `utm_medium=${utmMedium}; expires=${expires}; path=/`;
</file context>
Suggested change
document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/; SameSite=Lax; Secure`;
document.cookie = `utm_source=${encodeURIComponent(utmSource)}; expires=${expires}; path=/; SameSite=Lax; Secure`;
Fix with Cubic

Copy link
Copy Markdown
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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/app/api/user/categorize/senders/batch/handle-batch.ts (1)

44-53: Remove redundant database query and clarify security assumptions for this cron endpoint.

This is a Upstash QStash cron endpoint (protected by verifySignatureAppRouter signature verification), not a user-facing API route, so it doesn't require withEmailAccount or withAuth middleware. However, there are two issues:

  1. Redundant query: Lines 44-53 fetch emailAccount.account which is already available from validateUserAndAiAccess at line 38. Remove this duplicate query and reuse the data from userResult.

  2. validateUserAndAiAccess design weakness: This function queries by emailAccountId only without user scoping. While safe here (cron endpoint from trusted Upstash), the function lacks built-in ownership validation and could pose IDOR risks if reused in other contexts (e.g., user-facing API routes). Consider updating validateUserAndAiAccess to require and validate userId ownership.

🤖 Fix all issues with AI Agents
In
@apps/web/app/(app)/[emailAccountId]/bulk-archive/AutoCategorizationSetup.tsx:
- Around line 27-51: enableFeature sets setIsBulkCategorizing(true) but never
clears it when there are zero uncategorized senders; update enableFeature (the
async callback) to call setIsBulkCategorizing(false) when
result?.data?.totalUncategorizedSenders is 0 or falsy (before or immediately
after showing the "No uncategorized senders found." toast), keeping the existing
error and finally handling intact so the UI doesn't remain stuck in the
categorizing state.

In @apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx:
- Around line 23-27: Replace the direct document.cookie assignment in
setOnboardingReturnCookie with the centralized cookie utility used elsewhere
(the auth cookie helper), i.e., call the utility to set
CALENDAR_ONBOARDING_RETURN_COOKIE to encodeURIComponent(onboardingReturnPath)
and pass options for path:'/', maxAge:180, sameSite:'Lax', secure:true; update
references in ConnectCalendar.tsx to import and use that helper instead of
manipulating document.cookie directly.

In @apps/web/app/(app)/[emailAccountId]/quick-bulk-archive/BulkArchiveTab.tsx:
- Around line 583-586: The Link element rendering the email URL (href from
getEmailUrl(thread.id, userEmail, provider)) uses target="_blank" but is missing
rel="noopener noreferrer"; update the Link in BulkArchiveTab.tsx to include
rel="noopener noreferrer" alongside target="_blank" (matching the implementation
in BulkArchiveCards.tsx) to prevent the opened page from accessing
window.opener.

In @apps/web/app/utm.tsx:
- Around line 17-28: Replace the six direct document.cookie assignments in
utm.tsx with calls to the centralized cookie utility (e.g., setCookie) from the
auth-cookies module: for each key (utm_source, utm_medium, utm_campaign,
utm_term, affiliate, referral_code) call setCookie(name, value, { maxAge:
<seconds>, path: '/', sameSite: 'Lax', secure: true }) instead of using expires;
remove the expires usage and convert the expiration to max-age seconds to match
the rest of the PR; ensure you import the utility (setCookie) and only set
cookies when the corresponding variable (utmSource, utmMedium, utmCampaign,
utmTerm, affiliate, referralCode) is present.

In @apps/web/components/ui/sidebar.tsx:
- Around line 90-91: In the Sidebar component (sidebar.tsx) there are two
identical comments "// This sets the cookie to keep the sidebar state."
repeated; remove the duplicated line so only one instance of that comment
remains (preserve surrounding whitespace/indentation and any nearby comments or
code in the render/return block where the cookie-setting logic exists).
- Around line 92-94: The code is directly setting cookies via document.cookie in
the loop over sidebarNames (using sidebarNames, openState and
SIDEBAR_COOKIE_MAX_AGE); replace that direct manipulation with the centralized
cookie utility from auth-cookies.ts — call the utility’s setter (e.g., setCookie
or equivalent) for each sidebarName and pass the same cookie name/value and
attributes (path=/, max-age=SIDEBAR_COOKIE_MAX_AGE, SameSite=Lax, Secure) so the
persistence logic is centralized and document.cookie is removed from the
component.

In @apps/web/utils/auth-cookies.ts:
- Line 10: Replace the direct document.cookie assignment in auth-cookies.ts with
a call to a centralized cookie utility: create apps/web/utils/cookie-manager.ts
exposing setCookie(name,value,options), getCookie(name) and
deleteCookie(name,path) that encapsulate SameSite, Secure, path and max-age
logic, then import and call deleteCookie('auth_error') (or
setCookie('auth_error','',{ maxAge:0 })) from the existing function instead of
assigning document.cookie directly.

In @apps/web/utils/cookies.ts:
- Around line 11-13: The markOnboardingAsCompleted function currently sets
document.cookie max-age to Number.MAX_SAFE_INTEGER which is excessively large
and may cause browser/PDPR issues; change it to a reasonable long duration
(e.g., ONE_YEAR_SECONDS = 365 * 24 * 60 * 60 or TWO_YEARS_SECONDS = 2 * 365 * 24
* 60 * 60) and use that constant in the template for max-age instead of
Number.MAX_SAFE_INTEGER, keeping the cookie name parameter cookie and preserving
path/SameSite/Secure attributes.
- Around line 11-21: Replace the direct document.cookie assignments in
markOnboardingAsCompleted, setInvitationCookie, and clearInvitationCookie with
calls to the centralized cookie utility exported from auth-cookies.ts: import
the cookie helper and use its set and delete functions (or equivalent names
provided by auth-cookies.ts) to set the cookie name/value and options (path,
max-age for Number.MAX_SAFE_INTEGER and 7*24*60*60, SameSite=Lax, Secure) and to
clear the cookie using an expiration option; keep using the INVITATION_COOKIE
symbol and preserve the exact semantics for persistence and deletion.
🧹 Nitpick comments (9)
apps/web/app/(app)/early-access/page.tsx (1)

24-68: Consider removing commented-out code or using feature flags.

The fragment correctly wraps multiple elements, but two Card components are commented out (lines 40-53 and 54-67). Commented code should typically be removed since version control maintains history. If these features are planned for gradual rollout, consider using feature flags with conditional rendering instead.

Alternative approach with feature flags

If these are experimental features for gradual rollout:

{isGoogleProvider(provider) && (
  <>
    <Card>
      <CardHeader>
        <CardTitle>Sender Categories</CardTitle>
        <CardDescription>
          Sender Categories is a feature that allows you to categorize
          emails by sender, and take bulk actions or apply rules to
          them.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <Button asChild>
          <Link href="/smart-categories">Sender Categories</Link>
        </Button>
      </CardContent>
    </Card>
    {useBulkArchiveEnabled() && (
      <Card>
        <CardHeader>
          <CardTitle>Bulk Archive</CardTitle>
          <CardDescription>
            Archive emails from multiple senders at once, organized by
            category.
          </CardDescription>
        </CardHeader>
        <CardContent>
          <Button asChild>
            <Link href="/bulk-archive">Bulk Archive</Link>
          </Button>
        </CardContent>
      </Card>
    )}
    {useQuickBulkArchiveEnabled() && (
      <Card>
        <CardHeader>
          <CardTitle>Quick Bulk Archive</CardTitle>
          <CardDescription>
            Quickly archive emails from multiple senders at once, grouped
            by AI confidence level.
          </CardDescription>
        </CardHeader>
        <CardContent>
          <Button asChild>
            <Link href="/quick-bulk-archive">Quick Bulk Archive</Link>
          </Button>
        </CardContent>
      </Card>
    )}
  </>
)}

Based on learnings, these features would automatically appear on the Early Access page through the EarlyAccessFeatures component when enabled.

apps/web/utils/cookies.ts (1)

11-21: Inconsistent cookie expiration approaches.

The file uses two different methods for setting cookie expiration:

  • markOnboardingAsCompleted and setInvitationCookie use max-age (lines 12, 16)
  • clearInvitationCookie uses expires with epoch date (line 20)

While both approaches work, using max-age=0 is the more modern and consistent approach for deleting cookies. Consider standardizing on max-age throughout.

🔎 Standardize on max-age
 export function clearInvitationCookie() {
-  document.cookie = `${INVITATION_COOKIE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax; Secure`;
+  document.cookie = `${INVITATION_COOKIE}=; path=/; max-age=0; SameSite=Lax; Secure`;
 }
apps/web/components/BulkArchiveCards.tsx (2)

148-169: Consider parallelizing archive operations for better performance.

The sequential await inside the loop processes senders one at a time. If the archive queue can handle concurrent additions, this could be significantly faster with Promise.all or batched processing.

🔎 Optional parallel approach
 const archiveCategory = async (categoryName: string, e: React.MouseEvent) => {
   e.stopPropagation();
   const senders = groupedEmails[categoryName] || [];
   const selectedToArchive = senders.filter(
     (s) => selectedSenders[s.address] !== false,
   );

   try {
-    for (const sender of selectedToArchive) {
-      await addToArchiveSenderQueue({
-        sender: sender.address,
-        emailAccountId,
-      });
-    }
+    await Promise.all(
+      selectedToArchive.map((sender) =>
+        addToArchiveSenderQueue({
+          sender: sender.address,
+          emailAccountId,
+        }),
+      ),
+    );

     setArchivedCategories((prev) => ({ ...prev, [categoryName]: true }));
   } catch (_error) {
     toastError({
       description: "Failed to archive some senders. Please try again.",
     });
   }
 };

528-531: Redundant slice operation.

The useThreads hook already limits results to 5 via limit: 5. The .slice(0, 5) on line 531 is unnecessary.

🔎 Proposed fix
-       {data.threads.slice(0, 5).map((thread) => {
+       {data.threads.map((thread) => {
apps/web/app/(app)/[emailAccountId]/quick-bulk-archive/BulkArchiveTab.tsx (2)

78-103: Double computation of archive candidates.

getArchiveCandidates(emailGroups) is called in the state initializer (line 83) and again in useMemo (line 101). Consider storing the initial candidates or restructuring to avoid redundant computation.

🔎 Proposed optimization
+const initialCandidates = useMemo(
+  () => getArchiveCandidates(emailGroups),
+  [emailGroups],
+);

 const [selectedSenders, setSelectedSenders] = useState<
   Record<string, boolean>
 >(() => {
   // Pre-select high and medium confidence senders
   const initial: Record<string, boolean> = {};
-  const candidates = getArchiveCandidates(emailGroups);
-  for (const candidate of candidates) {
+  for (const candidate of initialCandidates) {
     initial[candidate.address] =
       candidate.confidence === "high" || candidate.confidence === "medium";
   }
   return initial;
 });
 // ...
-const candidates = useMemo(
-  () => getArchiveCandidates(emailGroups),
-  [emailGroups],
-);
+const candidates = initialCandidates;

Note: This requires careful handling since useMemo runs after state initialization. The current approach may be intentional to ensure state initializer has access to candidates. If so, this is acceptable as-is.


430-497: Consider extracting shared subcomponents to reduce duplication.

SenderRow, ArchiveStatus, and ExpandedEmails are nearly identical between this file and BulkArchiveCards.tsx. Consider extracting them into shared components under @/components/bulk-archive/ to improve maintainability.

Also applies to: 499-526, 528-619

apps/web/utils/bulk-archive/get-archive-candidates.ts (1)

36-40: Unnecessary type assertions.

The as ConfidenceLevel casts are redundant since the string literals "high", "medium", and "low" already satisfy the ConfidenceLevel type. TypeScript can infer these correctly.

🔎 Proposed simplification
       return {
         ...group,
-        confidence: "high" as ConfidenceLevel,
+        confidence: "high",
         reason: "Marketing / Promotional",
       };
     }
     // ... similar for medium and low

Also applies to: 50-54, 58-62

apps/web/app/(app)/[emailAccountId]/quick-bulk-archive/page.tsx (1)

49-61: Consider extracting data transformation logic.

The emailGroups transformation (sort + map with category lookup) is duplicated between this file and BulkArchiveContent.tsx. Also, performing this computation inline within JSX reduces readability.

🔎 Proposed extraction
+// Consider extracting to a shared utility, e.g., @/utils/bulk-archive/build-email-groups.ts
+const emailGroups = sortBy(senders, (sender) => sender.category?.name).map(
+  (sender) => ({
+    address: sender.email,
+    category:
+      categories.find((category) => category.id === sender.category?.id) ||
+      null,
+  }),
+);

 <ClientOnly>
   <BulkArchiveTab
-    emailGroups={sortBy(senders, (sender) => sender.category?.name).map(
-      (sender) => ({
-        address: sender.email,
-        category:
-          categories.find(
-            (category) => category.id === sender.category?.id,
-          ) || null,
-      }),
-    )}
+    emailGroups={emailGroups}
   />
 </ClientOnly>
apps/web/app/api/user/categorize/senders/batch/handle-batch.ts (1)

95-131: Consider removing the commented-out code.

The large commented block (lines 95-131) should be removed to improve code clarity. If this logic is needed in the future, it's preserved in version control history.

Comment on lines +27 to +51
const enableFeature = useCallback(async () => {
setIsEnabling(true);
setIsBulkCategorizing(true);

try {
const result = await bulkCategorizeSendersAction(emailAccountId);

if (result?.serverError) {
throw new Error(result.serverError);
}

toast.success(
result?.data?.totalUncategorizedSenders
? `Categorizing ${result.data.totalUncategorizedSenders} senders... This may take a few minutes.`
: "No uncategorized senders found.",
);
} catch (error) {
toast.error(
`Failed to enable feature: ${error instanceof Error ? error.message : "Unknown error"}`,
);
setIsBulkCategorizing(false);
} finally {
setIsEnabling(false);
}
}, [emailAccountId, setIsBulkCategorizing]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear the categorizing flag when no senders are found.

When totalUncategorizedSenders is 0, the success toast displays "No uncategorized senders found", but setIsBulkCategorizing(true) from Line 29 is never cleared. This leaves the UI in a perpetual "categorizing" state even though no work is being performed.

🔎 Proposed fix
     try {
       const result = await bulkCategorizeSendersAction(emailAccountId);

       if (result?.serverError) {
         throw new Error(result.serverError);
       }

-      toast.success(
-        result?.data?.totalUncategorizedSenders
-          ? `Categorizing ${result.data.totalUncategorizedSenders} senders... This may take a few minutes.`
-          : "No uncategorized senders found.",
-      );
+      if (result?.data?.totalUncategorizedSenders) {
+        toast.success(
+          `Categorizing ${result.data.totalUncategorizedSenders} senders... This may take a few minutes.`
+        );
+      } else {
+        toast.success("No uncategorized senders found.");
+        setIsBulkCategorizing(false);
+      }
     } catch (error) {
📝 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
const enableFeature = useCallback(async () => {
setIsEnabling(true);
setIsBulkCategorizing(true);
try {
const result = await bulkCategorizeSendersAction(emailAccountId);
if (result?.serverError) {
throw new Error(result.serverError);
}
toast.success(
result?.data?.totalUncategorizedSenders
? `Categorizing ${result.data.totalUncategorizedSenders} senders... This may take a few minutes.`
: "No uncategorized senders found.",
);
} catch (error) {
toast.error(
`Failed to enable feature: ${error instanceof Error ? error.message : "Unknown error"}`,
);
setIsBulkCategorizing(false);
} finally {
setIsEnabling(false);
}
}, [emailAccountId, setIsBulkCategorizing]);
const enableFeature = useCallback(async () => {
setIsEnabling(true);
setIsBulkCategorizing(true);
try {
const result = await bulkCategorizeSendersAction(emailAccountId);
if (result?.serverError) {
throw new Error(result.serverError);
}
if (result?.data?.totalUncategorizedSenders) {
toast.success(
`Categorizing ${result.data.totalUncategorizedSenders} senders... This may take a few minutes.`
);
} else {
toast.success("No uncategorized senders found.");
setIsBulkCategorizing(false);
}
} catch (error) {
toast.error(
`Failed to enable feature: ${error instanceof Error ? error.message : "Unknown error"}`,
);
setIsBulkCategorizing(false);
} finally {
setIsEnabling(false);
}
}, [emailAccountId, setIsBulkCategorizing]);
🤖 Prompt for AI Agents
In @apps/web/app/(app)/[emailAccountId]/bulk-archive/AutoCategorizationSetup.tsx
around lines 27 - 51, enableFeature sets setIsBulkCategorizing(true) but never
clears it when there are zero uncategorized senders; update enableFeature (the
async callback) to call setIsBulkCategorizing(false) when
result?.data?.totalUncategorizedSenders is 0 or falsy (before or immediately
after showing the "No uncategorized senders found." toast), keeping the existing
error and finally handling intact so the UI doesn't remain stuck in the
categorizing state.

Comment on lines 23 to 27
const setOnboardingReturnCookie = () => {
if (onboardingReturnPath) {
document.cookie = `${CALENDAR_ONBOARDING_RETURN_COOKIE}=${encodeURIComponent(onboardingReturnPath)}; path=/; max-age=180`;
document.cookie = `${CALENDAR_ONBOARDING_RETURN_COOKIE}=${encodeURIComponent(onboardingReturnPath)}; path=/; max-age=180; SameSite=Lax; Secure`;
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Apply centralized cookie utility here as well.

The security attributes addition is good, but this is another instance of direct document.cookie assignment that should use the centralized cookie utility suggested in the review of apps/web/utils/auth-cookies.ts.

🔎 Refactor with cookie utility
+import { setCookie } from '@/utils/cookie-manager';
 import { CALENDAR_ONBOARDING_RETURN_COOKIE } from "@/utils/calendar/constants";

 const setOnboardingReturnCookie = () => {
   if (onboardingReturnPath) {
-    document.cookie = `${CALENDAR_ONBOARDING_RETURN_COOKIE}=${encodeURIComponent(onboardingReturnPath)}; path=/; max-age=180; SameSite=Lax; Secure`;
+    setCookie(CALENDAR_ONBOARDING_RETURN_COOKIE, onboardingReturnPath, {
+      maxAge: 180,
+    });
   }
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @apps/web/app/(app)/[emailAccountId]/calendars/ConnectCalendar.tsx around
lines 23 - 27, Replace the direct document.cookie assignment in
setOnboardingReturnCookie with the centralized cookie utility used elsewhere
(the auth cookie helper), i.e., call the utility to set
CALENDAR_ONBOARDING_RETURN_COOKIE to encodeURIComponent(onboardingReturnPath)
and pass options for path:'/', maxAge:180, sameSite:'Lax', secure:true; update
references in ConnectCalendar.tsx to import and use that helper instead of
manipulating document.cookie directly.

Comment on lines +583 to +586
<Link
href={getEmailUrl(thread.id, userEmail, provider)}
target="_blank"
className="mr-2 flex flex-1 items-center gap-3 rounded-md px-2 py-2 transition-colors hover:bg-muted/50"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add rel="noopener noreferrer" to external links.

Links with target="_blank" should include rel="noopener noreferrer" to prevent the opened page from accessing the opener via window.opener. This is present in BulkArchiveCards.tsx (line 547) but missing here.

🔎 Proposed fix
 <Link
   href={getEmailUrl(thread.id, userEmail, provider)}
   target="_blank"
+  rel="noopener noreferrer"
   className="mr-2 flex flex-1 items-center gap-3 rounded-md px-2 py-2 transition-colors hover:bg-muted/50"
 >
📝 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
<Link
href={getEmailUrl(thread.id, userEmail, provider)}
target="_blank"
className="mr-2 flex flex-1 items-center gap-3 rounded-md px-2 py-2 transition-colors hover:bg-muted/50"
<Link
href={getEmailUrl(thread.id, userEmail, provider)}
target="_blank"
rel="noopener noreferrer"
className="mr-2 flex flex-1 items-center gap-3 rounded-md px-2 py-2 transition-colors hover:bg-muted/50"
>
🤖 Prompt for AI Agents
In @apps/web/app/(app)/[emailAccountId]/quick-bulk-archive/BulkArchiveTab.tsx
around lines 583 - 586, The Link element rendering the email URL (href from
getEmailUrl(thread.id, userEmail, provider)) uses target="_blank" but is missing
rel="noopener noreferrer"; update the Link in BulkArchiveTab.tsx to include
rel="noopener noreferrer" alongside target="_blank" (matching the implementation
in BulkArchiveCards.tsx) to prevent the opened page from accessing
window.opener.

Comment on lines 17 to +28
if (utmSource)
document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/`;
document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/; SameSite=Lax; Secure`;
if (utmMedium)
document.cookie = `utm_medium=${utmMedium}; expires=${expires}; path=/`;
document.cookie = `utm_medium=${utmMedium}; expires=${expires}; path=/; SameSite=Lax; Secure`;
if (utmCampaign)
document.cookie = `utm_campaign=${utmCampaign}; expires=${expires}; path=/`;
document.cookie = `utm_campaign=${utmCampaign}; expires=${expires}; path=/; SameSite=Lax; Secure`;
if (utmTerm)
document.cookie = `utm_term=${utmTerm}; expires=${expires}; path=/`;
document.cookie = `utm_term=${utmTerm}; expires=${expires}; path=/; SameSite=Lax; Secure`;
if (affiliate)
document.cookie = `affiliate=${affiliate}; expires=${expires}; path=/`;
document.cookie = `affiliate=${affiliate}; expires=${expires}; path=/; SameSite=Lax; Secure`;
if (referralCode)
document.cookie = `referral_code=${referralCode}; expires=${expires}; path=/`;
document.cookie = `referral_code=${referralCode}; expires=${expires}; path=/; SameSite=Lax; Secure`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Multiple document.cookie violations - consolidate with cookie utility.

Six instances of direct document.cookie assignment violate the coding guideline. Additionally, this file uses expires for cookie expiration while other files in this PR use max-age, creating inconsistency.

Refactor to use the centralized cookie utility suggested in apps/web/utils/auth-cookies.ts, which will also standardize the expiration approach.

🔎 Refactor with cookie utility
+import { setCookie } from '@/utils/cookie-manager';

 function setUtmCookies() {
   const urlParams = new URLSearchParams(window.location.search);
   const utmSource = urlParams.get("utm_source");
   const utmMedium = urlParams.get("utm_medium");
   const utmCampaign = urlParams.get("utm_campaign");
   const utmTerm = urlParams.get("utm_term");
   const affiliate = urlParams.get("aff_ref");
   const referralCode = urlParams.get("ref");

-  // expires in 30 days
-  const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toUTCString();
+  const maxAge = 30 * 24 * 60 * 60; // 30 days in seconds

-  if (utmSource)
-    document.cookie = `utm_source=${utmSource}; expires=${expires}; path=/; SameSite=Lax; Secure`;
-  if (utmMedium)
-    document.cookie = `utm_medium=${utmMedium}; expires=${expires}; path=/; SameSite=Lax; Secure`;
-  if (utmCampaign)
-    document.cookie = `utm_campaign=${utmCampaign}; expires=${expires}; path=/; SameSite=Lax; Secure`;
-  if (utmTerm)
-    document.cookie = `utm_term=${utmTerm}; expires=${expires}; path=/; SameSite=Lax; Secure`;
-  if (affiliate)
-    document.cookie = `affiliate=${affiliate}; expires=${expires}; path=/; SameSite=Lax; Secure`;
-  if (referralCode)
-    document.cookie = `referral_code=${referralCode}; expires=${expires}; path=/; SameSite=Lax; Secure`;
+  if (utmSource) setCookie('utm_source', utmSource, { maxAge });
+  if (utmMedium) setCookie('utm_medium', utmMedium, { maxAge });
+  if (utmCampaign) setCookie('utm_campaign', utmCampaign, { maxAge });
+  if (utmTerm) setCookie('utm_term', utmTerm, { maxAge });
+  if (affiliate) setCookie('affiliate', affiliate, { maxAge });
+  if (referralCode) setCookie('referral_code', referralCode, { maxAge });
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @apps/web/app/utm.tsx around lines 17 - 28, Replace the six direct
document.cookie assignments in utm.tsx with calls to the centralized cookie
utility (e.g., setCookie) from the auth-cookies module: for each key
(utm_source, utm_medium, utm_campaign, utm_term, affiliate, referral_code) call
setCookie(name, value, { maxAge: <seconds>, path: '/', sameSite: 'Lax', secure:
true }) instead of using expires; remove the expires usage and convert the
expiration to max-age seconds to match the rest of the PR; ensure you import the
utility (setCookie) and only set cookies when the corresponding variable
(utmSource, utmMedium, utmCampaign, utmTerm, affiliate, referralCode) is
present.

Comment on lines 90 to 91
// This sets the cookie to keep the sidebar state.
// This sets the cookie to keep the sidebar state.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove duplicate comment.

Lines 90-91 contain an identical comment that appears twice. Remove one instance.

🔎 Proposed fix
     }

     // This sets the cookie to keep the sidebar state.
-    // This sets the cookie to keep the sidebar state.
     sidebarNames.forEach((sidebarName) => {
📝 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
// This sets the cookie to keep the sidebar state.
// This sets the cookie to keep the sidebar state.
}
// This sets the cookie to keep the sidebar state.
sidebarNames.forEach((sidebarName) => {
🤖 Prompt for AI Agents
In @apps/web/components/ui/sidebar.tsx around lines 90 - 91, In the Sidebar
component (sidebar.tsx) there are two identical comments "// This sets the
cookie to keep the sidebar state." repeated; remove the duplicated line so only
one instance of that comment remains (preserve surrounding
whitespace/indentation and any nearby comments or code in the render/return
block where the cookie-setting logic exists).

Comment on lines 92 to 94
sidebarNames.forEach((sidebarName) => {
document.cookie = `${sidebarName}:state=${openState.includes(sidebarName)}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
document.cookie = `${sidebarName}:state=${openState.includes(sidebarName)}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}; SameSite=Lax; Secure`;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Apply centralized cookie utility to sidebar state persistence.

This UI component directly manipulates cookies for state persistence, which violates the coding guideline. Refactor to use the centralized cookie utility suggested in apps/web/utils/auth-cookies.ts.

🔎 Refactor with cookie utility
+import { setCookie } from '@/utils/cookie-manager';

 // This sets the cookie to keep the sidebar state.
 sidebarNames.forEach((sidebarName) => {
-  document.cookie = `${sidebarName}:state=${openState.includes(sidebarName)}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}; SameSite=Lax; Secure`;
+  setCookie(`${sidebarName}:state`, String(openState.includes(sidebarName)), {
+    maxAge: SIDEBAR_COOKIE_MAX_AGE,
+  });
 });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @apps/web/components/ui/sidebar.tsx around lines 92 - 94, The code is
directly setting cookies via document.cookie in the loop over sidebarNames
(using sidebarNames, openState and SIDEBAR_COOKIE_MAX_AGE); replace that direct
manipulation with the centralized cookie utility from auth-cookies.ts — call the
utility’s setter (e.g., setCookie or equivalent) for each sidebarName and pass
the same cookie name/value and attributes (path=/,
max-age=SIDEBAR_COOKIE_MAX_AGE, SameSite=Lax, Secure) so the persistence logic
is centralized and document.cookie is removed from the component.


if (authErrorCookie) {
document.cookie = "auth_error=; path=/; max-age=0";
document.cookie = "auth_error=; path=/; max-age=0; SameSite=Lax; Secure";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Security improvement approved, but consider refactoring to a cookie utility.

The addition of SameSite=Lax; Secure attributes improves security by preventing cookie transmission over unencrypted HTTP and providing CSRF protection.

However, this violates the coding guideline: "Don't assign directly to document.cookie." Based on learnings, direct cookie manipulation should be avoided across the codebase. Consider creating a centralized cookie utility module (e.g., utils/cookie-manager.ts) with functions like setCookie(), getCookie(), and deleteCookie() that encapsulate the security attributes and provide a consistent API.

🔎 Suggested cookie utility pattern

Create a new file apps/web/utils/cookie-manager.ts:

export function setCookie(
  name: string,
  value: string,
  options: {
    maxAge?: number;
    expires?: Date;
    path?: string;
    secure?: boolean;
    sameSite?: 'Strict' | 'Lax' | 'None';
  } = {}
) {
  const {
    maxAge,
    expires,
    path = '/',
    secure = true,
    sameSite = 'Lax',
  } = options;

  let cookie = `${name}=${encodeURIComponent(value)}`;
  if (maxAge !== undefined) cookie += `; max-age=${maxAge}`;
  if (expires) cookie += `; expires=${expires.toUTCString()}`;
  cookie += `; path=${path}`;
  cookie += `; SameSite=${sameSite}`;
  if (secure) cookie += '; Secure';

  document.cookie = cookie;
}

export function deleteCookie(name: string, path = '/') {
  setCookie(name, '', { maxAge: 0, path });
}

export function getCookie(name: string): string | undefined {
  return document.cookie
    .split('; ')
    .find(row => row.startsWith(`${name}=`))
    ?.split('=')
    .slice(1)
    .join('=');
}

Then refactor this function:

+import { getCookie, deleteCookie } from '@/utils/cookie-manager';
+
 export function getAndClearAuthErrorCookie(): string | undefined {
-  const authErrorCookie = document.cookie
-    .split("; ")
-    .find((row) => row.startsWith("auth_error="))
-    ?.split("=")
-    .slice(1)
-    .join("=");
+  const authErrorCookie = getCookie("auth_error");

   if (authErrorCookie) {
-    document.cookie = "auth_error=; path=/; max-age=0; SameSite=Lax; Secure";
+    deleteCookie("auth_error");
   }

   return authErrorCookie;
 }

Based on learnings, this pattern should be applied across all cookie operations in the codebase.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @apps/web/utils/auth-cookies.ts at line 10, Replace the direct
document.cookie assignment in auth-cookies.ts with a call to a centralized
cookie utility: create apps/web/utils/cookie-manager.ts exposing
setCookie(name,value,options), getCookie(name) and deleteCookie(name,path) that
encapsulate SameSite, Secure, path and max-age logic, then import and call
deleteCookie('auth_error') (or setCookie('auth_error','',{ maxAge:0 })) from the
existing function instead of assigning document.cookie directly.

Comment on lines 11 to 13
export function markOnboardingAsCompleted(cookie: string) {
document.cookie = `${cookie}=true; path=/; max-age=${Number.MAX_SAFE_INTEGER}; SameSite=Lax`;
document.cookie = `${cookie}=true; path=/; max-age=${Number.MAX_SAFE_INTEGER}; SameSite=Lax; Secure`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Question the use of Number.MAX_SAFE_INTEGER for cookie expiration.

Using Number.MAX_SAFE_INTEGER as the max-age creates an extremely long-lived cookie (approximately 285 million years). While the intent seems to be "permanent" storage, this approach has potential issues:

  • Some browsers may handle such large values inconsistently
  • Privacy regulations (GDPR, CCPA) may require reasonable cookie lifetimes
  • Standard practice is to use a more reasonable long duration (e.g., 1-2 years)

Consider using a more conventional long duration like 365 * 24 * 60 * 60 (1 year) or 2 * 365 * 24 * 60 * 60 (2 years).

🔎 Suggested fix
 export function markOnboardingAsCompleted(cookie: string) {
-  document.cookie = `${cookie}=true; path=/; max-age=${Number.MAX_SAFE_INTEGER}; SameSite=Lax; Secure`;
+  document.cookie = `${cookie}=true; path=/; max-age=${365 * 24 * 60 * 60}; SameSite=Lax; Secure`;
 }
🤖 Prompt for AI Agents
In @apps/web/utils/cookies.ts around lines 11 - 13, The
markOnboardingAsCompleted function currently sets document.cookie max-age to
Number.MAX_SAFE_INTEGER which is excessively large and may cause browser/PDPR
issues; change it to a reasonable long duration (e.g., ONE_YEAR_SECONDS = 365 *
24 * 60 * 60 or TWO_YEARS_SECONDS = 2 * 365 * 24 * 60 * 60) and use that
constant in the template for max-age instead of Number.MAX_SAFE_INTEGER, keeping
the cookie name parameter cookie and preserving path/SameSite/Secure attributes.

Comment on lines 11 to 21
export function markOnboardingAsCompleted(cookie: string) {
document.cookie = `${cookie}=true; path=/; max-age=${Number.MAX_SAFE_INTEGER}; SameSite=Lax`;
document.cookie = `${cookie}=true; path=/; max-age=${Number.MAX_SAFE_INTEGER}; SameSite=Lax; Secure`;
}

export function setInvitationCookie(invitationId: string) {
document.cookie = `${INVITATION_COOKIE}=${invitationId}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
document.cookie = `${INVITATION_COOKIE}=${invitationId}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax; Secure`;
}

export function clearInvitationCookie() {
document.cookie = `${INVITATION_COOKIE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`;
document.cookie = `${INVITATION_COOKIE}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax; Secure`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Apply centralized cookie utility to this file.

This file already serves as a cookie utilities module but still uses direct document.cookie assignments. These functions should be refactored to use (or become part of) the centralized cookie management utility suggested in apps/web/utils/auth-cookies.ts.

🤖 Prompt for AI Agents
In @apps/web/utils/cookies.ts around lines 11 - 21, Replace the direct
document.cookie assignments in markOnboardingAsCompleted, setInvitationCookie,
and clearInvitationCookie with calls to the centralized cookie utility exported
from auth-cookies.ts: import the cookie helper and use its set and delete
functions (or equivalent names provided by auth-cookies.ts) to set the cookie
name/value and options (path, max-age for Number.MAX_SAFE_INTEGER and
7*24*60*60, SameSite=Lax, Secure) and to clear the cookie using an expiration
option; keep using the INVITATION_COOKIE symbol and preserve the exact semantics
for persistence and deletion.

@elie222 elie222 closed this Jan 6, 2026
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