Skip to content

Mobile app prerequisites#189

Merged
acarlson33 merged 8 commits into
mainfrom
05-10-mobile_app_prerequisites
May 13, 2026
Merged

Mobile app prerequisites#189
acarlson33 merged 8 commits into
mainfrom
05-10-mobile_app_prerequisites

Conversation

@acarlson33

@acarlson33 acarlson33 commented May 11, 2026

Copy link
Copy Markdown
Owner

This pull request introduces several improvements and updates across the codebase, focusing on configuration flexibility, dependency updates, test reliability, and minor UI accessibility enhancements. The most significant changes are grouped below:

Configuration and Environment Variables:

  • Expanded .env.local.example with new environment variables for mobile app support, rate limiting, and CORS configuration, making it easier to customize deployments and enhance security.

Dependency Updates:

  • Upgraded multiple dependencies and devDependencies in package.json (including @tanstack/react-query, posthog-js, posthog-node, yaml, and several @types and linting packages) to their latest versions for improved stability and compatibility. Also added new scripts for dependency management. [1] [2] [3] [4] [5]

Testing Reliability and Consistency:

  • Refactored test mocks in several test files to use .mockResolvedValue() instead of custom async functions with explicit undefined returns, ensuring more consistent and modern mocking patterns. [1] [2] [3] [4] [5] [6] [7] [8] [9]
  • Updated API route tests for /api/servers/public to use a proper NextRequest object, improving test accuracy and alignment with actual usage. [1] [2] [3] [4] [5] [6] [7] [8]

UI and Accessibility Improvements:

  • Improved accessibility in the login form by adding aria-hidden="true" to decorative icons and simplified conditional rendering for cleaner code. [1] [2] [3] [4]
  • Minor logic simplification in login redirect path handling.

Code Quality:

  • Added a missing logger import in the login page for better observability.

These changes collectively enhance maintainability, security, test reliability, and user experience.

- Updated tests for GET /api/servers/public to use NextRequest for request creation.
- Simplified mock implementations in chat hooks tests for better readability.
- Enhanced error handling in notifications and chat hooks to log errors more effectively.
- Improved API response structure by changing 'total' to 'count' in public servers response.
- Added CORS and rate limiting logic to proxy handling for API routes.
- Removed middleware file and integrated its functionality into the proxy.
- Updated various components to improve accessibility and user experience.
- Refactored rate limit configuration to use parsed environment variables.
- Cleaned up unused variables and improved code readability across multiple files.
@appwrite

appwrite Bot commented May 11, 2026

Copy link
Copy Markdown

Firepit

Project ID: 68b230a0002245833242

Sites (1)
Site Status Logs Preview QR
 firepit
68eed9c6001f50d8f260
Ready Ready View Logs Preview URL QR Code

Tip

Preview deployments create instant URLs for every branch and commit

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Rate limit exceeded

@acarlson33 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 8 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8cf2b49f-4448-4ca3-819e-d7c96dcee13e

📥 Commits

Reviewing files that changed from the base of the PR and between a0dc2a2 and 8f0cac7.

📒 Files selected for processing (4)
  • src/app/api/servers/[serverId]/mentionable-roles/route.ts
  • src/components/chat-input.tsx
  • src/lib/rate-limit.ts
  • src/proxy.ts
📝 Walkthrough

Walkthrough

Adds header-first session auth and a SessionUser type, centralizes typed API error responses, implements in-memory rate limiting and CORS-aware API proxying, hardens realtime subscription lifecycle and pool teardown, converts public servers API to cursor pagination with search, extends instance/version endpoints, and updates many hooks, UI bits, tests, and deps (v1.8.0 → v1.9.0).

Changes

Auth & API error contract

Layer / File(s) Summary
SessionUser + header/cookie session resolution
src/lib/auth-server.ts
Add exported SessionUser; validate/transform Appwrite account shape; implement getSessionFromHeader (Bearer) and getSessionFromCookie; refactor `getServerSession(): Promise<SessionUser
Typed API error responses
src/lib/api-response.ts
New ApiErrorResponse/ApiErrorCode types, createApiError, request-id extraction, helpers (unauthorized, forbidden, notFound, badRequest, validationError, conflict, gone, internalError) and isApiErrorResponse guard.

Rate limiting & proxy CORS

Layer / File(s) Summary
In-memory rate limiter
src/lib/rate-limit.ts
New Map-backed windowed buckets, env parsing for auth/API windows and max requests, trusted-proxy parsing, public/private IP classification, client-ip resolution, fallback identifier hashing, checkRateLimit and rateLimitRequest, and periodic bucket cleanup.
Proxy middleware: API CORS, X-Request-ID, and rate limiting
src/proxy.ts
Detect /api/* requests, compute/validate CORS origins, short-circuit OPTIONS with 204, ensure/forward X-Request-ID, optionally enforce rateLimitRequest returning 429 with Retry-After and X-RateLimit-* headers, and update config.matcher to include "/api/:path*".

Realtime subscription lifecycle & pool

Layer / File(s) Summary
Safe teardown & optional unsubscribe
src/lib/realtime-error-suppression.ts
Extend RealtimeSubscription object shape with optional `unsubscribe?: () => Promise
Shared realtime pool, queued teardown & idle disposal
src/lib/realtime-pool.ts
Normalize toUnsubscribeFn() to always return async wrapper preferring unsubscribe, track activeSubscriptionCount, install idle teardown listeners to dispose shared realtime when inactive, and rewrite patched subscribe to sequence queued teardown and forward update/disconnect.
Realtime tests: unsubscribe function behavior
src/__tests__/realtime-pool.test.ts, src/__tests__/setup.ts
Add test verifying SDK subscribe resolving to an unsubscribe function is awaited/invoked; standardize mocked subscribe to mockResolvedValue(...) shape used broadly in tests.

Realtime hook wiring & guarded cleanup

Layer / File(s) Summary
useConversations: update existing subscription + cleanup
src/app/chat/hooks/useConversations.ts, src/__tests__/chat-hooks-*
Broaden subscriptionRef type to support update({ queries }); attempt to call existing.update(Query.contains(...)), log and recreate on failure; ensure cleanup uses closeSubscriptionSafely and logs failures; tests/mocks adjusted accordingly.
useStatusSubscription: reset & resilient update
src/app/chat/hooks/useStatusSubscription.ts
Clear previous user IDs when disabling or when normalized list becomes empty; on update failure attempt to close existing subscription (suppressing close errors) before recreating.
useInbox & useNotifications: fire-and-forget + guarded cleanup
src/app/chat/hooks/useInbox.ts, src/hooks/useNotifications.ts, tests
Switch inbox refresh to a voided async IIFE; replace local subscriptionRef patterns with explicit unsubscribe/cleanup/cancelled guards to prevent async setup after teardown; update tests to use async-resolving mock methods.
Test mocks across hooks
src/__tests__/chat-hooks-*.test.ts, src/__tests__/setup.ts, src/__tests__/hooks/useNotifications.test.ts
Standardize realtime subscription mock methods (update, disconnect, close) to mockResolvedValue() to reflect async teardown/update behavior.

API routes, pagination & metadata

Layer / File(s) Summary
Public servers: cursor pagination, search, richer response
src/app/api/servers/public/route.ts, src/__tests__/api-routes/servers-public.test.ts
Change GET signature to GET(request: NextRequest); parse limit, cursor, search; use Appwrite limit+1 and optional cursorAfter; apply isPublicServerDocument guard, optional in-memory case-insensitive name filtering, enrich with member counts, collect failedIds, and return { servers, nextCursor, count, failedIds }; tests updated to call handler with constructed NextRequest.
Mentionable roles: server-side filtered queries
src/app/api/servers/[serverId]/mentionable-roles/route.ts
Add local TS interfaces; generalize paginated fetch to accept extra Appwrite queries; fetch mentionable roles, conditionally fetch assignments for those role IDs, compute memberCount by iterating roleIds[], and default color to "".
Instance & version endpoints
src/app/api/instance/route.ts, src/app/api/version/route.ts
Add GET /api/instance returning InstanceInfo (env-derived name/URL, Appwrite config, support contacts, meta fields, feature flags with sensible defaults); extend /api/version response with apiVersion, minMobileAppVersion, maxMobileAppVersion, and deprecationWarnings.
Thread mention permission fix
src/app/api/messages/[messageId]/thread/route.ts
Pass parentMessage.serverId directly (no String coercion) to getServerPermissionsForUser when checking @everyone permission.

UI, components & misc

Layer / File(s) Summary
Header, auth UI, and status updates
src/components/header.tsx, src/app/(auth)/login/*, src/contexts/auth-context.tsx
Use structured logger for errors, add aria-hidden to decorative icons, conditional arrow rendering while loading, change updateUserStatus to return { success, error? } and show toast.error on failure; auth-context wiring updated to await/guard realtime subscription teardown.
Mention autocomplete & avatars
src/components/mention-autocomplete.tsx
Stabilize React keys (everyone-option, role-${id}, user:${userId}) and use shared Avatar component for user options; client-side role cache/filtering introduced in chat-input.
Chat input mention roles caching & cancellation
src/components/chat-input.tsx
Add mentionableRolesCacheRef keyed by serverId, AbortController-based cancellation for autocomplete requests, fetch+cache mentionable roles per serverId and filter client-side by substring.
Conversation list & unread handling
src/app/chat/components/ConversationList.tsx, src/__tests__/components/conversation-list.test.tsx
Derive unreadCount once using conversationUnreadStateById fallback and pass to badge renderer; tests updated to assert unread badges using scoped within(...) checks.
Notifications, landing page & manifest/CSS tweaks
src/components/notifications-menu.tsx, src/app/notifications/page.tsx, src/app/page.tsx, src/app/manifest.ts, src/index.css
Add type="button" to dropdown trigger, detect AuthError UNAUTHORIZED (and regex) to redirect to login for notifications, truncate non-privileged user IDs on landing, set explicit manifest icon sizes, and change background-attachment from localscroll.
Resource hints & layout
src/components/resource-hints.tsx, src/app/layout.tsx
Add ResourceHints client component that preconnects/prefetches Appwrite host and render it at top of RootLayout before service worker registration; minor background styling updates.
Providers, lazy GlobalSearch & formatting-only edits
src/components/providers.tsx, src/components/app-layout.tsx, many component files
Reformat QueryClient init via useState, lazy-load GlobalSearch with mapping; many JSX/formatting-only edits across components and tests.
Head removal
src/app/head.tsx
Removed default Head export that previously emitted Appwrite link tags server-side.

Package, docs & tests

Layer / File(s) Summary
package.json version & deps
package.json
Bump package version 1.8.01.9.0; add update:pin and update:check scripts (keep update:force); bump @tanstack/react-query, posthog-js, posthog-node, and several dev deps.
README: dependency policy & SYSTEM_SENDER docs
README.md
Document dependency update policy and update Available Scripts table; expand SYSTEM_SENDER_USER_ID env var documentation for system announcement DM behavior.
Test updates & additions
src/__tests__/*
Widespread test adjustments: route tests now call handlers with NextRequest, realtime mocks standardized to async-resolving methods, add rate-limit env parsing tests, expand servers-public pagination tests, and refine realtime-pool tests for unsubscribe semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • acarlson33/firepit#174: Shares realtime mock and hook wiring updates (getSharedRealtime async subscribe + test adjustments).
  • acarlson33/firepit#173: Overlaps Appwrite Query/cursor mock changes and servers-public pagination test scaffolding.
  • acarlson33/firepit#184: Closely related changes to the public servers listing, mapping and pagination.

Poem

🐰 I hopped through code to tidy the threads,
Headers carry sessions where the bearer treads,
Subscriptions rest when tabs go still,
Servers page by page — no big-list thrill,
Version 1.9: a crunchy carrot, well-fed!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 05-10-mobile_app_prerequisites
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch 05-10-mobile_app_prerequisites

acarlson33 commented May 11, 2026

Copy link
Copy Markdown
Owner Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@acarlson33 acarlson33 mentioned this pull request May 11, 2026
@acarlson33

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (2)
src/lib/realtime-pool.ts (1)

48-81: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

toUnsubscribeFn detaches async teardown and breaks ordering guarantees.

At Line 69/Line 70, the wrapper returns immediately and does not await the underlying unsubscribe/close Promise. That makes await calls (e.g., Line 235) complete before teardown actually finishes, so subscribe/teardown operations can interleave.

Proposed fix
-function toUnsubscribeFn(subscription: unknown): () => void {
+function toUnsubscribeFn(subscription: unknown): () => Promise<void> {
@@
-        return () => {
-            void Promise.resolve(unsubscribe()).catch((error) => {
-                logger.warn(
-                    "Realtime subscription unsubscribe failed in wrapper",
-                    {
-                        error:
-                            error instanceof Error
-                                ? error.message
-                                : String(error),
-                    },
-                );
-            });
-        };
+        return async () => {
+            try {
+                await Promise.resolve(unsubscribe());
+            } catch (error) {
+                logger.warn(
+                    "Realtime subscription unsubscribe failed in wrapper",
+                    {
+                        error:
+                            error instanceof Error
+                                ? error.message
+                                : String(error),
+                    },
+                );
+            }
+        };
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/realtime-pool.ts` around lines 48 - 81, toUnsubscribeFn currently
wraps object unsubscribe/close calls but detaches the async teardown by
resolving the promise and swallowing completion, which breaks ordering
guarantees; change the returned wrapper to be an async function that awaits the
underlying unsubscribe/close call (i.e., await unsubscribe()) and let it
propagate or catch+rethrow as appropriate so callers can await teardown
completion; keep the existing logger.warn but only log inside a catch after
awaiting, and ensure the wrapped value (unsubscribe variable) remains the bound
function used by the async wrapper in toUnsubscribeFn.
src/lib/auth-server.ts (1)

28-131: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Extract duplicate user validation and transformation logic.

Both getSessionFromHeader (lines 54-77) and getSessionFromCookie (lines 104-127) contain identical logic for:

  1. Validating the user object structure (checking $id, name, email as strings)
  2. Rejecting the system sender user
  3. Constructing the SessionUser object

This duplication increases maintenance burden and the risk that future changes won't be applied consistently.

♻️ Refactor to eliminate duplication

Add a helper function to handle common validation and transformation:

+function validateAndTransformUser(
+    user: unknown,
+    systemSenderUserId: string | null,
+): SessionUser | null {
+    if (
+        !user ||
+        typeof user !== "object" ||
+        !("$id" in user) ||
+        typeof user.$id !== "string" ||
+        typeof user.name !== "string" ||
+        typeof user.email !== "string"
+    ) {
+        return null;
+    }
+
+    if (systemSenderUserId && user.$id === systemSenderUserId) {
+        return null;
+    }
+
+    return {
+        $id: user.$id,
+        name: user.name,
+        email: user.email,
+        $createdAt:
+            "$createdAt" in user && typeof user.$createdAt === "string"
+                ? user.$createdAt
+                : undefined,
+    };
+}
+
 async function getSessionFromHeader(
     endpoint: string,
     project: string,
     systemSenderUserId: string | null,
 ): Promise<SessionUser | null> {
     try {
         const headerStore = await headers();
         const authHeader = headerStore.get("Authorization");

         if (!authHeader?.startsWith("Bearer ")) {
             return null;
         }

         const token = authHeader.slice(7);
         if (!token) {
             return null;
         }

         const client = new Client()
             .setEndpoint(endpoint)
             .setProject(project)
             .setSession(token);

         const account = new Account(client);
         const user = await account.get().catch(() => null);

-        if (
-            !user ||
-            typeof user !== "object" ||
-            !("$id" in user) ||
-            typeof user.$id !== "string" ||
-            typeof user.name !== "string" ||
-            typeof user.email !== "string"
-        ) {
-            return null;
-        }
-
-        if (systemSenderUserId && user.$id === systemSenderUserId) {
-            return null;
-        }
-
-        return {
-            $id: user.$id,
-            name: user.name,
-            email: user.email,
-            $createdAt:
-                "$createdAt" in user && typeof user.$createdAt === "string"
-                    ? user.$createdAt
-                    : undefined,
-        };
+        return validateAndTransformUser(user, systemSenderUserId);
     } catch {
         return null;
     }
 }

 async function getSessionFromCookie(
     endpoint: string,
     project: string,
     systemSenderUserId: string | null,
 ): Promise<SessionUser | null> {
     try {
         const cookieStore = await cookies();
         const sessionCookie = cookieStore.get(`a_session_${project}`);

         if (!sessionCookie?.value) {
             return null;
         }

         const client = new Client()
             .setEndpoint(endpoint)
             .setProject(project)
             .setSession(sessionCookie.value);

         const account = new Account(client);
         const user = await account.get().catch(() => null);

-        if (
-            !user ||
-            typeof user !== "object" ||
-            !("$id" in user) ||
-            typeof user.$id !== "string" ||
-            typeof user.name !== "string" ||
-            typeof user.email !== "string"
-        ) {
-            return null;
-        }
-
-        if (systemSenderUserId && user.$id === systemSenderUserId) {
-            return null;
-        }
-
-        return {
-            $id: user.$id,
-            name: user.name,
-            email: user.email,
-            $createdAt:
-                "$createdAt" in user && typeof user.$createdAt === "string"
-                    ? user.$createdAt
-                    : undefined,
-        };
+        return validateAndTransformUser(user, systemSenderUserId);
     } catch {
         return null;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/auth-server.ts` around lines 28 - 131, Extract the repeated user
validation/transformation into a helper like validateAndTransformUser(user:
unknown, systemSenderUserId: string | null): SessionUser | null and call it from
both getSessionFromHeader and getSessionFromCookie after you fetch the user via
new Account(client).get(); the helper should check that user is an object with
string $id, name, email, reject if user.$id === systemSenderUserId, and return
the SessionUser shape including optional $createdAt when it's a string; replace
the identical inline checks/returns in getSessionFromHeader and
getSessionFromCookie with a single call to this helper and return its result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Around line 24-26: The package.json scripts "update:force", "update:exact",
and "update:check" are inconsistent with the project's current caret (^)
dependency ranges; decide and enforce a single versioning strategy: either (A)
keep caret ranges and remove --save-exact from "update:exact" or rename it
(e.g., to "update:pin") to avoid confusion, (B) convert existing dependencies to
exact versions and keep --save-exact, or (C) keep both scripts but add a short
README note explaining when to use "update:force" (latest caret/semver updates)
vs "update:exact" (strict exact pinning). Update the package.json scripts and
the repository docs accordingly and reference the script names ("update:force",
"update:exact", "update:check") when making changes so contributors understand
the chosen policy.

In `@src/app/api/servers/public/route.ts`:
- Around line 83-93: The pagination metadata is computed after applying a
client-side search filter, which can make hasMore and nextCursor incorrect; move
the search filtering step so that (doc.name) filtering on serverDocuments
happens immediately after you map/collect the raw results and before you compute
hasMore and slice to limit, and then compute hasMore and nextCursor from the
filtered array (serverDocuments) rather than from the pre-filtered subset;
ensure the logic around limit, hasMore and nextCursor (the variables
serverDocuments, search, hasMore, limit, nextCursor) uses the filtered length so
cursors remain valid even when mapping or filtering reduces matches.
- Around line 131-140: In the catch block of the route handler (where
NextResponse.json is used) don't expose error.message to clients; instead log
the raw error server-side (e.g., console.error(error) or
processLogger.error(error)) and return a generic 500 response body such as {
error: "Internal server error" } or { error: "Failed to fetch servers" } while
keeping the status: 500; update the catch to remove the ternary that returns
error.message and ensure the real error is only written to server logs.

In `@src/app/page.tsx`:
- Line 81: The element building its className uses Tailwind v3 syntax
`bg-gradient-to-r`; update it to v4 syntax by replacing `bg-gradient-to-r` with
`bg-linear-to-r` and ensure explicit color stop classes are present (e.g.
`from-...` and `to-...`); if your gradient colors come from feature.accentClass,
modify that value (or append the stops in the className where used) so it
provides the `from-<color>` and `to-<color>` Tailwind classes alongside
`bg-linear-to-r` instead of relying on `bg-gradient-to-r`.

In `@src/lib/rate-limit.ts`:
- Around line 23-34: The current parsing for PARSED_RATE_LIMIT_AUTH_WINDOW_MS,
PARSED_RATE_LIMIT_AUTH_MAX, PARSED_RATE_LIMIT_API_WINDOW_MS, and
PARSED_RATE_LIMIT_API_MAX accepts negative or invalid numbers because it relies
on `parseInt(...) || default`; change each to explicitly parse the env var
(e.g., Number.parseInt or Number(...)), then validate that the result is a
finite integer and > 0 (or >= 0 if zero is allowed for your semantics); if
validation fails, fall back to DEFAULT_AUTH_CONFIG.windowMs /
DEFAULT_AUTH_CONFIG.maxRequests or DEFAULT_API_CONFIG.windowMs /
DEFAULT_API_CONFIG.maxRequests respectively, and ensure you do not accept NaN,
Infinity, or negative values.

In `@src/proxy.ts`:
- Around line 127-135: The generated requestId (requestId) is only being added
to corsHeaders and not forwarded to downstream handlers; update the forwarded
request headers passed into NextResponse.next() so they include the generated
"X-Request-ID" — create a mutable Headers copy from request.headers,
set("X-Request-ID", requestId) on it, and pass that Headers instance into
NextResponse.next({ request: { headers: newHeaders } }) so route handlers using
the helpers in src/lib/api-response.ts can access the same ID.

---

Outside diff comments:
In `@src/lib/auth-server.ts`:
- Around line 28-131: Extract the repeated user validation/transformation into a
helper like validateAndTransformUser(user: unknown, systemSenderUserId: string |
null): SessionUser | null and call it from both getSessionFromHeader and
getSessionFromCookie after you fetch the user via new Account(client).get(); the
helper should check that user is an object with string $id, name, email, reject
if user.$id === systemSenderUserId, and return the SessionUser shape including
optional $createdAt when it's a string; replace the identical inline
checks/returns in getSessionFromHeader and getSessionFromCookie with a single
call to this helper and return its result.

In `@src/lib/realtime-pool.ts`:
- Around line 48-81: toUnsubscribeFn currently wraps object unsubscribe/close
calls but detaches the async teardown by resolving the promise and swallowing
completion, which breaks ordering guarantees; change the returned wrapper to be
an async function that awaits the underlying unsubscribe/close call (i.e., await
unsubscribe()) and let it propagate or catch+rethrow as appropriate so callers
can await teardown completion; keep the existing logger.warn but only log inside
a catch after awaiting, and ensure the wrapped value (unsubscribe variable)
remains the bound function used by the async wrapper in toUnsubscribeFn.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 492bfb63-db2a-42f1-be3f-d701f41a477b

📥 Commits

Reviewing files that changed from the base of the PR and between d323e87 and 3a504f2.

⛔ Files ignored due to path filters (2)
  • .env.local.example is excluded by none and included by none
  • bun.lock is excluded by !**/*.lock and included by bun.lock
📒 Files selected for processing (33)
  • package.json
  • src/__tests__/api-routes/servers-public.test.ts
  • src/__tests__/chat-hooks-thread-read-wiring.test.tsx
  • src/__tests__/chat-hooks-useConversations.test.ts
  • src/__tests__/chat-hooks-useDirectMessages.test.ts
  • src/__tests__/hooks/useNotifications.test.ts
  • src/__tests__/setup.ts
  • src/app/(auth)/login/login-form.tsx
  • src/app/(auth)/login/page.tsx
  • src/app/api/instance/route.ts
  • src/app/api/messages/[messageId]/thread/route.ts
  • src/app/api/servers/[serverId]/mentionable-roles/route.ts
  • src/app/api/servers/public/route.ts
  • src/app/api/version/route.ts
  • src/app/chat/hooks/useConversations.ts
  • src/app/chat/hooks/useInbox.ts
  • src/app/chat/hooks/useStatusSubscription.ts
  • src/app/chat/page.tsx
  • src/app/manifest.ts
  • src/app/notifications/page.tsx
  • src/app/page.tsx
  • src/components/header.tsx
  • src/components/mention-autocomplete.tsx
  • src/components/notifications-menu.tsx
  • src/components/settings-section-nav.tsx
  • src/hooks/useNotifications.ts
  • src/index.css
  • src/lib/api-response.ts
  • src/lib/auth-server.ts
  • src/lib/rate-limit.ts
  • src/lib/realtime-error-suppression.ts
  • src/lib/realtime-pool.ts
  • src/proxy.ts
💤 Files with no reviewable changes (1)
  • src/hooks/useNotifications.ts

Comment thread package.json
Comment thread src/app/api/servers/public/route.ts
Comment thread src/app/api/servers/public/route.ts
Comment thread src/app/page.tsx Outdated
Comment thread src/lib/rate-limit.ts Outdated
Comment thread src/proxy.ts
…onents

- Refactored NotificationsMenu component for better readability and consistency in formatting.
- Updated Providers component to enhance clarity in QueryClient instantiation.
- Modified SettingsSectionNav to improve transition effects on active states.
- Cleaned up AuthContext by organizing imports and enhancing subscription management logic.
- Introduced validateAndTransformUser function in auth-server for user validation.
- Enhanced rate-limit logic with a new function to parse environment variables.
- Added idle teardown listeners in realtime-pool for better resource management.
- Improved request handling in proxy to ensure consistent request IDs.
- Created tests for rate-limit environment variable parsing to ensure defaults are applied correctly.
- Added ResourceHints component for preconnecting to external services.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 18

♻️ Duplicate comments (1)
src/proxy.ts (1)

49-51: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Narrow API-route detection to /api and /api/* only.

Line 49 uses startsWith("/api"), which also matches non-API paths like /apiary. Those requests would incorrectly bypass the non-API auth branch.

Suggested fix
-    if (pathname.startsWith("/api")) {
+    if (pathname === "/api" || pathname.startsWith("/api/")) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/proxy.ts` around lines 49 - 51, The API-route check using
pathname.startsWith("/api") is too broad and matches paths like "/apiary";
update the conditional in src/proxy.ts to only treat requests as API routes when
the pathname is exactly "/api" or begins with "/api/". Replace the current check
around the variables rateLimitEnabled/rateLimitHeaders (the block using
pathname.startsWith("/api")) with a tightened predicate (e.g., pathname ===
"/api" || pathname.startsWith("/api/")) so non-API routes do not fall into the
API auth/rate-limit branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/__tests__/api-routes/servers-public.test.ts`:
- Around line 48-50: Add tests exercising the new cursor-pagination path by
updating the test helper createRequest and the test cases in
servers-public.test.ts to send requests with a query string containing ?cursor=
(use createRequest("http://localhost/api/servers/public?cursor=<value>")) and
assert that the JSON response includes nextCursor and count fields and that
items correspond to the expected second-page slice; additionally add a test that
performs a search query plus pagination (e.g.,
createRequest("http://localhost/api/servers/public?search=<term>&cursor=<boundary>"))
to assert that the search results respect the pagination boundary and that
nextCursor/count behave correctly. Use the existing request helper createRequest
and the same handler invocation used elsewhere in this test file so the new
cases run with the current fixtures and assert both presence and correctness of
nextCursor and count.

In `@src/__tests__/middleware.test.ts`:
- Around line 220-234: The test "should forward the request ID to downstream
handlers" restores the NextResponse.next spy only at the end, so if an assertion
throws the spy isn't restored; wrap the test logic that calls middleware() and
asserts forwarded headers in a try/finally and move nextSpy.mockRestore() into
the finally block so NextResponse.next is always restored; reference the test
case, the nextSpy created with vi.spyOn(NextResponse, "next"), and the
middleware() invocation to locate where to add the try/finally.

In `@src/__tests__/rate-limit-env.test.ts`:
- Around line 9-37: The tests mutate process.env (e.g., setting
RATE_LIMIT_AUTH_WINDOW_MS/ MAX and RATE_LIMIT_API_WINDOW_MS/ MAX) and do not
restore it; capture a snapshot of process.env before each test (or in a
top-level beforeEach) and restore the original env in an afterEach so tests
don't leak state, or explicitly revert the specific keys after each it; update
the test file around the uses of importFreshRateLimitModule and rateLimitRequest
to ensure process.env is reset after each test run.

In `@src/__tests__/realtime-pool.test.ts`:
- Around line 191-211: The test must prove the wrapper awaits the unsubscribe
promise: change unsubscribeSpy to return a deferred promise (create a deferred
with its resolve function), have mockRealtimeSubscribe resolve to that
unsubscribeSpy, call wrappedSubscribe and capture the returned unsubscribe
promise but do not await it immediately—assert the spy has not been called yet
and the wrapper's unsubscribe is still pending, then resolve the deferred and
await the unsubscribe promise, finally assert unsubscribeSpy was called once;
reference getSharedRealtime, mockRealtimeSubscribe, wrappedSubscribe and
unsubscribe in your changes.

In `@src/app/api/servers/`[serverId]/mentionable-roles/route.ts:
- Around line 169-175: The payload currently forwards the optional field
doc.mentionable directly in the mentionableRoles mapping which can be undefined;
change the mapping in the mentionableRoles creation (the map that builds
id/name/color/mentionable/memberCount) to coerce mentionable to a strict boolean
(e.g., use Boolean(doc.mentionable) or !!doc.mentionable) so the API always
returns true/false for the mentionable property.

In `@src/app/api/servers/public/route.ts`:
- Around line 102-121: The current pagination uses servers.at(-1) which can be
earlier than the final fetched document if mapServerDocument() threw; instead
derive nextCursor from the last raw document in serverDocuments so failures at
the tail don't reappear. Replace the logic that computes last/nextCursor to use
serverDocuments.at(-1) (e.g., const lastDoc = serverDocuments.at(-1)) and set
nextCursor = hasMore && lastDoc ? String(lastDoc.$id) : null; keep failedIds
logging and mapping logic unchanged.

In `@src/app/invite/`[code]/InvitePreviewClient.tsx:
- Around line 35-40: The effect that auto-joins is missing dependencies and can
close over stale values: include both joining and handleJoin in the dependency
array and prevent repeat runs by tracking if we've already auto-joined. Wrap the
existing handleJoin function in useCallback (with its current dependencies) so
it can be safely referenced from the effect, add a hasAutoJoinedRef
(useRef<boolean>(false)) to short-circuit subsequent effects, and update the
useEffect dependencies to [searchParams, isAuthenticated, joining, handleJoin];
inside the effect check auto param, isAuthenticated, !joining and
!hasAutoJoinedRef.current before calling handleJoin and set
hasAutoJoinedRef.current = true after invoking it.

In `@src/app/page.tsx`:
- Around line 230-241: Replace the three paragraphs that render bullet
characters with a semantic unordered list: convert the three <p> elements into a
single <ul> containing three <li> items (remove the leading "•" characters),
ensure the list is placed in the same JSX location where the current paragraph
block is rendered, and remove any role attributes used to emulate list behavior
so the native <ul>/<li> semantics are relied upon instead.

In `@src/components/chat-input.tsx`:
- Around line 112-131: The code currently refetches
`/api/servers/${serverId}/mentionable-roles` inside the mentionQuery handler on
every debounced keystroke; instead, fetch mentionable roles once per server and
filter locally. Implement a per-server cache (e.g., a ref or state object keyed
by serverId) and change the block around mentionQuery/serverId so that on
serverId change or cache miss you call fetch and populate the cache, then always
derive filtered results by applying the existing .filter(...) to the cached
roles and call setMentionableRoles; only call the network when the cache does
not contain roles for the current serverId.
- Around line 125-130: Role ID comparison is currently case-sensitive because
role.id is compared directly to the lowercased query; change the filter so both
sides are normalized by using role.id.toLowerCase().includes(queryLower)
(alongside the existing role.name.toLowerCase().includes(queryLower)), ensuring
you call toLowerCase() safely on role.id within the arrow filter (role:
MentionableRole) => ... so ID matching is case-insensitive.
- Around line 236-242: The warning currently logs the full selectable object
which may contain PII; update the handleMentionSelect logger.warn call to stop
including the raw selectable and instead emit only minimal diagnostics such as
typeof selectable and a small non-sensitive shape indicator (e.g., whether it is
an array/object and a list of keys or a boolean hasId flag) so no
identifiers/display names are logged; locate the logger.warn inside
handleMentionSelect and replace the selectable payload with these minimal
diagnostics.

In `@src/components/header.tsx`:
- Around line 278-288: handleCustomStatusSubmit currently assumes
updateUserStatus will throw on failure; instead await its result and handle
non-throwing failures explicitly by checking the returned value (e.g.,
result.success or result.error) from updateUserStatus, calling toast.error(...)
and not calling setAccountMenuOpen(false) when the update indicates failure, and
only closing the menu when the response denotes success; reference
handleCustomStatusSubmit, updateUserStatus, setAccountMenuOpen, and toast.error
when making this change.

In `@src/contexts/auth-context.tsx`:
- Around line 292-308: When existing.update(...) throws, the old live
subscription is left open; modify the catch block that follows the call to
existing.update on subscriptionRef.current so it explicitly closes/unsubscribes
the old subscription (call and await existing.close(...) or
existing.unsubscribe(...) if present, or fallback to any provided teardown
method), then clear subscriptionRef.current before proceeding to recreate a new
subscription; ensure you reference subscriptionRef.current and the existing
object around the update() call and handle both sync and async close methods
safely.

In `@src/lib/auth-server.ts`:
- Around line 67-75: The current check uses authHeader?.startsWith("Bearer ")
which rejects valid but differently-cased schemes; update the Authorization
parsing in the authHeader handling (where headerStore.get("Authorization") is
read, the authHeader check and token extraction) to compare the scheme
case-insensitively (e.g., split authHeader on whitespace and compare the scheme
lowercased to "bearer" or use a case-insensitive regex) and then extract the
token from the second segment (ensure you still return null when token is
missing). This fixes acceptance of "bearer", "BEARER", or mixed-case schemes
while keeping existing null returns when no token is present.

In `@src/lib/rate-limit.ts`:
- Around line 70-86: The getClientIp function currently trusts the first value
in x-forwarded-for which can be spoofed; update getClientIp to prefer
direct/proxy-supplied addresses (e.g. x-real-ip or cloud-provider headers like
cf-connecting-ip) and only fall back to x-forwarded-for when the request is
known to come through a trusted proxy: implement validation using a proper IP
check (net.isIP or an IP regex), ignore private/loopback ranges when determining
the client address, and allow configuration of trusted proxy IPs or a
TRUSTED_PROXIES env var so x-forwarded-for is only honored when the immediate
peer is in that trusted list; reference and modify getClientIp to perform these
checks and return "unknown" if no validated public IP is found.
- Around line 110-151: The rate-limit key currently only uses the client
identifier so auth and API requests share one counter; change the key to include
the request scope (auth vs api) so they have separate counters—either update
rateLimitRequest to prepend a scope prefix (e.g., "auth:" or "api:") to the
clientIp before calling checkRateLimit, or modify checkRateLimit to accept a
second scope parameter and include that scope in the generated key; adjust uses
of checkRateLimit and the key construction in checkRateLimit accordingly
(functions to locate: rateLimitRequest and checkRateLimit).

In `@src/lib/realtime-pool.ts`:
- Around line 19-37: The current installIdleTeardownListeners uses teardown that
always calls disposeSharedRealtime on blur/pagehide/visibilitychange which can
silently stop updates; change teardown to only call disposeSharedRealtime when
there are no active mounted consumers (e.g., check a module-level
activeSubscriptionCount or similar counter) and when the document is actually
hidden/page is unloading. Specifically, add or use an existing
activeSubscriptionCount (increment on subscriber mount, decrement on unmount),
and in installIdleTeardownListeners replace the unconditional teardown with a
guard: if (activeSubscriptionCount === 0 && document.visibilityState ===
"hidden") { disposeSharedRealtime(); } and avoid disposing on plain blur unless
activeSubscriptionCount is 0 and pagehide/visibility confirm unload. Ensure you
update subscriber mount/unmount code to maintain the counter so
disposeSharedRealtime is only called when truly idle.

In `@src/proxy.ts`:
- Around line 52-81: The rate-limiting block currently runs before CORS
preflight handling, so OPTIONS requests can be throttled; update the middleware
to detect preflight (request.method === "OPTIONS") and short-circuit before
calling rateLimitRequest so preflight responses are returned/forwarded without
decrementing or blocking via rateLimitRequest; locate the rateLimitEnabled check
and the rateLimitRequest call in src/proxy.ts and ensure OPTIONS handling occurs
above/before that block (or that rateLimitRequest is skipped for OPTIONS),
returning an appropriate CORS/NextResponse for preflight.

---

Duplicate comments:
In `@src/proxy.ts`:
- Around line 49-51: The API-route check using pathname.startsWith("/api") is
too broad and matches paths like "/apiary"; update the conditional in
src/proxy.ts to only treat requests as API routes when the pathname is exactly
"/api" or begins with "/api/". Replace the current check around the variables
rateLimitEnabled/rateLimitHeaders (the block using pathname.startsWith("/api"))
with a tightened predicate (e.g., pathname === "/api" ||
pathname.startsWith("/api/")) so non-API routes do not fall into the API
auth/rate-limit branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: afb91ebf-0f44-4cc9-a9d3-79d704d77477

📥 Commits

Reviewing files that changed from the base of the PR and between 3a504f2 and 3321fc9.

📒 Files selected for processing (28)
  • README.md
  • package.json
  • src/__tests__/api-routes/servers-public.test.ts
  • src/__tests__/components/conversation-list.test.tsx
  • src/__tests__/middleware.test.ts
  • src/__tests__/rate-limit-env.test.ts
  • src/__tests__/realtime-pool.test.ts
  • src/app/api/servers/[serverId]/mentionable-roles/route.ts
  • src/app/api/servers/public/route.ts
  • src/app/chat/components/ConversationList.tsx
  • src/app/chat/hooks/useStatusSubscription.ts
  • src/app/head.tsx
  • src/app/invite/[code]/InvitePreviewClient.tsx
  • src/app/layout.tsx
  • src/app/manifest.ts
  • src/app/page.tsx
  • src/components/chat-input.tsx
  • src/components/header.tsx
  • src/components/mention-autocomplete.tsx
  • src/components/notifications-menu.tsx
  • src/components/providers.tsx
  • src/components/resource-hints.tsx
  • src/components/settings-section-nav.tsx
  • src/contexts/auth-context.tsx
  • src/lib/auth-server.ts
  • src/lib/rate-limit.ts
  • src/lib/realtime-pool.ts
  • src/proxy.ts
💤 Files with no reviewable changes (1)
  • src/app/head.tsx

Comment thread src/__tests__/api-routes/servers-public.test.ts
Comment thread src/__tests__/middleware.test.ts
Comment thread src/__tests__/rate-limit-env.test.ts
Comment thread src/__tests__/realtime-pool.test.ts
Comment thread src/app/api/servers/[serverId]/mentionable-roles/route.ts Outdated
Comment thread src/lib/auth-server.ts
Comment thread src/lib/rate-limit.ts Outdated
Comment thread src/lib/rate-limit.ts Outdated
Comment thread src/lib/realtime-pool.ts
Comment thread src/proxy.ts

@coderabbitai coderabbitai Bot left a comment

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.

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)
src/components/chat-input.tsx (1)

101-155: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cancel in-flight mention lookups before writing state.

The current cleanup function only clears the debounce timer. When mentionQuery changes rapidly, an older fetch can still resolve after a newer one completes, calling setAvailableUsers() or setMentionableRoles() with stale results and overwriting the fresh data. Use AbortController to cancel the fetch request and add guards to prevent state updates after cancellation.

Suggested fix
 useEffect(() => {
     if (!showMentionAutocomplete) {
         setAvailableUsers([]);
         setMentionableRoles([]);
         setIsLoadingUsers(false);
         return;
     }

+    const controller = new AbortController();
+    let cancelled = false;
+
     // Show loading immediately when autocomplete is shown
     setIsLoadingUsers(true);

     // If query is empty, fetch all users
     const fetchUsersAndRoles = async () => {
         try {
             const query = mentionQuery || "";
             const response = await fetch(
                 `/api/users/search?q=${encodeURIComponent(query)}&limit=10`,
+                { signal: controller.signal },
             );
+            if (cancelled) {
+                return;
+            }
             if (response.ok) {
                 const data = await response.json();
-                setAvailableUsers(data.users || []);
+                if (!cancelled) {
+                    setAvailableUsers(data.users || []);
+                }
             } else {
-                setAvailableUsers([]);
+                if (!cancelled) {
+                    setAvailableUsers([]);
+                }
             }

             const queryLower = query.toLowerCase();
             if (serverId) {
                 try {
                     let cachedRoles =
                         mentionableRolesCacheRef.current[serverId];

                     if (!cachedRoles) {
                         const rolesResponse = await fetch(
                             `/api/servers/${serverId}/mentionable-roles`,
+                            { signal: controller.signal },
                         );
+                        if (cancelled) {
+                            return;
+                        }
                         if (rolesResponse.ok) {
                             const rolesData = await rolesResponse.json();
                             cachedRoles = (rolesData.roles || []).map(
                                 (role: Omit<MentionableRole, "type">) => ({
                                     ...role,
                                     type: "role" as const,
                                 }),
                             );
                             mentionableRolesCacheRef.current[serverId] =
                                 cachedRoles;
                         } else {
                             cachedRoles = [];
                         }
                     }

                     const typedRoles = cachedRoles.filter(
                         (role: MentionableRole) =>
                             role.name.toLowerCase().includes(queryLower) ||
                             role.id.toLowerCase().includes(queryLower),
                     );
-                    setMentionableRoles(typedRoles);
-                } catch {
-                    setMentionableRoles([]);
+                    if (!cancelled) {
+                        setMentionableRoles(typedRoles);
+                    }
+                } catch (error) {
+                    if (
+                        !cancelled &&
+                        !(
+                            error instanceof DOMException &&
+                            error.name === "AbortError"
+                        )
+                    ) {
+                        setMentionableRoles([]);
+                    }
                 }
             } else {
-                setMentionableRoles([]);
+                if (!cancelled) {
+                    setMentionableRoles([]);
+                }
             }
-        } catch {
-            setAvailableUsers([]);
+        } catch (error) {
+            if (
+                !cancelled &&
+                !(
+                    error instanceof DOMException &&
+                    error.name === "AbortError"
+                )
+            ) {
+                setAvailableUsers([]);
+                setMentionableRoles([]);
+            }
         } finally {
-            setIsLoadingUsers(false);
+            if (!cancelled) {
+                setIsLoadingUsers(false);
+            }
         }
     };

     const debounce = setTimeout(() => {
         void fetchUsersAndRoles();
     }, 150);

-    return () => clearTimeout(debounce);
+    return () => {
+        cancelled = true;
+        controller.abort();
+        clearTimeout(debounce);
+    };
 }, [mentionQuery, showMentionAutocomplete, serverId]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chat-input.tsx` around lines 101 - 155, The fetchUsersAndRoles
function can race when mentionQuery changes; create an AbortController for each
call (or separate controllers for the user fetch and the roles fetch), pass
controller.signal to both fetch calls, store the controller so the debounce
cleanup can call controller.abort(), and in the fetch promise chains catch
AbortError and skip calling setAvailableUsers/setMentionableRoles; additionally,
before calling the setters verify the request wasn't aborted (e.g., check
controller.signal.aborted or compare the current mentionQuery/serverId) so stale
responses don't overwrite fresh state; ensure the finally block still sets
setIsLoadingUsers(false) only for non-aborted flows or handles aborted flows
appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/servers/`[serverId]/mentionable-roles/route.ts:
- Around line 136-155: The Appwrite queries use unindexed or non-existent
fields: the roles query filters on "mentionable" and the role assignments query
filters on "roleId" while the collection stores "roleIds" (array) and neither
field has indexes; either add indexes for "mentionable" on env.collections.roles
and for "roleIds" on env.collections.roleAssignments in
scripts/setup-appwrite.ts, or change the code around getAllDocumentsPaginated
where mentionableRoleDocs and mentionableAssignments are built to fetch broader
results and perform the "mentionable" and roleId->roleIds membership filtering
in-memory (use roleIds array membership checks against mentionableRoleIds) to
avoid relying on unsupported Appwrite filters.

In `@src/app/invite/`[code]/InvitePreviewClient.tsx:
- Line 92: The decorative icon elements in InvitePreviewClient.tsx (the <Users
... /> and <Loader2 ... /> JSX components) should be hidden from assistive tech:
update each occurrence of Users and Loader2 (the instances near where the user
count and loading indicators are rendered) to include aria-hidden="true" so
screen readers skip them and do not duplicate the adjacent text labels.

In `@src/lib/auth-server.ts`:
- Around line 60-113: Extract the duplicated Appwrite client/account lookup into
a new helper getSessionForToken(endpoint, project, token, systemSenderUserId):
have it create the Client (new
Client().setEndpoint(endpoint).setProject(project).setSession(token)), construct
Account(client), call account.get() (catching errors to return null) and finally
return validateAndTransformUser(user, systemSenderUserId) or null; then simplify
getSessionFromHeader and getSessionFromCookie to only obtain the token (from
Authorization header or cookie) and call getSessionForToken with that token,
preserving the existing try/catch behavior and null returns used in
getSessionFromHeader/getSessionFromCookie.

In `@src/lib/rate-limit.ts`:
- Line 8: The current in-memory Map rateLimitStore (and its cleanup interval) is
per-process and won’t work correctly in multi-instance or serverless
deployments; update the code to (1) add a clear comment/docstring near
rateLimitStore and the cleanup logic explaining this limitation and that it’s
only suitable for single-process/local use, and (2) refactor the rate limiting
to use an interchangeable store interface (e.g., IRateLimitStore with methods
like get/set/delete/cleanup) and provide a Redis-backed implementation to be
used in production (swap out the Map-based implementation used by the functions
that reference rateLimitStore and the cleanup interval with a RedisStore
implementation).
- Around line 140-148: The cf-connecting-ip and true-client-ip headers are
currently trusted unconditionally; update the logic around where cfConnectingIp
and trueClientIp are read (the block that calls isPublicIp(...) and
normalizeIp(...)) to only honor those headers when the request is coming from a
known/trusted proxy or when a platform flag is set: either (a) require the
immediate peer (remote address) to be in TRUSTED_PROXIES before reading
cf-connecting-ip/true-client-ip, or (b) gate acceptance behind a config flag
like TRUSTED_PLATFORM==='cloudflare'; mirror the existing treatment used for
x-forwarded-for so you reuse the same trusted-proxy checks and ensure
isPublicIp/normalizeIp are only applied after that verification.
- Around line 139-175: getClientIp currently returns the literal "unknown" which
collapses all unidentified clients into one rate-limit bucket; change
getClientIp to return null/undefined when it cannot determine a public client IP
(preserve use of normalizeIp and firstValidPublicIp) and remove the "unknown"
fallback, then update rateLimitRequest to detect a missing client IP and instead
key rate limits by a secondary identifier (preferably Authorization bearer token
or a session cookie name used by your app, falling back to a per-request
fingerprint like a hashed User-Agent+Accept-Language+IP-less-id) so each
user/session gets a distinct bucket; additionally add a startup check that logs
a warning if TRUSTED_PROXIES is empty and no platform headers (cf-connecting-ip
/ true-client-ip / x-real-ip) are configured so operators are alerted.
- Around line 116-122: The IPv6 link-local check currently only matches "fe80:";
update the check on the local variable lower so it matches the entire fe80::/10
range (fe80 through febf). Replace the single startsWith("fe80:") test with a
range test that confirms the first two hex bytes are between 0xfe80 and 0xfebf
(e.g., by validating lower begins with "fe" and the next hex byte is between
0x80 and 0xbf or by using a regex for /^fe[89ab][0-9a-f]:/i), keeping the other
conditions (\"::1\", \"fc\", \"fd\") intact.

In `@src/proxy.ts`:
- Around line 53-60: The current logic sets responseOrigin = allowedOrigins[0]
when origin is missing/invalid which causes the code to send an incorrect
Access-Control-Allow-Origin and still set Access-Control-Allow-Credentials;
update the handling in the proxy so that responseOrigin is only set when
isValidOrigin(origin, allowedOrigins) returns true or
allowedOrigins.includes("*") is true, and otherwise do not emit the
Access-Control-Allow-Origin header nor the Access-Control-Allow-Credentials
header; adjust the code paths around the responseOrigin variable (and any places
that set headers using responseOrigin, e.g., where
Access-Control-Allow-Credentials is set) so headers are only added when a valid
responseOrigin or "*" was determined and ensure Vary: Origin is only added when
Access-Control-Allow-Origin is sent.
- Around line 94-120: The 429 rate-limit response created in the if
(!rateLimitResult.allowed) block does not include the CORS headers assembled in
corsHeaders, so cross-origin clients cannot read the response body or
Retry-After/X-RateLimit headers; update the NextResponse.json call to merge
corsHeaders into the response headers (combine corsHeaders with the existing
headers object, preserving Retry-After, X-RateLimit-Remaining,
X-RateLimit-Reset) so the returned response exposes the throttling info to
browsers; reference the rateLimitResult check and the NextResponse.json call
when making this change.

---

Outside diff comments:
In `@src/components/chat-input.tsx`:
- Around line 101-155: The fetchUsersAndRoles function can race when
mentionQuery changes; create an AbortController for each call (or separate
controllers for the user fetch and the roles fetch), pass controller.signal to
both fetch calls, store the controller so the debounce cleanup can call
controller.abort(), and in the fetch promise chains catch AbortError and skip
calling setAvailableUsers/setMentionableRoles; additionally, before calling the
setters verify the request wasn't aborted (e.g., check controller.signal.aborted
or compare the current mentionQuery/serverId) so stale responses don't overwrite
fresh state; ensure the finally block still sets setIsLoadingUsers(false) only
for non-aborted flows or handles aborted flows appropriately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ad8707f6-e070-49e2-bad3-3b5296be709f

📥 Commits

Reviewing files that changed from the base of the PR and between 3321fc9 and 2ca24f2.

📒 Files selected for processing (18)
  • src/__tests__/api-routes/servers-public.test.ts
  • src/__tests__/middleware.test.ts
  • src/__tests__/rate-limit-env.test.ts
  • src/__tests__/realtime-pool.test.ts
  • src/app/api/servers/[serverId]/mentionable-roles/route.ts
  • src/app/api/servers/public/route.ts
  • src/app/invite/[code]/InvitePreviewClient.tsx
  • src/app/layout.tsx
  • src/app/page.tsx
  • src/components/app-layout.tsx
  • src/components/chat-input.tsx
  • src/components/header.tsx
  • src/components/notifications-menu.tsx
  • src/contexts/auth-context.tsx
  • src/lib/auth-server.ts
  • src/lib/rate-limit.ts
  • src/lib/realtime-pool.ts
  • src/proxy.ts

Comment thread src/app/api/servers/[serverId]/mentionable-roles/route.ts Outdated
Comment thread src/app/invite/[code]/InvitePreviewClient.tsx Outdated
Comment thread src/lib/auth-server.ts
Comment thread src/lib/rate-limit.ts
resetAt: number;
}

const rateLimitStore = new Map<string, RateLimitEntry>();

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.

🧹 Nitpick | 🔵 Trivial | 🏗️ Heavy lift

In-memory store won't survive multi-instance / serverless deployments.

rateLimitStore is a per-process Map, and the cleanup interval runs per process. In any deployment with >1 worker (PM2, multi-pod, serverless invocations), each instance enforces its own limit, so a 10/min limit effectively becomes 10 * N per minute, and serverless cold starts reset entirely. This is fine as a baseline, but please document it and plan a Redis (or other shared store) backend before relying on these limits for security-critical throttling (e.g., login attempts).

Also applies to: 177-190

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/rate-limit.ts` at line 8, The current in-memory Map rateLimitStore
(and its cleanup interval) is per-process and won’t work correctly in
multi-instance or serverless deployments; update the code to (1) add a clear
comment/docstring near rateLimitStore and the cleanup logic explaining this
limitation and that it’s only suitable for single-process/local use, and (2)
refactor the rate limiting to use an interchangeable store interface (e.g.,
IRateLimitStore with methods like get/set/delete/cleanup) and provide a
Redis-backed implementation to be used in production (swap out the Map-based
implementation used by the functions that reference rateLimitStore and the
cleanup interval with a RedisStore implementation).

Comment thread src/lib/rate-limit.ts
Comment thread src/lib/rate-limit.ts Outdated
Comment thread src/lib/rate-limit.ts Outdated
Comment thread src/proxy.ts Outdated
Comment thread src/proxy.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/proxy.ts (1)

190-192: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject protocol-relative redirect targets.

Line 191 accepts any value starting with /, including //evil.example. new URL(destination, request.url) will treat that as an external URL, so this reopens an open-redirect on the login/register flow.

🔒 Proposed fix
-        const destination = redirect?.startsWith("/") ? redirect : "/";
+        const destination =
+            redirect && /^\/(?!\/)/.test(redirect) ? redirect : "/";
         return NextResponse.redirect(new URL(destination, request.url));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/proxy.ts` around lines 190 - 192, The redirect handling accepts
protocol-relative targets like "//evil.example"; update the check for redirect
(from request.nextUrl.searchParams.get("redirect")) to only allow single-slash
absolute paths by rejecting values that start with "//" or match a protocol
(e.g., "http:"), e.g. require redirect to match a pattern like /^\/(?!\/)/
before setting destination and calling NextResponse.redirect(new
URL(destination, request.url)); if the check fails, fall back to "/" to prevent
open redirects.
♻️ Duplicate comments (1)
src/lib/rate-limit.ts (1)

159-181: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't derive proxy trust from X-Forwarded-For.

Line 176 treats the last x-forwarded-for hop as the immediate peer, but that header is still client-controlled at this point. A caller can append a trusted proxy IP there, make isTrustedProxyRequest() return true, and then spoof the downstream client-IP headers to evade or shard throttling.

🛡️ Minimal safe fix
 function getImmediatePeerIp(request: Request): string | null {
     const requestWithIp = request as Request & { ip?: string | null };
     const requestIp = requestWithIp.ip;
     if (typeof requestIp === "string" && requestIp.trim().length > 0) {
         return normalizeIp(requestIp);
     }
-
-    const forwardedFor = request.headers.get("x-forwarded-for");
-    if (!forwardedFor) {
-        return null;
-    }
-
-    const ips = forwardedFor
-        .split(",")
-        .map((ip) => ip.trim())
-        .filter(Boolean);
-
-    const proxyIp = ips.at(-1);
-    if (!proxyIp) {
-        return null;
-    }
-
-    return normalizeIp(proxyIp);
+
+    // Never establish proxy trust from X-Forwarded-For itself.
+    return null;
 }

Also applies to: 184-190

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/rate-limit.ts` around lines 159 - 181, getImmediatePeerIp is
currently treating the last hop in X-Forwarded-For as the immediate peer even
when the request has not been validated as coming from a trusted proxy; update
getImmediatePeerIp to never derive trust from X-Forwarded-For unless the request
is from a trusted proxy by checking isTrustedProxyRequest(request) before
reading or using the x-forwarded-for header, and only then use normalizeIp on
the appropriate hop; also make the same change to the other X-Forwarded-For
usage noted (lines 184-190) so both places require
isTrustedProxyRequest(request) before trusting or parsing the header.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/servers/`[serverId]/mentionable-roles/route.ts:
- Around line 152-175: The current code in route.ts uses
getAllDocumentsPaginated to compute memberCountByRoleId by scanning all
roleAssignments (mentionableAssignments), which is too expensive on the hot
mention autocomplete path; remove the paginated scan and instead read a
precomputed member count from role metadata or an aggregate source (e.g., a
memberCount field on the Role document) for the mentionableRoleIds, or simply
omit member counts from this endpoint; update any uses of memberCountByRoleId
accordingly and eliminate the getAllDocumentsPaginated call and the for-loops
that build memberCountByRoleId so the endpoint no longer performs a full
roleAssignments scan.

In `@src/components/chat-input.tsx`:
- Around line 109-195: The outer try/catch currently wraps both the users fetch
and the roles fetch so a failure in `/api/users/search` prevents the
`/mentionable-roles` logic from running; split these into two independent flows
(either two separate try/catch blocks or use Promise.allSettled) so failures
don’t cancel the other source: keep the users fetch block that calls
`/api/users/search` and updates setAvailableUsers in its own try/catch (honoring
controller.signal.aborted and handling AbortError), and move the server-specific
logic that reads/sets mentionableRolesCacheRef.current[serverId], fetches
`/api/servers/${serverId}/mentionable-roles`, filters into typedRoles, and calls
setMentionableRoles into a separate try/catch that also checks
controller.signal.aborted and handles AbortError; ensure each catch only clears
its own state (setAvailableUsers or setMentionableRoles) and does not suppress
the other.

---

Outside diff comments:
In `@src/proxy.ts`:
- Around line 190-192: The redirect handling accepts protocol-relative targets
like "//evil.example"; update the check for redirect (from
request.nextUrl.searchParams.get("redirect")) to only allow single-slash
absolute paths by rejecting values that start with "//" or match a protocol
(e.g., "http:"), e.g. require redirect to match a pattern like /^\/(?!\/)/
before setting destination and calling NextResponse.redirect(new
URL(destination, request.url)); if the check fails, fall back to "/" to prevent
open redirects.

---

Duplicate comments:
In `@src/lib/rate-limit.ts`:
- Around line 159-181: getImmediatePeerIp is currently treating the last hop in
X-Forwarded-For as the immediate peer even when the request has not been
validated as coming from a trusted proxy; update getImmediatePeerIp to never
derive trust from X-Forwarded-For unless the request is from a trusted proxy by
checking isTrustedProxyRequest(request) before reading or using the
x-forwarded-for header, and only then use normalizeIp on the appropriate hop;
also make the same change to the other X-Forwarded-For usage noted (lines
184-190) so both places require isTrustedProxyRequest(request) before trusting
or parsing the header.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0141d6e6-043c-438b-a881-87cfca2f0c40

📥 Commits

Reviewing files that changed from the base of the PR and between 2ca24f2 and a0dc2a2.

⛔ Files ignored due to path filters (1)
  • .gitignore is excluded by none and included by none
📒 Files selected for processing (7)
  • src/__tests__/rate-limit-env.test.ts
  • src/app/api/servers/[serverId]/mentionable-roles/route.ts
  • src/app/invite/[code]/InvitePreviewClient.tsx
  • src/components/chat-input.tsx
  • src/lib/auth-server.ts
  • src/lib/rate-limit.ts
  • src/proxy.ts

Comment thread src/app/api/servers/[serverId]/mentionable-roles/route.ts Outdated
Comment thread src/components/chat-input.tsx

acarlson33 commented May 13, 2026

Copy link
Copy Markdown
Owner Author

Merge activity

  • May 13, 11:21 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • May 13, 11:22 PM UTC: Graphite couldn't merge this PR because it had merge conflicts.

@acarlson33 acarlson33 changed the base branch from 05-08-ui_overhaul to graphite-base/189 May 13, 2026 23:21
@acarlson33 acarlson33 changed the base branch from graphite-base/189 to main May 13, 2026 23:21
@acarlson33 acarlson33 merged commit 776a431 into main May 13, 2026
3 of 4 checks passed
@acarlson33 acarlson33 deleted the 05-10-mobile_app_prerequisites branch May 13, 2026 23:29
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.

1 participant