Skip to content

feat: update GraphQL fragments and queries for bounty management#114

Merged
0xdevcollins merged 8 commits intoboundlessfi:mainfrom
Ekene001:feat/Create-a-GraphQL-API-service-layer
Feb 23, 2026
Merged

feat: update GraphQL fragments and queries for bounty management#114
0xdevcollins merged 8 commits intoboundlessfi:mainfrom
Ekene001:feat/Create-a-GraphQL-API-service-layer

Conversation

@Ekene001
Copy link
Contributor

@Ekene001 Ekene001 commented Feb 23, 2026

closes #100

  • Added createdBy field to the BountyFields fragment.
  • Refactored bounty queries to use GraphQL documents and fragments.
  • Updated query options for listing and fetching bounties to utilize new GraphQL structure.
  • Enhanced prefetching functions to align with updated query parameters.
  • Modified query key generation to support new GraphQL queries.
  • Updated sync handlers to work with the new bounty fragment structure.
  • Improved server authentication to retrieve user session data more effectively.

Summary by CodeRabbit

  • New Features

    • Magic-link verification page with loading, success, error states and auto-redirect.
    • Bounties now display a visible "created by" field.
  • Refactor

    • Data layer migrated from REST to GraphQL; hooks and components updated to use generated GraphQL types.
    • Authentication/session retrieval made async and centralized.
  • Chores

    • Removed legacy REST bounty API surfaces and obsolete client token helpers.

- Added `createdBy` field to the `BountyFields` fragment.
- Refactored bounty queries to use GraphQL documents and fragments.
- Updated query options for listing and fetching bounties to utilize new GraphQL structure.
- Enhanced prefetching functions to align with updated query parameters.
- Modified query key generation to support new GraphQL queries.
- Updated sync handlers to work with the new bounty fragment structure.
- Improved server authentication to retrieve user session data more effectively.
@vercel
Copy link

vercel bot commented Feb 23, 2026

@Ekene001 is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Warning

Rate limit exceeded

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

⌛ 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 48607ca and 101d72b.

📒 Files selected for processing (1)
  • lib/query/query-keys.ts
📝 Walkthrough

Walkthrough

Removes REST bounty API and many Next.js app/api/bounties routes, migrates hooks/components to use GraphQL-generated queries/mutations and GraphQL types, adds async cross-environment access token utilities and GraphQL client auth changes, and adds a client-side magic-link verification page.

Changes

Cohort / File(s) Summary
API Routes Removed
app/api/bounties/route.ts, app/api/bounties/[id]/route.ts, app/api/bounties/[id]/apply/route.ts, app/api/bounties/[id]/claim/route.ts, app/api/bounties/[id]/competition/join/route.ts, app/api/bounties/[id]/join/route.ts, app/api/bounties/[id]/milestones/advance/route.ts, app/api/bounties/[id]/applications/route.ts, app/api/bounties/[id]/submissions/route.ts, app/api/bounties/[id]/submit/route.ts
Deleted server-side Next.js route handlers for bounty listing, detail, and all action endpoints (apply/claim/join/competition/milestones/submissions/applications/submit).
REST API Client & Types Removed
lib/api/bounties.ts, lib/api/client.ts, lib/api/index.ts
Removed REST bounties API surface, Zod schemas and exported types, and removed client token helper exports (setAccessToken, clearAccessToken); client functions reduced to core http helpers.
GraphQL Client & Auth Utilities Added/Updated
lib/graphql/client.ts, lib/auth-utils.ts, lib/server-auth.ts
Added async cross-environment getAccessToken, moved token retrieval to auth-utils, updated GraphQL client to await tokens and include credentials, changed server session validation and getCurrentUser implementation, and exposed async hasAccessToken.
GraphQL Generated Types / Fragment Change
lib/graphql/generated.ts, lib/graphql/operations/fragments.graphql
Added createdBy: string to the Bounty fragment and related query/mutation responses and updated fragment Document.
Hooks → GraphQL Migration
hooks/use-bounties.ts, hooks/use-bounty.ts, hooks/use-bounty-search.ts, hooks/use-infinite-bounties.ts, hooks/use-bounty-mutations.ts, hooks/use-bounty-detail.ts, hooks/Use-bounty-detail.ts, hooks/use-bounty-mutations.ts
Replaced REST query/mutation implementations with GraphQL-generated hooks/fetcher, updated input/return types to GraphQL types (BountyQueryInput, BountyFieldsFragment), reshaped return data and implemented optimistic updates/invalidation for mutations.
Query Utilities & Keys
lib/query/bounty-queries.ts, lib/query/query-keys.ts, lib/query/prefetch.ts, lib/query/sync/handlers.ts
Migrated query options and key shapes to GraphQL fetcher/hooks and new bountyKeys shape; adapted pagination and data shaping to GraphQL responses; updated sync handlers to BountyFieldsFragment.
Component Prop & Type Updates
components/bounty/bounty-card.tsx, components/bounty/bounty-list.tsx, components/bounty-detail/bounty-badges.tsx, components/bounty-detail/bounty-detail-header-card.tsx, components/bounty-detail/bounty-detail-sidebar-cta.tsx, components/bounty/bounty-sidebar.tsx, components/projects/project-bounties.tsx, components/bounty-detail/bounty-detail-client.tsx
Switched components to consume GraphQL fragment types (BountyFieldsFragment), changed status/type prop typings to strings/enums, updated imports/usages, and replaced direct REST calls with GraphQL hook usage (e.g., useClaimBounty).
Config & Type Relaxation
lib/bounty-config.ts, various references
Changed STATUS_CONFIG/TYPE_CONFIG key types to string and adapted related utilities to accept GraphQL-driven string/enum values.
New Client Magic-Link Page
app/(auth)/auth/magic-link/verify/page.tsx
Added client-side magic-link verification page/component that reads token and callback from URL, validates callback path, calls authClient.magicLink.verify, and handles loading/success/error UI and redirect.
Misc Hook / Path Fixes
hooks/Use-bounty-detail.tshooks/use-bounty-detail.ts, components/bounty-detail/bounty-detail-client.tsx
Added/renamed hook wrapper useBountyDetail (wraps generated useBountyQuery) and fixed import path casing in a client component.

Sequence Diagram(s)

(Skipped)

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • Benjtalkshow
  • 0xdevcollins

Poem

"I’m a rabbit with a keyboard bright,
I hopped from REST into GraphQL light,
Tokens whisper soft through async trees,
Magic links sparkle and redirect with ease,
Hooray — new fragments and bounties take flight!" 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: update GraphQL fragments and queries for bounty management' accurately summarizes the main change: migrating bounty management from REST APIs to GraphQL.
Linked Issues check ✅ Passed All major requirements from issue #100 are met: GraphQL hooks replace REST bountiesApi, Next.js API routes under app/api/bounties/ are removed, lib/api/bounties.ts REST methods are deleted, and all bounty data flows through GraphQL.
Out of Scope Changes check ✅ Passed Changes are narrowly focused on replacing REST with GraphQL for bounty management. Magic link verification page addition aligns with auth improvements mentioned in server-auth enhancements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link

@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)
lib/query/sync/handlers.ts (1)

13-25: ⚠️ Potential issue | 🔴 Critical

Cache shape mismatch causes runtime errors when search query cache exists.

setQueriesData with bountyKeys.lists() (["Bounties"]) prefix-matches all queries keyed under ["Bounties", ...]. However, use-bounty-search.ts (lines 36-48) stores { data: BountyFieldsFragment[] } (no pagination property), while sync handlers assume PaginatedResponse shape with both data and pagination fields.

When handleBountyCreated or handleBountyDeleted runs and the search query cache exists, accessing oldData.pagination.total throws TypeError: Cannot read property 'total' of undefined.

Add defensive guards in all three handlers:

Fix for all three handlers
  queryClient.setQueriesData<PaginatedResponse<BountyFieldsFragment>>(
    { queryKey: bountyKeys.lists() },
    (oldData) => {
-      if (!oldData) return oldData;
+      if (!oldData || !('pagination' in oldData)) return oldData;
       return {
         ...oldData,
         data: [bounty, ...oldData.data],
         pagination: {
           ...oldData.pagination,
           total: oldData.pagination.total + 1,
         },
       };
     },
   );

Apply the same guard in handleBountyUpdated and handleBountyDeleted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/query/sync/handlers.ts` around lines 13 - 25, The sync handlers
(handleBountyCreated, handleBountyUpdated, handleBountyDeleted) assume old cache
is PaginatedResponse and crash when a search cache stores {data:
BountyFieldsFragment[]}; update each setQueriesData callback (the calls using
queryClient.setQueriesData with bountyKeys.lists()) to defensively check that
oldData exists and has the expected pagination and data shape (e.g., typeof
oldData.pagination === 'object' and Array.isArray(oldData.data')); if the guard
fails, return oldData unchanged; only perform the paginated modifications
(adjusting data array and pagination.total) when the shape checks pass.
♻️ Duplicate comments (1)
lib/query/sync/handlers.ts (1)

39-47: ⚠️ Potential issue | 🟠 Major

Same shape mismatch risk in handleBountyUpdated.

Same issue as above — oldData.data.map(...) will fail on cache entries that don't have a .data array in the expected shape.

🛡️ Defensive guard
   queryClient.setQueriesData<PaginatedResponse<BountyFieldsFragment>>(
     { queryKey: bountyKeys.lists() },
     (oldData) => {
-      if (!oldData) return oldData;
+      if (!oldData || !Array.isArray(oldData.data)) return oldData;
       return {
         ...oldData,
         data: oldData.data.map((b) => (b.id === bounty.id ? bounty : b)),
       };
     },
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/query/sync/handlers.ts` around lines 39 - 47, The cache update in
handleBountyUpdated risks crashing when oldData isn't the expected
PaginatedResponse shape because it assumes oldData.data is an array; change the
updater passed to queryClient.setQueriesData for bountyKeys.lists() to
defensively check that oldData exists and Array.isArray(oldData.data) before
mapping—if the check fails, simply return oldData (or return a safe normalized
shape) so you only call oldData.data.map(...) when data is an array.
🧹 Nitpick comments (9)
lib/auth-utils.ts (1)

31-38: Fallback getSession() call may add latency on every request.

When the cookie is httpOnly (not accessible via document.cookie), each call to getAccessToken() triggers a network round-trip via authClient.getSession(). Since the axios interceptor calls this on every request, consider caching the token client-side (e.g., in a module-level variable with a short TTL) to avoid repeated network calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/auth-utils.ts` around lines 31 - 38, The fallback getSession() in
getAccessToken causes a network call on every request when cookies are httpOnly;
update getAccessToken to use a module-level cached token with a short TTL (e.g.,
tokenCache/tokenExpiresAt) so that if a valid token exists you return it
immediately, and only call authClient.getSession() when the cache is empty or
expired; on successful authClient.getSession() populate the cache and expiry,
and ensure the axios interceptor continues to call getAccessToken() (so it
benefits from the cache) and clears the cache on auth errors or logout.
hooks/use-bounty-mutations.ts (1)

175-209: useClaimBounty uses string literal "IN_PROGRESS" instead of the BountyStatus enum.

The rest of the codebase (e.g., project-bounties.tsx) uses BountyStatus.InProgress from the generated enums. Using a raw string here could drift out of sync if the enum value changes.

Proposed fix
+ import { BountyStatus } from "@/lib/graphql/generated";
  // ...
- ) => mutation.mutate({ input: { id, status: "IN_PROGRESS" } }, options),
+ ) => mutation.mutate({ input: { id, status: BountyStatus.InProgress } }, options),
  // ...
- mutation.mutateAsync({ input: { id, status: "IN_PROGRESS" } }, options),
+ mutation.mutateAsync({ input: { id, status: BountyStatus.InProgress } }, options),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-bounty-mutations.ts` around lines 175 - 209, In useClaimBounty
replace the hard-coded string "IN_PROGRESS" with the generated enum value
(BountyStatus.InProgress) so the mutation uses the enum constant; update imports
to include BountyStatus (if missing) and ensure the mutate/mutateAsync calls
pass { input: { id, status: BountyStatus.InProgress } } to
useUpdateBountyMutation so types (UpdateBountyMutationVariables) remain correct
and consistent with other usages like project-bounties.tsx.
hooks/Use-bounty-detail.ts (1)

6-12: useBountyDetail is redundant — it duplicates useBounty exactly.

useBountyDetail(id) and useBounty(id) (called without options) are functionally identical:

  • Both call useBountyQuery({ id }, { enabled: Boolean(id) /* or !!id */ })
  • Both return { ...rest, data: data?.bounty … }

The UseBountyOptions.enabled override in useBounty makes it a strict superset of useBountyDetail. Consider removing useBountyDetail and having its consumers call useBounty directly.

Additionally, the same null-cast issue applies here — replace the as cast with ?? undefined:

♻️ Proposed fix
  return {
    ...rest,
-   data: data?.bounty as BountyFieldsFragment | undefined,
+   data: data?.bounty ?? undefined,
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/Use-bounty-detail.ts` around lines 6 - 12, Remove the redundant hook
useBountyDetail: instead of keeping a duplicate wrapper around useBountyQuery,
update consumers to call useBounty directly (useBounty(id) already calls
useBountyQuery({ id }, { enabled: Boolean(id) }) and returns the same shape).
Delete the useBountyDetail function and any imports/usages of it, and where data
typing was forced with "data?.bounty as BountyFieldsFragment | undefined"
replace that pattern with a safe coalescing expression (data?.bounty ??
undefined) when refactoring remaining call sites to maintain the same runtime
behavior and correct typing.
hooks/use-bounty.ts (1)

16-19: Prefer ?? undefined to coerce null and maintain type fidelity.

data?.bounty is typed as BountyFieldsFragment | null | undefined by the generated code (GraphQL fields can return null for not-found). The as cast strips null from the TypeScript type without converting it at runtime, so consumers receive null while TypeScript believes they receive BountyFieldsFragment | undefined.

♻️ Proposed fix
  return {
    ...rest,
-   data: data?.bounty as BountyFieldsFragment | undefined,
+   data: data?.bounty ?? undefined,
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-bounty.ts` around lines 16 - 19, The returned data field currently
uses an unsafe cast ("data?.bounty as BountyFieldsFragment | undefined") which
strips null from the static type but leaves null at runtime; replace the cast by
coercing null to undefined using the nullish coalescing operator (e.g., use
data?.bounty ?? undefined) and drop the "as" assertion so the returned type
aligns with runtime values (refer to data?.bounty and BountyFieldsFragment in
hooks/use-bounty.ts).
lib/query/bounty-queries.ts (1)

47-53: Casting to BountyFieldsFragment discards the submissions field from the type.

The BountyDocument GraphQL query fetches submissions { ...SubmissionFields } alongside ...BountyFields, but the cast on Line 52 narrows the TypeScript type to BountyFieldsFragment which doesn't include submissions. At runtime the data is there, but consumers (e.g. bounty-sidebar.tsx) are forced to manually extend the type with & { submissions?: ... }, which is fragile and can drift.

Consider returning the full BountyQuery["bounty"] type instead, or creating a dedicated fragment/type that includes submissions.

♻️ Minimal fix: use the query's own return type
 export function bountyDetailQueryOptions(id: string) {
-  return queryOptions({
+  return queryOptions<BountyQuery["bounty"]>({
     queryKey: bountyKeys.detail(id),
     queryFn: async () => {
       const response = await fetcher<BountyQuery, { id: string }>(
         BountyDocument,
         { id },
       )();
-      return response.bounty as BountyFieldsFragment;
+      return response.bounty;
     },
     enabled: !!id,
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/query/bounty-queries.ts` around lines 47 - 53, The queryFn is casting
response.bounty to BountyFieldsFragment which drops the fetched submissions
field; update queryFn to return the query's actual type (use
BountyQuery["bounty"] or a dedicated type that includes submissions) instead of
BountyFieldsFragment so consumers receive submissions without manual extension —
locate queryFn in bounty-queries.ts (uses BountyDocument, BountyQuery, and
BountyFieldsFragment) and change the returned type to the fuller
BountyQuery["bounty"] (or create/return a fragment type that includes
submissions).
components/bounty-detail/bounty-badges.tsx (1)

3-4: Fallback to COMPLETED for unknown status may mislead users.

If an unrecognized status string arrives (e.g. from a future backend enum value), the badge will silently render as "Completed" with its grey styling, which could confuse users into thinking a bounty is done when it isn't. Consider falling back to a more neutral config (e.g. DRAFT or a dedicated UNKNOWN entry) that won't be misinterpreted as a terminal state. Same reasoning applies to FIXED_PRICE on Line 16, though the impact there is lower.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-badges.tsx` around lines 3 - 4, The
StatusBadge currently falls back to STATUS_CONFIG.COMPLETED for unknown statuses
which can mislead users; update the fallback to a neutral entry (add
STATUS_CONFIG.UNKNOWN or use STATUS_CONFIG.DRAFT) and use that instead of
COMPLETED in StatusBadge (and similarly replace the FIXED_PRICE fallback with
the neutral config in the related price-badge logic). Locate STATUS_CONFIG, the
StatusBadge function, and the price/fixed-price handling, add an UNKNOWN (or
DRAFT) config with neutral text/styling, and change the fallback expressions to
reference that neutral entry so unrecognized backend enums render safely.
components/bounty/bounty-sidebar.tsx (2)

34-41: Side effect in render body — move console.warn into a useEffect.

This console.warn runs on every render when the env flag is set. Placing side effects directly in the component body (outside of useEffect) is a React anti-pattern and will produce excessive log noise.

♻️ Suggested fix
+  useEffect(() => {
+    if (process.env.NEXT_PUBLIC_MOCK_MAINTAINER === "true") {
+      console.warn(
+        "DEV: Mock maintainer enabled in components/bounty/bounty-sidebar.tsx — do NOT enable in production",
+      );
+    }
+  }, []);
+
-  if (
-    typeof window !== "undefined" &&
-    process.env.NEXT_PUBLIC_MOCK_MAINTAINER === "true"
-  ) {
-    console.warn(
-      "DEV: Mock maintainer enabled in components/bounty/bounty-sidebar.tsx — do NOT enable in production",
-    );
-  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/bounty-sidebar.tsx` around lines 34 - 41, Move the
console.warn side effect out of the component render and into a React effect:
inside the BountySidebar component (or whichever component contains the current
typeof window && process.env.NEXT_PUBLIC_MOCK_MAINTAINER check), replace the
top-level console.warn with a useEffect that runs on mount (e.g., useEffect(()
=> { if (typeof window !== "undefined" &&
process.env.NEXT_PUBLIC_MOCK_MAINTAINER === "true") console.warn(...); }, []));
keep the same warning message and window/env checks but ensure they execute only
inside the useEffect to avoid running on every render.

92-117: Mock completion flow should be tracked or removed before release.

handleMarkCompleted uses setTimeout to simulate an API call. This is fine for development but should be wired to a real GraphQL mutation (consistent with the rest of this PR's migration) before shipping. Consider adding a // TODO: comment referencing a tracking issue if this isn't being addressed in this PR.

Would you like me to open an issue to track replacing this mock with a real GraphQL mutation?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/bounty-sidebar.tsx` around lines 92 - 117,
handleMarkCompleted currently uses a setTimeout to simulate the completion API
call; replace this mock with the actual GraphQL mutation flow (call the
appropriate mutation, await its result, handle errors, and only then call
setLoading(false), setCompleted(true), setRatingTarget(...) and
setShowRating(true)). Update the function handleMarkCompleted to invoke the
project's real completion mutation and derive contributorId/contributorName from
the mutation response (falling back to bounty.createdBy as now), handle errors
via the existing error/reporting pattern, and remove the setTimeout; if you
can't implement the mutation in this PR add a clear TODO with a tracking issue
reference (e.g., "// TODO: replace mock setTimeout with completeBounty mutation
— see ISSUE-1234") so it is tracked before release.
lib/query/query-keys.ts (1)

15-16: Unsafe cast params as BountyQueryInput erases the Omit<..., "page"> constraint.

The infinite factory accepts Omit<BountyQueryInput, "page"> but immediately casts to BountyQueryInput, defeating the purpose of the Omit. If getKey actually accepts the narrower type, the cast is unnecessary; if it doesn't, consider adjusting the getKey call or the parameter type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/query/query-keys.ts` around lines 15 - 16, The current infinite factory
unsafely casts params to BountyQueryInput when calling useBountiesQuery.getKey,
which discards the Omit<..., "page"> constraint; fix by removing the cast and
either (a) update useBountiesQuery.getKey to accept
Omit<BountyQueryInput,"page">, or (b) build a full BountyQueryInput before
calling getKey by merging params with a safe default page value (so you call
useBountiesQuery.getKey({ query: { ...(params || {}), page: <default> } })), or
(c) change the infinite signature to accept the exact type getKey
expects—reference the infinite factory, BountyQueryInput and
useBountiesQuery.getKey to locate the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`(auth)/auth/magic-link/verify/page.tsx:
- Around line 27-55: The verifyToken function reads callbackURL from
searchParams and passes it directly to router.push, creating an open-redirect;
fix by validating callbackURL is a safe relative path before using it (e.g.,
ensure it starts with "/" and does not include a protocol/host or backslashes),
and fall back to a safe default ("/bounty") when validation fails; update the
logic in verifyToken where callbackURL is obtained and before
router.push(callbackURL) to perform this check and use the validatedCallback (or
default) for toast/redirect.

In `@hooks/use-bounties.ts`:
- Around line 17-33: The totalPages calc in useBounties can divide by zero if
data.bounties.limit is 0; update useBounties to guard the denominator
(data.bounties.limit) before computing totalPages — e.g., treat zero or falsy
limit as 1 or return 0 totalPages when limit is <= 0. Locate the totalPages
expression in useBounties and replace Math.ceil(data.bounties.total /
data.bounties.limit) with a safe computation that uses
Math.max(data.bounties.limit, 1) or a conditional that yields 0 when limit <= 0
so Infinity is never produced.
- Around line 8-15: Remove the duplicate bountyKeys definition from
hooks/use-bounties.ts and instead import and re-export the centralized
bountyKeys from lib/query/query-keys.ts so the complete factory (including the
infinite key) is used; update any local references in hooks/use-bounties.ts to
use the imported bountyKeys; then update downstream files
hooks/use-infinite-bounties.ts and hooks/use-bounty-mutations.ts to import
bountyKeys from lib/query/query-keys.ts (so calls like
bountyKeys.infinite(params) resolve correctly).

In `@hooks/Use-bounty-detail.ts`:
- Around line 1-13: Rename the hook file from Use-bounty-detail.ts to
use-bounty-detail.ts to match the project hook naming convention; update any
imports that reference the old name (e.g., in bounty-detail-client.tsx) to
import useBountyDetail from "use-bounty-detail" (preserve the exported function
name useBountyDetail and the existing export), and ensure the repository/CI is
updated so case-sensitive filesystems see the rename as a true rename (delete
the old-cased file and add the new one in the same commit if your VCS requires
it).

In `@hooks/use-infinite-bounties.ts`:
- Around line 54-57: Remove the duplicate export of flattenBountyPages from the
hooks file: delete the local function definition
(flattenBountyPages(PaginatedResponse<BountyFieldsFragment>[] | undefined):
BountyFieldsFragment[]) and instead import the canonical implementation from the
shared query module (the re-export in lib/query index). Update any local
references to use the imported flattenBountyPages and remove the duplicate
symbol to keep a single source of truth.
- Line 16: Replace the non-canonical import of bountyKeys in
use-infinite-bounties.ts (currently pulling from hooks/use-bounties.ts) with the
centralized bountyKeys from lib/query/query-keys.ts and update the queryKey
construction to use bountyKeys.infinite(params) instead of
[...bountyKeys.lists(), "infinite", params]; ensure the hook now builds the
exact key shape used by lib/query/bounty-queries.ts (bountyKeys.infinite()
yields ["Bounties", { query: params }]) so cache lookups and invalidations are
consistent.

In `@lib/auth-utils.ts`:
- Around line 23-28: The regex building for extracting the cookie uses the
literal cookie name variable name ("boundless_auth.session_token") but the dot
is not escaped, so change the RegExp construction in the cookie lookup to escape
the dot (e.g., replace "." with "\\.") or otherwise escape regex metacharacters
in name before using it in new RegExp; update the expression that sets match
(document.cookie.match(new RegExp(...))) so it matches the exact cookie name and
continue returning decodeURIComponent(match[2]) as before.

In `@lib/query/query-keys.ts`:
- Around line 10-19: bountyKeys currently duplicates keys and breaks the
hierarchical pattern used elsewhere; update bountyKeys so its lists() and
details() build off a single base all key using spread syntax (e.g., make
lists() return [...bountyKeys.all, "lists"] and details() return
[...bountyKeys.all, "details"]) and keep detail(id) as before, ensuring any
invalidation calls that expect the hierarchy still work; alternatively, if the
broad all key is truly unused, remove bountyKeys.all and lists() together—but
prefer the spread-based restructuring to match authKeys/complianceKeys patterns
and maintain consistent invalidation scopes.

In `@lib/server-auth.ts`:
- Around line 15-37: getCurrentUser currently sets User.id to the raw session
token from getSessionCookie, causing authorization and lookup failures; instead
validate the session token and fetch the real user record (id, name, email,
etc.) before returning. Update getCurrentUser to: read the session token via
getSessionCookie, call the auth/session backend or authClient.getSession (or
your session-store lookup) with the request headers to validate the token and
retrieve the user payload, return null if validation fails, and populate the
returned User with the real user id and profile fields (not the session token).
Ensure callers still get null when no session cookie exists and log/handle
validation errors appropriately.

---

Outside diff comments:
In `@lib/query/sync/handlers.ts`:
- Around line 13-25: The sync handlers (handleBountyCreated,
handleBountyUpdated, handleBountyDeleted) assume old cache is PaginatedResponse
and crash when a search cache stores {data: BountyFieldsFragment[]}; update each
setQueriesData callback (the calls using queryClient.setQueriesData with
bountyKeys.lists()) to defensively check that oldData exists and has the
expected pagination and data shape (e.g., typeof oldData.pagination === 'object'
and Array.isArray(oldData.data')); if the guard fails, return oldData unchanged;
only perform the paginated modifications (adjusting data array and
pagination.total) when the shape checks pass.

---

Duplicate comments:
In `@lib/query/sync/handlers.ts`:
- Around line 39-47: The cache update in handleBountyUpdated risks crashing when
oldData isn't the expected PaginatedResponse shape because it assumes
oldData.data is an array; change the updater passed to
queryClient.setQueriesData for bountyKeys.lists() to defensively check that
oldData exists and Array.isArray(oldData.data) before mapping—if the check
fails, simply return oldData (or return a safe normalized shape) so you only
call oldData.data.map(...) when data is an array.

---

Nitpick comments:
In `@components/bounty-detail/bounty-badges.tsx`:
- Around line 3-4: The StatusBadge currently falls back to
STATUS_CONFIG.COMPLETED for unknown statuses which can mislead users; update the
fallback to a neutral entry (add STATUS_CONFIG.UNKNOWN or use
STATUS_CONFIG.DRAFT) and use that instead of COMPLETED in StatusBadge (and
similarly replace the FIXED_PRICE fallback with the neutral config in the
related price-badge logic). Locate STATUS_CONFIG, the StatusBadge function, and
the price/fixed-price handling, add an UNKNOWN (or DRAFT) config with neutral
text/styling, and change the fallback expressions to reference that neutral
entry so unrecognized backend enums render safely.

In `@components/bounty/bounty-sidebar.tsx`:
- Around line 34-41: Move the console.warn side effect out of the component
render and into a React effect: inside the BountySidebar component (or whichever
component contains the current typeof window &&
process.env.NEXT_PUBLIC_MOCK_MAINTAINER check), replace the top-level
console.warn with a useEffect that runs on mount (e.g., useEffect(() => { if
(typeof window !== "undefined" && process.env.NEXT_PUBLIC_MOCK_MAINTAINER ===
"true") console.warn(...); }, [])); keep the same warning message and window/env
checks but ensure they execute only inside the useEffect to avoid running on
every render.
- Around line 92-117: handleMarkCompleted currently uses a setTimeout to
simulate the completion API call; replace this mock with the actual GraphQL
mutation flow (call the appropriate mutation, await its result, handle errors,
and only then call setLoading(false), setCompleted(true), setRatingTarget(...)
and setShowRating(true)). Update the function handleMarkCompleted to invoke the
project's real completion mutation and derive contributorId/contributorName from
the mutation response (falling back to bounty.createdBy as now), handle errors
via the existing error/reporting pattern, and remove the setTimeout; if you
can't implement the mutation in this PR add a clear TODO with a tracking issue
reference (e.g., "// TODO: replace mock setTimeout with completeBounty mutation
— see ISSUE-1234") so it is tracked before release.

In `@hooks/Use-bounty-detail.ts`:
- Around line 6-12: Remove the redundant hook useBountyDetail: instead of
keeping a duplicate wrapper around useBountyQuery, update consumers to call
useBounty directly (useBounty(id) already calls useBountyQuery({ id }, {
enabled: Boolean(id) }) and returns the same shape). Delete the useBountyDetail
function and any imports/usages of it, and where data typing was forced with
"data?.bounty as BountyFieldsFragment | undefined" replace that pattern with a
safe coalescing expression (data?.bounty ?? undefined) when refactoring
remaining call sites to maintain the same runtime behavior and correct typing.

In `@hooks/use-bounty-mutations.ts`:
- Around line 175-209: In useClaimBounty replace the hard-coded string
"IN_PROGRESS" with the generated enum value (BountyStatus.InProgress) so the
mutation uses the enum constant; update imports to include BountyStatus (if
missing) and ensure the mutate/mutateAsync calls pass { input: { id, status:
BountyStatus.InProgress } } to useUpdateBountyMutation so types
(UpdateBountyMutationVariables) remain correct and consistent with other usages
like project-bounties.tsx.

In `@hooks/use-bounty.ts`:
- Around line 16-19: The returned data field currently uses an unsafe cast
("data?.bounty as BountyFieldsFragment | undefined") which strips null from the
static type but leaves null at runtime; replace the cast by coercing null to
undefined using the nullish coalescing operator (e.g., use data?.bounty ??
undefined) and drop the "as" assertion so the returned type aligns with runtime
values (refer to data?.bounty and BountyFieldsFragment in hooks/use-bounty.ts).

In `@lib/auth-utils.ts`:
- Around line 31-38: The fallback getSession() in getAccessToken causes a
network call on every request when cookies are httpOnly; update getAccessToken
to use a module-level cached token with a short TTL (e.g.,
tokenCache/tokenExpiresAt) so that if a valid token exists you return it
immediately, and only call authClient.getSession() when the cache is empty or
expired; on successful authClient.getSession() populate the cache and expiry,
and ensure the axios interceptor continues to call getAccessToken() (so it
benefits from the cache) and clears the cache on auth errors or logout.

In `@lib/query/bounty-queries.ts`:
- Around line 47-53: The queryFn is casting response.bounty to
BountyFieldsFragment which drops the fetched submissions field; update queryFn
to return the query's actual type (use BountyQuery["bounty"] or a dedicated type
that includes submissions) instead of BountyFieldsFragment so consumers receive
submissions without manual extension — locate queryFn in bounty-queries.ts (uses
BountyDocument, BountyQuery, and BountyFieldsFragment) and change the returned
type to the fuller BountyQuery["bounty"] (or create/return a fragment type that
includes submissions).

In `@lib/query/query-keys.ts`:
- Around line 15-16: The current infinite factory unsafely casts params to
BountyQueryInput when calling useBountiesQuery.getKey, which discards the
Omit<..., "page"> constraint; fix by removing the cast and either (a) update
useBountiesQuery.getKey to accept Omit<BountyQueryInput,"page">, or (b) build a
full BountyQueryInput before calling getKey by merging params with a safe
default page value (so you call useBountiesQuery.getKey({ query: { ...(params ||
{}), page: <default> } })), or (c) change the infinite signature to accept the
exact type getKey expects—reference the infinite factory, BountyQueryInput and
useBountiesQuery.getKey to locate the code.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b4cfff8 and 565abbd.

📒 Files selected for processing (37)
  • app/(auth)/auth/magic-link/verify/page.tsx
  • app/api/bounties/[id]/applications/route.ts
  • app/api/bounties/[id]/apply/route.ts
  • app/api/bounties/[id]/claim/route.ts
  • app/api/bounties/[id]/competition/join/route.ts
  • app/api/bounties/[id]/join/route.ts
  • app/api/bounties/[id]/milestones/advance/route.ts
  • app/api/bounties/[id]/route.ts
  • app/api/bounties/[id]/submissions/route.ts
  • app/api/bounties/[id]/submit/route.ts
  • app/api/bounties/route.ts
  • components/bounty-detail/bounty-badges.tsx
  • components/bounty-detail/bounty-detail-header-card.tsx
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx
  • components/bounty/bounty-card.tsx
  • components/bounty/bounty-list.tsx
  • components/bounty/bounty-sidebar.tsx
  • components/projects/project-bounties.tsx
  • hooks/Use-bounty-detail.ts
  • hooks/use-bounties.ts
  • hooks/use-bounty-mutations.ts
  • hooks/use-bounty-search.ts
  • hooks/use-bounty.ts
  • hooks/use-infinite-bounties.ts
  • lib/api/bounties.ts
  • lib/api/client.ts
  • lib/api/index.ts
  • lib/auth-utils.ts
  • lib/bounty-config.ts
  • lib/graphql/client.ts
  • lib/graphql/generated.ts
  • lib/graphql/operations/fragments.graphql
  • lib/query/bounty-queries.ts
  • lib/query/prefetch.ts
  • lib/query/query-keys.ts
  • lib/query/sync/handlers.ts
  • lib/server-auth.ts
💤 Files with no reviewable changes (11)
  • app/api/bounties/[id]/apply/route.ts
  • app/api/bounties/[id]/claim/route.ts
  • app/api/bounties/[id]/milestones/advance/route.ts
  • app/api/bounties/[id]/submit/route.ts
  • lib/api/bounties.ts
  • app/api/bounties/[id]/join/route.ts
  • app/api/bounties/[id]/competition/join/route.ts
  • app/api/bounties/[id]/submissions/route.ts
  • app/api/bounties/[id]/applications/route.ts
  • app/api/bounties/route.ts
  • app/api/bounties/[id]/route.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
lib/server-auth.ts (2)

57-61: Loose type assertion on the response payload.

The normalization at lines 58-61 uses as casts on the raw JSON without runtime validation. If the backend returns an unexpected shape (e.g., { data: null } or { data: "string" }), normalized could be null/malformed and slip through. The normalized?.user?.id check on line 63 does provide a safety net, but consider adding a basic structural check (e.g., verifying normalized is a non-null object) to avoid subtle issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/server-auth.ts` around lines 57 - 61, The JSON normalization uses
unchecked casts (payload as ...) which can produce null/malformed normalized
values; replace the cast with a runtime type guard that validates payload/data
is a non-null object and conforms at least to the minimal SessionPayload shape
(e.g., has a non-null object "user" with a string "id") before assigning to
normalized (or return undefined otherwise). Implement a small type-guard helper
like isSessionPayload(value): value is SessionPayload and use it when handling
response.json() so normalized is only set when the structural check passes (then
downstream checks like normalized?.user?.id are safe).

25-72: Session validation now fetches real user data — resolves the previously reported critical issue.

The rewrite correctly validates the session against the backend and extracts the actual user identity, fixing the prior bug where the raw session token was used as User.id.

Two resilience concerns with fetchValidatedSession:

  1. No fetch timeout. If the auth endpoint is unresponsive, this blocks server component rendering indefinitely. Consider adding an AbortSignal.timeout().

  2. Multi-endpoint fallback is asymmetric on 401/403. Lines 51-53 return null immediately on the first endpoint's 401/403 without trying the second endpoint. If only the first endpoint is deployed (or vice versa), this is fine — but if the endpoints have different auth behaviors, the early return could mask a valid session on the second endpoint.

🛡️ Suggested timeout addition
       const url = new URL(endpoint, baseURL);
       const response = await fetch(url, {
         method: "GET",
         headers: {
           cookie: cookieHeader,
           authorization: `Bearer ${sessionToken}`,
           accept: "application/json",
         },
         cache: "no-store",
+        signal: AbortSignal.timeout(5000),
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/server-auth.ts` around lines 25 - 72, fetchValidatedSession currently can
hang because fetch has no timeout and it returns early on a 401/403 from the
first endpoint, which prevents trying the fallback endpoint; update
fetchValidatedSession to use an AbortController/AbortSignal.timeout (or
AbortController with setTimeout) when calling fetch so each request times out
(ensure the controller is aborted and cleaned up), and change the 401/403
handling inside the endpoints loop (where response.status is checked) to
continue to the next endpoint instead of immediately returning null so both
endpoints are tried before returning null; reference the function
fetchValidatedSession and the endpoints array when making these edits.
lib/graphql/client.ts (1)

62-74: Multiple toast notifications fire for batch auth errors.

If gqlError.response.errors contains more than one entry with an auth status, the forEach loop will trigger toast.error(...) and dispatch auth:unauthorized for each one. This results in duplicate toasts stacked for the user.

♻️ Suggested fix: break after the first auth error
-      if (gqlError?.response?.errors) {
-        gqlError.response.errors.forEach((err) => {
-          const status = err?.extensions?.status ?? 500;
-          if (isAuthStatus(status)) {
-            // Let the application handle unauthorized state, potentially redirecting to login
-            if (typeof window !== "undefined") {
-              toast.error("Your session has expired. Please log in again.");
-              window.dispatchEvent(
-                new CustomEvent("auth:unauthorized", { detail: { status } }),
-              );
-            }
-          }
-        });
-      }
+      if (gqlError?.response?.errors) {
+        const authErr = gqlError.response.errors.find((err) =>
+          isAuthStatus(err?.extensions?.status ?? 500),
+        );
+        if (authErr && typeof window !== "undefined") {
+          toast.error("Your session has expired. Please log in again.");
+          window.dispatchEvent(
+            new CustomEvent("auth:unauthorized", {
+              detail: { status: authErr.extensions?.status },
+            }),
+          );
+        }
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/graphql/client.ts` around lines 62 - 74, The loop over
gqlError.response.errors can emit multiple auth handling actions; in the
error-processing block (where gqlError, gqlError.response.errors, isAuthStatus,
toast.error and the "auth:unauthorized" CustomEvent are used) change the logic
to stop after the first authentication error is handled (e.g., break/return out
of the loop or use .some/find to short-circuit) so that toast.error and
window.dispatchEvent are invoked only once per GraphQL response.
hooks/use-bounties.ts (1)

18-19: page is derived from input params, not from the server response.

Line 19 uses params?.page ?? 1 for the pagination page value. If the server normalizes or clamps the page (e.g., caps at totalPages), the client-side value won't reflect the server's actual page. Consider using a page field from the GraphQL response if one is available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-bounties.ts` around lines 18 - 19, The pagination.page in
hooks/use-bounties.ts is set from the input params (params?.page ?? 1) which may
diverge from the server-normalized page; update the hook to use the
server-returned page field instead (for example, replace params?.page with the
GraphQL response page such as data?.bounties?.page or data?.page depending on
your schema) inside the pagination object so pagination reflects the server's
clamp/normalization; adjust any typing and null checks accordingly in
useBounties to fall back to 1 only if the server page is absent.
hooks/use-infinite-bounties.ts (1)

15-48: Specify TPageParam generic to eliminate pageParam as number casts.

In TanStack Query v5, useInfiniteQuery accepts a TPageParam generic parameter. Without it, pageParam defaults to unknown, requiring the as number casts on lines 24 and 32. Passing the generic makes this type-safe.

♻️ Suggested fix
-  return useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>({
+  return useInfiniteQuery<
+    PaginatedResponse<BountyFieldsFragment>,
+    Error,
+    InfiniteData<PaginatedResponse<BountyFieldsFragment>>,
+    ReturnType<typeof bountyKeys.infinite>,
+    number
+  >({
     queryKey: bountyKeys.infinite(params),
     queryFn: async ({ pageParam }) => {
       const response = await fetcher<
         BountiesQuery,
         { query: BountyQueryInput }
       >(BountiesDocument, {
         query: {
           ...params,
-          page: pageParam as number,
+          page: pageParam,
           limit: params?.limit ?? DEFAULT_LIMIT,
         },
       })();
       const data = response.bounties;
       return {
         data: data.bounties as BountyFieldsFragment[],
         pagination: {
-          page: pageParam as number,
+          page: pageParam,
           limit: data.limit,
           total: data.total,
           totalPages: Math.ceil(data.total / data.limit),
         },
       };
     },

Note: You'll need to add import { type InfiniteData } from "@tanstack/react-query".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-infinite-bounties.ts` around lines 15 - 48, The issue is that
useInfiniteQuery's pageParam is typed as unknown, causing unsafe "as number"
casts in queryFn and pagination helpers; fix it by adding the TPageParam generic
(number) to the useInfiniteQuery call so pageParam is typed as number (adjust
the other generic type args as needed for
PaginatedResponse<BountyFieldsFragment>), remove the "as number" casts in
queryFn and pagination logic (queryFn, getNextPageParam, getPreviousPageParam,
initialPageParam), and add the import for type InfiniteData from
"@tanstack/react-query" if your surrounding code requires that type.
hooks/use-bounty-mutations.ts (1)

198-198: Replace hardcoded status strings with the BountyStatus enum for type safety.

The strings "IN_PROGRESS" at lines 198-199 match the enum value BountyStatus.InProgress = "IN_PROGRESS", but using the generated enum directly would provide better type safety and maintainability. Import BountyStatus from @/lib/graphql/generated and replace both occurrences with BountyStatus.InProgress.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-bounty-mutations.ts` at line 198, Replace the hardcoded status
string "IN_PROGRESS" used in the mutation call with the generated enum value
BountyStatus.InProgress for type safety: import { BountyStatus } from
"@/lib/graphql/generated" and update the mutation invocation (the call site
using mutation.mutate({ input: { id, status: "IN_PROGRESS" } }, options)) to use
status: BountyStatus.InProgress; make the same replacement for any other
occurrence in this file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@hooks/use-bounty-mutations.ts`:
- Around line 53-68: The optimistic update is reading and writing the wrong type
for the cache key used by bountyKeys.detail(id): change getQueryData and
setQueryData to use BountyQuery (not BountyFieldsFragment), import BountyQuery
from "@/lib/graphql/generated", read previous as const previous =
queryClient.getQueryData<BountyQuery>(bountyKeys.detail(id)), and when updating
call queryClient.setQueryData<BountyQuery>(bountyKeys.detail(id), { ...previous,
bounty: { ...previous!.bounty, ...variables.input, updatedAt: new
Date().toISOString() } }), preserving the { bounty: BountyFieldsFragment }
wrapper so useBountyDetail's data?.bounty remains valid.
- Around line 175-186: useClaimBounty writes data.updateBounty (a
BountyFieldsFragment) directly into the detail cache key which expects a
BountyQuery shape ({ bounty: ... }), causing useBountyDetail to see undefined;
update the onSuccess in useClaimBounty (the useUpdateBountyMutation callback) to
setQueryData for bountyKeys.detail(variables.input.id) with an object shaped
like { bounty: data.updateBounty } instead of data.updateBounty, preserving the
expected BountyQuery format and keep the existing queryClient.invalidateQueries
call.

In `@hooks/use-infinite-bounties.ts`:
- Line 35: The totalPages calculation in the use-infinite-bounties hook can
produce Infinity when data.limit is 0; update the logic that computes totalPages
(currently Math.ceil(data.total / data.limit)) to guard against zero or falsy
limits by treating a zero limit as 1 or returning 0 pages (e.g., if data.limit
<= 0 then totalPages = 0 else Math.ceil(data.total / data.limit)); apply this
change where totalPages is computed to ensure safe behavior when data.limit is
0.

---

Nitpick comments:
In `@hooks/use-bounties.ts`:
- Around line 18-19: The pagination.page in hooks/use-bounties.ts is set from
the input params (params?.page ?? 1) which may diverge from the
server-normalized page; update the hook to use the server-returned page field
instead (for example, replace params?.page with the GraphQL response page such
as data?.bounties?.page or data?.page depending on your schema) inside the
pagination object so pagination reflects the server's clamp/normalization;
adjust any typing and null checks accordingly in useBounties to fall back to 1
only if the server page is absent.

In `@hooks/use-bounty-mutations.ts`:
- Line 198: Replace the hardcoded status string "IN_PROGRESS" used in the
mutation call with the generated enum value BountyStatus.InProgress for type
safety: import { BountyStatus } from "@/lib/graphql/generated" and update the
mutation invocation (the call site using mutation.mutate({ input: { id, status:
"IN_PROGRESS" } }, options)) to use status: BountyStatus.InProgress; make the
same replacement for any other occurrence in this file.

In `@hooks/use-infinite-bounties.ts`:
- Around line 15-48: The issue is that useInfiniteQuery's pageParam is typed as
unknown, causing unsafe "as number" casts in queryFn and pagination helpers; fix
it by adding the TPageParam generic (number) to the useInfiniteQuery call so
pageParam is typed as number (adjust the other generic type args as needed for
PaginatedResponse<BountyFieldsFragment>), remove the "as number" casts in
queryFn and pagination logic (queryFn, getNextPageParam, getPreviousPageParam,
initialPageParam), and add the import for type InfiniteData from
"@tanstack/react-query" if your surrounding code requires that type.

In `@lib/graphql/client.ts`:
- Around line 62-74: The loop over gqlError.response.errors can emit multiple
auth handling actions; in the error-processing block (where gqlError,
gqlError.response.errors, isAuthStatus, toast.error and the "auth:unauthorized"
CustomEvent are used) change the logic to stop after the first authentication
error is handled (e.g., break/return out of the loop or use .some/find to
short-circuit) so that toast.error and window.dispatchEvent are invoked only
once per GraphQL response.

In `@lib/server-auth.ts`:
- Around line 57-61: The JSON normalization uses unchecked casts (payload as
...) which can produce null/malformed normalized values; replace the cast with a
runtime type guard that validates payload/data is a non-null object and conforms
at least to the minimal SessionPayload shape (e.g., has a non-null object "user"
with a string "id") before assigning to normalized (or return undefined
otherwise). Implement a small type-guard helper like isSessionPayload(value):
value is SessionPayload and use it when handling response.json() so normalized
is only set when the structural check passes (then downstream checks like
normalized?.user?.id are safe).
- Around line 25-72: fetchValidatedSession currently can hang because fetch has
no timeout and it returns early on a 401/403 from the first endpoint, which
prevents trying the fallback endpoint; update fetchValidatedSession to use an
AbortController/AbortSignal.timeout (or AbortController with setTimeout) when
calling fetch so each request times out (ensure the controller is aborted and
cleaned up), and change the 401/403 handling inside the endpoints loop (where
response.status is checked) to continue to the next endpoint instead of
immediately returning null so both endpoints are tried before returning null;
reference the function fetchValidatedSession and the endpoints array when making
these edits.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 565abbd and f86de9f.

📒 Files selected for processing (9)
  • app/(auth)/auth/magic-link/verify/page.tsx
  • components/bounty-detail/bounty-detail-client.tsx
  • hooks/use-bounties.ts
  • hooks/use-bounty-detail.ts
  • hooks/use-bounty-mutations.ts
  • hooks/use-infinite-bounties.ts
  • lib/auth-utils.ts
  • lib/graphql/client.ts
  • lib/server-auth.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/(auth)/auth/magic-link/verify/page.tsx
  • lib/auth-utils.ts

Copy link

@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.

♻️ Duplicate comments (2)
hooks/use-bounty-mutations.ts (2)

56-77: Previous cache-type issues are fully resolved — LGTM.

Both getQueryData<BountyQuery> (Line 56) and setQueryData<BountyQuery> with the preserved { ...previous, bounty: { ... } } wrapper (Lines 67–74) are now correct. useBountyDetail's data?.bounty access will remain valid after the optimistic update.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-bounty-mutations.ts` around lines 56 - 77, The optimistic cache
update using queryClient.getQueryData<BountyQuery> and
queryClient.setQueryData<BountyQuery> with the preserved wrapper around previous
and bounty (using bountyKeys.detail(id)) is correct and requires no code
changes; keep the current logic in the mutation (including the optimisticInput
construction and updatedAt override) so useBountyDetail will continue to see
data?.bounty as expected.

188-195: Previous cache-shape mismatch is resolved — LGTM.

queryClient.setQueryData<BountyQuery> now correctly wraps the result as { bounty: data.updateBounty } (Line 191), matching the shape useBountyDetail expects.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-bounty-mutations.ts` around lines 188 - 195, The cache shape
mismatch has been resolved by wrapping the updated bounty result in the expected
object shape; keep the onSuccess handler in the mutation (the
queryClient.setQueryData<BountyQuery> call that sets
bountyKeys.detail(variables.input.id) to { bounty: data.updateBounty } and the
subsequent queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }))
as-is—no change needed.
🧹 Nitpick comments (4)
hooks/use-infinite-bounties.ts (1)

15-15: Dropping the explicit generic lets TypeScript infer TPageParam = number, removing the as number casts.

With useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>, only TQueryFnData is fixed; TPageParam defaults to unknown, which is why pageParam as number is needed twice (lines 24 and 32). Removing the explicit generic allows TanStack Query v5 to infer TPageParam = number directly from initialPageParam: 1.

♻️ Proposed refactor
-  return useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>({
+  return useInfiniteQuery({
     queryKey: bountyKeys.infinite(params),
     queryFn: async ({ pageParam }) => {
       const response = await fetcher<
         BountiesQuery,
         { query: BountyQueryInput }
       >(BountiesDocument, {
         query: {
           ...params,
-          page: pageParam as number,
+          page: pageParam,
           limit: params?.limit ?? DEFAULT_LIMIT,
         },
       })();
       const data = response.bounties;
       return {
         data: data.bounties as BountyFieldsFragment[],
         pagination: {
-          page: pageParam as number,
+          page: pageParam,
           limit: data.limit,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-infinite-bounties.ts` at line 15, Remove the explicit generic on
useInfiniteQuery so TanStack Query can infer TPageParam as number (i.e., change
the call to useInfiniteQuery(...) instead of
useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>), keep
initialPageParam: 1, and then remove the redundant pageParam as number casts
found in the fetch function and getNextPageParam logic (references:
useInfiniteQuery call, initialPageParam, and the occurrences of pageParam as
number). This will let the hook infer page param type correctly and eliminate
the unnecessary type assertions.
hooks/use-bounty-mutations.ts (3)

61-65: Null filter in optimistic update silently drops intentional field-clearing.

Filtering null values alongside undefined means that if a consumer explicitly sets a field to null to clear it (valid for nullable GraphQL fields), the optimistic update will not reflect that change — it will keep the old value until the server confirms. Only undefined (i.e. "not provided") should be excluded.

♻️ Proposed fix
- ([, value]) => value !== undefined && value !== null,
+ ([, value]) => value !== undefined,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-bounty-mutations.ts` around lines 61 - 65, The optimistic update
currently removes both undefined and null values when building optimisticInput
in the optimistic update (see the optimisticInput creation that filters
Object.entries(variables.input)), which prevents intentionally-cleared nullable
fields from being represented; change the filter to only exclude undefined
(i.e., ([, value]) => value !== undefined) so null values are preserved in
optimisticInput and null-clearing is reflected immediately in the UI.

97-119: MutateOptions exposes the internal variable shape, not the wrapper's public input shape.

The wrapper changes the public API from { input: UpdateBountyInput } to { id, data }, but the options parameter is still typed as MutateOptions<UpdateBountyMutation, unknown, UpdateBountyMutationVariables, unknown>. Since options is passed through to the underlying mutation, callbacks like onSuccess(data, variables) will receive variables = { input: UpdateBountyInput } at runtime — not { id, data } as a consumer might expect from the wrapper's call signature. The same applies to useDeleteBounty (whose consumers call mutate(id: string) but callbacks receive { id: string }).

If per-call callback consumers are uncommon, this is low priority. To fix it properly, define wrapper-local MutateOptions types that match the public input and intercept/adapt the callbacks before forwarding to the underlying mutation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-bounty-mutations.ts` around lines 97 - 119, The current
mutate/mutateAsync signatures (in hooks/use-bounty-mutations.ts) accept public
inputs ({ id, data }) but still type options as MutateOptions<...,
UpdateBountyMutationVariables, ...>, which exposes the underlying { input:
UpdateBountyInput } shape to callbacks; define wrapper-local option types that
use the public variable shape (e.g., { id: string; data:
Omit<UpdateBountyInput,"id"> }) for both useUpdateBounty's mutate/mutateAsync
and useDeleteBounty's mutate, then intercept/adapt the options before forwarding
to mutation.mutate/mutateAsync: wrap onSuccess/onError/onSettled callbacks to
translate the variables parameter from the underlying { input: UpdateBountyInput
} to the wrapper { id, data } (and vice‑versa when calling mutation) so runtime
callback args match the public API while preserving underlying call behavior.

207-217: Use the generated BountyStatus enum instead of the raw string literal for consistency and maintainability.

The file currently uses "IN_PROGRESS" as a string literal in both mutate and mutateAsync bodies. The generated BountyStatus enum (with BountyStatus.InProgress = "IN_PROGRESS") is available and should be used instead. While the current code passes type-checking since UpdateBountyInput.status accepts any string, using the enum prevents typos, improves refactor-resilience, and aligns with generated code best practices.

♻️ Suggested refactor
-import {
+import {
+  BountyStatus,
   useCreateBountyMutation,
-    mutation.mutate({ input: { id, status: "IN_PROGRESS" } }, options),
+    mutation.mutate({ input: { id, status: BountyStatus.InProgress } }, options),
-      mutation.mutateAsync({ input: { id, status: "IN_PROGRESS" } }, options),
+      mutation.mutateAsync({ input: { id, status: BountyStatus.InProgress } }, options),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-bounty-mutations.ts` around lines 207 - 217, Replace the raw string
literal "IN_PROGRESS" with the generated enum value BountyStatus.InProgress in
both the mutate and mutateAsync calls inside the object that wraps
mutation.mutate and mutation.mutateAsync; update the payloads ({ input: { id,
status: ... } }) used by mutate and mutateAsync so they reference
BountyStatus.InProgress (importing BountyStatus if not already) to ensure
enum-based status usage across use-bounty-mutations.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@hooks/use-bounty-mutations.ts`:
- Around line 56-77: The optimistic cache update using
queryClient.getQueryData<BountyQuery> and queryClient.setQueryData<BountyQuery>
with the preserved wrapper around previous and bounty (using
bountyKeys.detail(id)) is correct and requires no code changes; keep the current
logic in the mutation (including the optimisticInput construction and updatedAt
override) so useBountyDetail will continue to see data?.bounty as expected.
- Around line 188-195: The cache shape mismatch has been resolved by wrapping
the updated bounty result in the expected object shape; keep the onSuccess
handler in the mutation (the queryClient.setQueryData<BountyQuery> call that
sets bountyKeys.detail(variables.input.id) to { bounty: data.updateBounty } and
the subsequent queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }))
as-is—no change needed.

---

Nitpick comments:
In `@hooks/use-bounty-mutations.ts`:
- Around line 61-65: The optimistic update currently removes both undefined and
null values when building optimisticInput in the optimistic update (see the
optimisticInput creation that filters Object.entries(variables.input)), which
prevents intentionally-cleared nullable fields from being represented; change
the filter to only exclude undefined (i.e., ([, value]) => value !== undefined)
so null values are preserved in optimisticInput and null-clearing is reflected
immediately in the UI.
- Around line 97-119: The current mutate/mutateAsync signatures (in
hooks/use-bounty-mutations.ts) accept public inputs ({ id, data }) but still
type options as MutateOptions<..., UpdateBountyMutationVariables, ...>, which
exposes the underlying { input: UpdateBountyInput } shape to callbacks; define
wrapper-local option types that use the public variable shape (e.g., { id:
string; data: Omit<UpdateBountyInput,"id"> }) for both useUpdateBounty's
mutate/mutateAsync and useDeleteBounty's mutate, then intercept/adapt the
options before forwarding to mutation.mutate/mutateAsync: wrap
onSuccess/onError/onSettled callbacks to translate the variables parameter from
the underlying { input: UpdateBountyInput } to the wrapper { id, data } (and
vice‑versa when calling mutation) so runtime callback args match the public API
while preserving underlying call behavior.
- Around line 207-217: Replace the raw string literal "IN_PROGRESS" with the
generated enum value BountyStatus.InProgress in both the mutate and mutateAsync
calls inside the object that wraps mutation.mutate and mutation.mutateAsync;
update the payloads ({ input: { id, status: ... } }) used by mutate and
mutateAsync so they reference BountyStatus.InProgress (importing BountyStatus if
not already) to ensure enum-based status usage across use-bounty-mutations.ts.

In `@hooks/use-infinite-bounties.ts`:
- Line 15: Remove the explicit generic on useInfiniteQuery so TanStack Query can
infer TPageParam as number (i.e., change the call to useInfiniteQuery(...)
instead of useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>), keep
initialPageParam: 1, and then remove the redundant pageParam as number casts
found in the fetch function and getNextPageParam logic (references:
useInfiniteQuery call, initialPageParam, and the occurrences of pageParam as
number). This will let the hook infer page param type correctly and eliminate
the unnecessary type assertions.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f86de9f and 961d51c.

📒 Files selected for processing (2)
  • hooks/use-bounty-mutations.ts
  • hooks/use-infinite-bounties.ts

Copy link

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/query/query-keys.ts`:
- Around line 13-14: bountyKeys.details() is dead code because
bountyKeys.detail(id) uses useBountyQuery.getKey({ id }) (root "Bounty") so the
keys don't share a prefix; fix by either (A) remove the unused
bountyKeys.details() export and any unreachable types/usages (verify
BountyQueryKey still matches), or (B) change the key hierarchy so detail(id) is
a child of details() — i.e., make bountyKeys.detail = (id) =>
[...bountyKeys.details(), id] (and then update all useBountyQuery call sites to
pass queryKey: bountyKeys.detail(id)); choose one option and apply consistently
across bountyKeys, useBountyQuery call sites (e.g., in bounty-queries.ts), and
types.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 961d51c and 48607ca.

📒 Files selected for processing (2)
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx
  • lib/query/query-keys.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx

@0xdevcollins 0xdevcollins merged commit f1fcb62 into boundlessfi:main Feb 23, 2026
2 of 3 checks passed
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.

Create a GraphQL API service layer

2 participants