Skip to content

feat: Elysia YouTube channel-info POC with typed client and OpenAPI#1626

Open
arpitgupta1214 wants to merge 1 commit intotestfrom
codex/elysia-youtube-poc
Open

feat: Elysia YouTube channel-info POC with typed client and OpenAPI#1626
arpitgupta1214 wants to merge 1 commit intotestfrom
codex/elysia-youtube-poc

Conversation

@arpitgupta1214
Copy link
Copy Markdown
Collaborator

@arpitgupta1214 arpitgupta1214 commented Apr 1, 2026

Summary

  • add chat-local Elysia scaffold with nested route modules under lib/api/elysia/routes/youtube/channelInfo.ts
  • migrate /api/youtube/channel-info to Elysia while keeping Next route as thin adapter
  • add auth macro (header/cookie-aware) and status-based error handling
  • add shared Eden client exposing api.youtube.channelInfo
  • remove redundant lib/youtube/fetchYouTubeChannel.ts helper
  • add OpenAPI passthrough route at /api/openapi and /api/openapi/json
  • update YouTube hooks/types to drop tokenStatus dependency

Notes

  • Eden client is configured with credentials: include for cookie auth.
  • pnpm exec tsc --noEmit still shows pre-existing baseline errors in lib/emails/__tests__/extractSendEmailResults.test.ts unrelated to this PR.

Summary by CodeRabbit

  • New Features

    • Introduced interactive API documentation with automatic schema generation
    • Added authentication support via API keys and token verification
  • Refactor

    • Migrated backend API infrastructure for improved stability and documentation
    • Updated API client integration to use new endpoint structure

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 1, 2026

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

Project Deployment Actions Updated (UTC)
recoup-chat Error Error Apr 3, 2026 5:27pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request migrates YouTube channel info fetching from a Next.js API route to an Elysia-based REST API framework. It introduces authentication plugins for API key and Privy token validation, updates hooks and components to use the new API client, and removes the legacy fetch implementation.

Changes

Cohort / File(s) Summary
API Route Removal
app/api/youtube/channel-info/route.ts, lib/youtube/fetchYouTubeChannel.ts
Removed Next.js GET endpoint and direct fetch utility that previously handled YouTube channel info requests. Functionality migrated to Elysia framework.
Elysia API Framework
lib/api/elysia/app.ts, lib/api/elysia/client.ts, lib/api/elysia/routes/index.ts, lib/api/elysia/routes/youtube/index.ts, lib/api/elysia/routes/youtube/channelInfo.ts
Established new Elysia application with OpenAPI configuration, treaty client, route composition, and YouTube channel info endpoint with query validation and typed responses.
Elysia Authentication Plugin
lib/api/elysia/plugins/auth.ts
Added auth plugin that derives per-request user identity from x-api-key header or privy-token cookie, resolving account IDs via external lookups and enforcing 401 on missing authentication.
Authentication Utilities
lib/supabase/account_api_keys/getAccountIdFromApiKey.ts, lib/auth/getAccountIdFromPrivyToken.ts
Introduced API key lookup with SHA-256 HMAC hashing and Privy token verification with email-to-account resolution for auth plugin support.
Hook Updates
hooks/useYoutubeChannel.tsx, hooks/useYoutubeStatus.ts, hooks/useYouTubeLoginSuccess.ts
Updated hooks to call api.youtube["channel-info"].get() instead of fetchYouTubeChannel(), added response validation, and adjusted status/error logic to match new API contract.
Component Updates
components/ArtistSetting/StandaloneYoutubeComponent/ChannelInfo.tsx, components/YouTube/ChatInputYoutubeButtonPopover/index.tsx
Updated channel data extraction from data?.channels?.[0] to data?.[0] to match new API response shape.
Next.js API Bridge
app/api/[...slug]/route.ts
Added catch-all route handler that proxies all HTTP methods to the Elysia application with force-dynamic caching configuration.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant NextApi as Next.js API<br/>/api/[...slug]
    participant Elysia as Elysia App<br/>Channel Info Route
    participant AuthPlugin as Auth Plugin
    participant ApiKeyLookup as API Key Lookup<br/>(Supabase)
    participant PrivyAuth as Privy Auth<br/>Token Verification
    participant YouTube as YouTube API
    
    Client->>NextApi: GET /api/youtube/channel-info<br/>?artist_account_id=123<br/>Header: x-api-key or Cookie: privy-token
    NextApi->>Elysia: Forward request
    Elysia->>AuthPlugin: Check authentication
    
    alt x-api-key present
        AuthPlugin->>ApiKeyLookup: Lookup account ID
        ApiKeyLookup-->>AuthPlugin: Return account ID
        AuthPlugin->>AuthPlugin: Set user context
    else privy-token present
        AuthPlugin->>PrivyAuth: Verify token
        PrivyAuth-->>AuthPlugin: Return user ID
        AuthPlugin->>AuthPlugin: Set user context
    else No credentials
        AuthPlugin-->>Elysia: Return 401
        Elysia-->>NextApi: 401 Unauthorized
        NextApi-->>Client: 401 Unauthorized
    end
    
    Elysia->>Elysia: Validate artist_account_id
    Elysia->>Elysia: Get stored YouTube tokens
    Elysia->>YouTube: Fetch channel info<br/>with access token
    YouTube-->>Elysia: Channel data
    Elysia-->>NextApi: 200 + channelData
    NextApi-->>Client: 200 + channelData
Loading
sequenceDiagram
    actor React as React Component
    participant Hook as useYoutubeChannel<br/>Hook
    participant Client as Treaty Client<br/>api.youtube["channel-info"]
    participant API as API Server
    
    React->>Hook: Call with artistAccountId
    activate Hook
    Hook->>Hook: Set up useQuery
    Hook->>Client: Call .get() with<br/>artist_account_id query
    Client->>API: GET /api/youtube/channel-info?artist_account_id=...
    API-->>Client: Response with channelData
    Client-->>Hook: queryFn receives response
    Hook->>Hook: Validate response<br/>(check error, data exists)
    Hook-->>React: Return channel array
    deactivate Hook
    React->>React: Extract channel[0]<br/>for rendering
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

The changes introduce a new authentication framework and API layer architecture with multiple interacting components (auth plugins, API routing, token validation). The migration spans hooks, components, and infrastructure files with varying logic density. Authentication logic in particular requires careful scrutiny for security correctness, token handling, and error edge cases.

Suggested reviewers

  • sweetmantech

Poem

✨ From Next routes to Elysia's gleaming gate,
Where tokens dance and auth keys validate,
YouTube channels flow through sanctioned ways,
Clean architecture lights the modern days! 🚀
No more fetch, just treaty's elegant call,
The refactor rises, architected for all. 🎭

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning PR violates SOLID principles: response contract mismatch between route returning array and hook expecting success property, duplicated auth logic (DRY violation), raw credential storage (security risk), weak error schemas, and use of t.Any() bypassing type safety. Wrap array in success object or update consumer; extract auth logic into helpers; replace raw tokens with auth method identifiers; define structured error schemas; replace t.Any() with typed schemas.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/elysia-youtube-poc

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
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
lib/chat/validateHeaders.ts (1)

64-66: ⚠️ Potential issue | 🟡 Minor

accessToken may be undefined in cookie-only authentication scenarios.

Cookie-based authentication (when neither authorization nor x-api-key headers are present) results in undefined accessToken. While validateChatRequest.ts guards against this with explicit validation requiring a non-empty token, the auth.ts plugin in lib/api/elysia/plugins/auth.ts passes validated.accessToken directly without validation. This creates inconsistent handling across code paths. If MCP auth or other consumers use the auth macro directly, they could receive undefined tokens. Verify that all consumers explicitly check for token presence or add validation to the auth plugin.

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

In `@lib/chat/validateHeaders.ts` around lines 64 - 66, The auth plugin is passing
validated.accessToken that can be undefined when auth comes from cookies; update
the auth macro in auth.ts (the code that consumes
validateHeaders.ts/validateChatRequest.ts) to explicitly assert and throw or
return a validation error if validated.accessToken is missing (empty/undefined)
before passing it on, or alternatively add a non-optional fallback/explicit
error inside validateHeaders.ts where accessToken is derived (the const
accessToken = ... line) so that accessToken is never undefined; reference
validated.accessToken and the accessToken creation in validateHeaders.ts and
ensure every consumer (including auth.ts) either checks for non-empty token or
receives a guaranteed non-undefined token.
🧹 Nitpick comments (4)
hooks/useArtistFromRoom.ts (1)

28-28: Nitpick: Boolean(roomId) vs !!roomId — equivalent but verbose.

Both forms are semantically identical. Boolean(roomId) is slightly more explicit/readable, while !!roomId is the more common idiom in TypeScript. Either works fine — just noting the style inconsistency with line 28's !!userData?.id on the same line.

🔧 Optional: Consistent style
-    enabled: Boolean(roomId) && !!userData?.id,
+    enabled: !!roomId && !!userData?.id,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useArtistFromRoom.ts` at line 28, In hooks/useArtistFromRoom.ts update
the enabled flag to use a consistent boolean coercion style: change the
expression currently using Boolean(roomId) in the enabled property (alongside
!!userData?.id) to the shorter idiomatic form !!roomId so both checks use the
same coercion pattern; locate the enabled: Boolean(roomId) && !!userData?.id
line in the useArtistFromRoom hook and replace Boolean(roomId) with !!roomId.
components/VercelChat/chat.tsx (1)

30-39: Consider the coupling between id prop and URL-derived roomId.

The Chat component receives id as a prop and passes it to VercelChatProvider as chatId. Meanwhile, ChatContentMemoized independently derives roomId from useParams() for useArtistFromRoom.

In practice, these should always match since the page component (app/chat/[roomId]/page.tsx) passes the route param as id. However, there's now an implicit assumption that they're identical without explicit coupling.

This is likely fine given the current architecture, but worth noting if the component is ever used in contexts where id prop might differ from the URL param (e.g., embedded chat, modals).

🔧 Optional: Make the relationship explicit via context

If you want to ensure consistency, you could expose chatId from VercelChatProvider context and use it in ChatContentMemoized instead of calling useParams() again:

// In ChatContentMemoized:
const { chatId } = useVercelChatContext(); // Add chatId to context
useArtistFromRoom(chatId);

This would guarantee the artist fetch uses the same ID as the chat provider.

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

In `@components/VercelChat/chat.tsx` around lines 30 - 39, The Chat component
currently passes id to VercelChatProvider as chatId while ChatContentMemoized
independently calls useParams() to derive roomId for useArtistFromRoom, creating
an implicit coupling; update the VercelChatProvider context to include chatId
(expose it via useVercelChatContext) and change ChatContentMemoized to consume
chatId from that context and call useArtistFromRoom(chatId) instead of using
useParams(), ensuring both provider and consumer always use the same chat
identifier (adjust VercelChatProvider, its context type, and the
ChatContentMemoized usage accordingly).
components/YouTube/ChatInputYoutubeButtonPopover/index.tsx (1)

31-32: Consider keyboard accessibility for the mobile toggle.

The mobile popover toggle only responds to onClick. For full accessibility, consider adding onKeyDown to handle Enter/Space activation, and appropriate ARIA attributes (aria-expanded, aria-haspopup).

♿ Proposed accessibility improvement
-        <div onClick={() => setIsOpen(!isOpen)}>
+        <div
+          role="button"
+          tabIndex={0}
+          aria-expanded={isOpen}
+          aria-haspopup="dialog"
+          onClick={() => setIsOpen(!isOpen)}
+          onKeyDown={(e) => {
+            if (e.key === "Enter" || e.key === " ") {
+              e.preventDefault();
+              setIsOpen(!isOpen);
+            }
+          }}
+        >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/YouTube/ChatInputYoutubeButtonPopover/index.tsx` around lines 31 -
32, The mobile popover toggle currently only uses onClick; update the
interactive element that calls setIsOpen(!isOpen) (the div wrapping {children})
to be keyboard-accessible by adding onKeyDown that toggles on Enter/Space, give
it role="button" and tabIndex={0}, and include ARIA attributes
aria-expanded={isOpen} and aria-haspopup="true" so assistive tech can detect
state and purpose; ensure the onKeyDown handler prevents default for Space and
calls setIsOpen(!isOpen) similarly to the onClick handler.
lib/api/elysia/routes/youtube/channelInfo.ts (1)

40-44: t.Any() weakens type safety in the OpenAPI spec.

Using t.Array(t.Any()) for the channels response loses the type information in the generated OpenAPI documentation. Consider defining a proper schema for YouTubeChannelData to provide accurate API documentation and runtime validation.

♻️ Example channel schema
const channelDataSchema = t.Object({
  id: t.String(),
  title: t.String(),
  description: t.String(),
  // ... other required fields from YouTubeChannelData
});

// Then use:
channels: t.Array(channelDataSchema),

As per coding guidelines: "Use strict TypeScript types with zero 'any' types".

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

In `@lib/api/elysia/routes/youtube/channelInfo.ts` around lines 40 - 44, The
response currently uses t.Array(t.Any()) in channelInfo.ts which weakens type
safety; define a strict schema (e.g., channelDataSchema or YouTubeChannelData)
using t.Object with the required fields (id, title, description, etc.) and
replace t.Any() with that schema in the response: change channels:
t.Array(t.Any()) to channels: t.Array(channelDataSchema) and ensure any
runtime/type exports (YouTubeChannelData or channelDataSchema) are imported/used
where the route is declared so OpenAPI generation and validation use the precise
shape.
🤖 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/api/elysia/client.ts`:
- Around line 1-8: The import and client creation are using the legacy
edenTreaty API; replace the import and factory call by importing treaty from
"@elysiajs/eden" and call treaty<ElysiaApi>(...) instead of
edenTreaty<ElysiaApi>(...), keeping the same options object (the eden
constant/name and $fetch credentials setting should remain unchanged).

In `@lib/api/elysia/routes/youtube/channelInfo.ts`:
- Around line 13-18: The handler currently ignores the authenticated user and
only reads query, allowing authorization bypass; update the route handler
signature to destructure auth and query (i.e., async ({ auth, query }) => ...)
and after validateYouTubeTokens succeeds, check that query.artist_account_id ===
auth.accountId and if not throw an HttpError(403, "Unauthorized: cannot access
other accounts"); ensure this check occurs before any token operations
(reference the validateYouTubeTokens call and the anonymous async route handler
in channelInfo.ts).

---

Outside diff comments:
In `@lib/chat/validateHeaders.ts`:
- Around line 64-66: The auth plugin is passing validated.accessToken that can
be undefined when auth comes from cookies; update the auth macro in auth.ts (the
code that consumes validateHeaders.ts/validateChatRequest.ts) to explicitly
assert and throw or return a validation error if validated.accessToken is
missing (empty/undefined) before passing it on, or alternatively add a
non-optional fallback/explicit error inside validateHeaders.ts where accessToken
is derived (the const accessToken = ... line) so that accessToken is never
undefined; reference validated.accessToken and the accessToken creation in
validateHeaders.ts and ensure every consumer (including auth.ts) either checks
for non-empty token or receives a guaranteed non-undefined token.

---

Nitpick comments:
In `@components/VercelChat/chat.tsx`:
- Around line 30-39: The Chat component currently passes id to
VercelChatProvider as chatId while ChatContentMemoized independently calls
useParams() to derive roomId for useArtistFromRoom, creating an implicit
coupling; update the VercelChatProvider context to include chatId (expose it via
useVercelChatContext) and change ChatContentMemoized to consume chatId from that
context and call useArtistFromRoom(chatId) instead of using useParams(),
ensuring both provider and consumer always use the same chat identifier (adjust
VercelChatProvider, its context type, and the ChatContentMemoized usage
accordingly).

In `@components/YouTube/ChatInputYoutubeButtonPopover/index.tsx`:
- Around line 31-32: The mobile popover toggle currently only uses onClick;
update the interactive element that calls setIsOpen(!isOpen) (the div wrapping
{children}) to be keyboard-accessible by adding onKeyDown that toggles on
Enter/Space, give it role="button" and tabIndex={0}, and include ARIA attributes
aria-expanded={isOpen} and aria-haspopup="true" so assistive tech can detect
state and purpose; ensure the onKeyDown handler prevents default for Space and
calls setIsOpen(!isOpen) similarly to the onClick handler.

In `@hooks/useArtistFromRoom.ts`:
- Line 28: In hooks/useArtistFromRoom.ts update the enabled flag to use a
consistent boolean coercion style: change the expression currently using
Boolean(roomId) in the enabled property (alongside !!userData?.id) to the
shorter idiomatic form !!roomId so both checks use the same coercion pattern;
locate the enabled: Boolean(roomId) && !!userData?.id line in the
useArtistFromRoom hook and replace Boolean(roomId) with !!roomId.

In `@lib/api/elysia/routes/youtube/channelInfo.ts`:
- Around line 40-44: The response currently uses t.Array(t.Any()) in
channelInfo.ts which weakens type safety; define a strict schema (e.g.,
channelDataSchema or YouTubeChannelData) using t.Object with the required fields
(id, title, description, etc.) and replace t.Any() with that schema in the
response: change channels: t.Array(t.Any()) to channels:
t.Array(channelDataSchema) and ensure any runtime/type exports
(YouTubeChannelData or channelDataSchema) are imported/used where the route is
declared so OpenAPI generation and validation use the precise shape.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 013d14d4-8485-4456-b18f-f4385cc35bee

📥 Commits

Reviewing files that changed from the base of the PR and between 7c82c73 and efcd9d5.

⛔ Files ignored due to path filters (3)
  • package.json is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
  • types/youtube.ts is excluded by none and included by none
📒 Files selected for processing (19)
  • app/api/openapi/[[...slug]]/route.ts
  • app/api/youtube/channel-info/route.ts
  • components/ArtistSetting/StandaloneYoutubeComponent/ChannelInfo.tsx
  • components/VercelChat/chat.tsx
  • components/YouTube/ChatInputYoutubeButtonPopover/index.tsx
  • hooks/useArtistFromRoom.ts
  • hooks/useVercelChat.ts
  • hooks/useYouTubeLoginSuccess.ts
  • hooks/useYoutubeChannel.tsx
  • hooks/useYoutubeStatus.ts
  • lib/api/elysia/app.ts
  • lib/api/elysia/client.ts
  • lib/api/elysia/errors.ts
  • lib/api/elysia/plugins/auth.ts
  • lib/api/elysia/routes/index.ts
  • lib/api/elysia/routes/youtube/channelInfo.ts
  • lib/api/elysia/routes/youtube/index.ts
  • lib/chat/validateHeaders.ts
  • lib/youtube/fetchYouTubeChannel.ts
💤 Files with no reviewable changes (1)
  • lib/youtube/fetchYouTubeChannel.ts

Comment on lines +1 to +8
import { edenTreaty } from "@elysiajs/eden";
import type { ElysiaApi } from "@/lib/api/elysia/app";

const eden = edenTreaty<ElysiaApi>("", {
$fetch: {
credentials: "include",
},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What is the current API for @elysiajs/eden client? Is edenTreaty deprecated?

💡 Result:

The current API for the @elysiajs/eden client is Eden Treaty (imported as treaty from '@elysiajs/eden'), which is the recommended object-like RPC client for end-to-end type-safe interactions with Elysia servers. It provides tree-like syntax (e.g., app.hi.get), full type safety, auto-completion, error handling with type narrowing, and support for dynamic paths, streaming, WebSockets, and config options like custom fetchers. Example: import { treaty } from '@elysiajs/eden' import type { App } from './server' const app = treaty('http://localhost:3000') const { data, error } = await app.hi.get Alternative: Eden Fetch (edenFetch(url)), a fetch-like API for those preferring standard fetch syntax. edenTreaty (Eden Treaty 1, imported as edenTreaty) is the legacy version. Official docs label it as "Eden Treaty Legacy" and recommend Eden Treaty 2 (treaty) for new projects. It is not formally deprecated but superseded by the improved v2 with better performance, inference, and features (introduced around v1.0.0 in Mar 2024, latest @elysiajs/eden v1.4.8 as of Feb 2026). Continue using legacy only for compatibility; migrate for latest optimizations.

Citations:


Migrate from legacy edenTreaty to the current treaty API.

The edenTreaty import is the legacy Eden Treaty 1 API. Since @elysiajs/eden v1.0.0, the recommended API is treaty, which provides better performance, inference, and features. Update the import and client initialization:

import { treaty } from "@elysiajs/eden";
import type { ElysiaApi } from "@/lib/api/elysia/app";

const eden = treaty<ElysiaApi>("", {
  $fetch: {
    credentials: "include",
  },
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/elysia/client.ts` around lines 1 - 8, The import and client creation
are using the legacy edenTreaty API; replace the import and factory call by
importing treaty from "@elysiajs/eden" and call treaty<ElysiaApi>(...) instead
of edenTreaty<ElysiaApi>(...), keeping the same options object (the eden
constant/name and $fetch credentials setting should remain unchanged).

Comment on lines +13 to +18
async ({ query }) => {
const tokenValidation = await validateYouTubeTokens(query.artist_account_id);

if (!tokenValidation.success || !tokenValidation.tokens) {
throw new HttpError(403, "YouTube authentication required");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if artist_account_id should match the authenticated accountId
rg -n 'artist_account_id|accountId' lib/api/elysia/routes/youtube/channelInfo.ts lib/youtube/token-validator.ts -C2

Repository: recoupable/chat

Length of output: 2016


🏁 Script executed:

# Get the full route handler to see lines around auth: true
cat -n lib/api/elysia/routes/youtube/channelInfo.ts

Repository: recoupable/chat

Length of output: 2226


🏁 Script executed:

# Search for how auth macro is typically used in other routes
rg -A5 'auth:\s*true' lib/api/elysia/routes --type ts --type tsx | head -50

Repository: recoupable/chat

Length of output: 86


🏁 Script executed:

# Check if there's validation that artist_account_id should match authenticated accountId
rg -n 'artist_account_id.*auth|accountId.*artist_account_id' lib --type ts --type tsx

Repository: recoupable/chat

Length of output: 86


🏁 Script executed:

# Find auth macro definition or middleware
find lib -name "*.ts" -o -name "*.tsx" | xargs grep -l "auth.*macro\|createAuthMacro\|authMiddleware" | head -10

Repository: recoupable/chat

Length of output: 89


🏁 Script executed:

# Look at other routes using auth: true to see the pattern
rg -A10 'auth:\s*true' lib/api/elysia/routes -A 5 | head -80

Repository: recoupable/chat

Length of output: 465


🏁 Script executed:

# Check if Elysia auth plugin is defined or used
rg -n "\.plugin.*auth|\.derive.*auth" lib/api/elysia --type ts | head -20

Repository: recoupable/chat

Length of output: 41


🏁 Script executed:

# Look for the route handler parameter type to see what auth provides
rg -n "async.*{.*auth" lib/api/elysia/routes --type ts | head -20

Repository: recoupable/chat

Length of output: 163


🏁 Script executed:

# Read the auth plugin to understand what it provides
cat -n lib/api/elysia/plugins/auth.ts

Repository: recoupable/chat

Length of output: 1682


🏁 Script executed:

# Check how auth is used in other route handlers that destructure it
rg -B2 -A10 'async.*{.*auth.*query' lib/api/elysia/routes --type ts | head -60

Repository: recoupable/chat

Length of output: 170


🏁 Script executed:

# Look for other routes handling artist_account_id to understand the pattern
rg -B5 -A10 'artist_account_id' lib/api/elysia/routes --type ts

Repository: recoupable/chat

Length of output: 2403


🏁 Script executed:

# Check if there are other routes that properly use auth.accountId
rg -B2 -A8 'async.*{.*auth' lib/api/elysia/routes --type ts | head -80

Repository: recoupable/chat

Length of output: 163


🏁 Script executed:

# Find routes that use auth object in destructuring
rg 'async.*\{' lib/api/elysia/routes --type ts -A 1 | grep -E '(auth|query|body)' | head -30

Repository: recoupable/chat

Length of output: 255


🏁 Script executed:

# Check how other routes handle authenticated operations
find lib/api/elysia/routes -name "*.ts" -exec grep -l "auth:" {} \; | xargs -I {} sh -c 'echo "=== {} ===" && head -20 {}'

Repository: recoupable/chat

Length of output: 824


Fix authorization bypass: destructure auth and validate artist_account_id matches authenticated user.

The route declares auth: true which validates the request and provides auth.accountId, but the handler ignores it:

async ({ query }) => {

Should be:

async ({ auth, query }) => {

This creates a security vulnerability: any authenticated user can request YouTube tokens for any artist_account_id by manipulating the query parameter. The authenticated user's identity is never validated against the requested account.

After auth validation, verify the requested account matches the authenticated user:

if (query.artist_account_id !== auth.accountId) {
  throw new HttpError(403, "Unauthorized: cannot access other accounts");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/elysia/routes/youtube/channelInfo.ts` around lines 13 - 18, The
handler currently ignores the authenticated user and only reads query, allowing
authorization bypass; update the route handler signature to destructure auth and
query (i.e., async ({ auth, query }) => ...) and after validateYouTubeTokens
succeeds, check that query.artist_account_id === auth.accountId and if not throw
an HttpError(403, "Unauthorized: cannot access other accounts"); ensure this
check occurs before any token operations (reference the validateYouTubeTokens
call and the anonymous async route handler in channelInfo.ts).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: efcd9d5540

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +20 to +23
const hasAuthCredential =
request.headers.has("authorization") ||
request.headers.has("x-api-key") ||
request.headers.has("cookie");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict cookie-based auth detection before upstream lookup

Treating any cookie header as an auth credential causes requests with unrelated browser cookies (but no authorization/x-api-key) to call /api/accounts/id anyway, turning the previous fast no-auth path into an upstream-dependent one. That introduces unnecessary latency/load and can surface 500s when the account lookup service is unavailable or returns an unexpected body, whereas the old behavior returned {} immediately and let callers handle unauthenticated requests locally.

Useful? React with 👍 / 👎.

Comment on lines +20 to +23
youtubeStatus?.status === "invalid" ||
isLoading ||
isChannelInfoLoading ||
!channel
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Hide YouTube popover when status is error

This guard only blocks on status === "invalid", but after moving channel-info to Elysia the disconnected/expired-auth path now commonly returns HTTP errors, which useYoutubeStatus maps to "error". Because React Query can retain prior successful channelInfo on refetch failure, !channel may still be false and the popover remains visible with stale channel data even though YouTube auth has failed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
hooks/useYouTubeLoginSuccess.ts (2)

54-72: The data.success check is redundant.

Looking at the endpoint definition in lib/api/elysia/routes/youtube/channelInfo.ts (lines 41-44), the 200 response schema is { success: t.Literal(true), channels: t.Array(t.Any()) }. If unwrapResponse succeeds (no error, data not null), data.success will always be true. The conditional adds no value here.

♻️ Simplify by removing redundant check
        .then((youtubeChannel) => {
          const data = unwrapResponse(
            youtubeChannel,
            "Failed to fetch YouTube channel information",
          );

-          if (data.success) {
-            const successMessage = {
-              id: generateUUID(),
-              role: "user" as const,
-              parts: [
-                {
-                  type: "text",
-                  text: "Great! I've successfully connected my YouTube account. Please continue with what you were helping me with.",
-                },
-              ],
-            } as UIMessage;
-
-            append(successMessage);
-          }
+          const successMessage: UIMessage = {
+            id: generateUUID(),
+            role: "user" as const,
+            parts: [
+              {
+                type: "text",
+                text: "Great! I've successfully connected my YouTube account. Please continue with what you were helping me with.",
+              },
+            ],
+          };
+
+          append(successMessage);
        })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useYouTubeLoginSuccess.ts` around lines 54 - 72, Remove the redundant
data.success conditional after unwrapResponse: since
unwrapResponse(youtubeChannel, ...) guarantees a successful 200 response shape
(success === true), always construct the successMessage (using generateUUID()
and UIMessage shape) and call append(successMessage) directly; locate this logic
around the unwrapResponse call that consumes youtubeChannel and simplify by
eliminating the if (data.success) branch so the message creation and append
happen unconditionally after a successful unwrapResponse.

74-76: Silent error swallowing may hinder debugging.

While the comment explains the intent, completely silencing errors can make production issues harder to diagnose. Consider at minimum a console.debug or structured logging for observability.

🔍 Optional: Add debug logging
        .catch(() => {
-          // Ignore auth/check errors here; normal flow continues.
+          // Ignore auth/check errors here; normal flow continues.
+          // Uncomment for debugging: console.debug("YouTube channel check failed silently");
        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useYouTubeLoginSuccess.ts` around lines 74 - 76, The current promise
catch block in hooks/useYouTubeLoginSuccess.ts (the .catch(() => { ... })
attached to the login/check flow) silently swallows errors; change the catch to
accept the error (e.g., .catch((err) => { ... })) and emit a low-level
debug/trace log with the error (console.debug or the app's logger) while keeping
the original behavior (no user-facing disruption); locate the catch in the
useYouTubeLoginSuccess flow and add the error parameter and a single-line
structured debug log message including the error and context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@hooks/useYouTubeLoginSuccess.ts`:
- Around line 54-72: Remove the redundant data.success conditional after
unwrapResponse: since unwrapResponse(youtubeChannel, ...) guarantees a
successful 200 response shape (success === true), always construct the
successMessage (using generateUUID() and UIMessage shape) and call
append(successMessage) directly; locate this logic around the unwrapResponse
call that consumes youtubeChannel and simplify by eliminating the if
(data.success) branch so the message creation and append happen unconditionally
after a successful unwrapResponse.
- Around line 74-76: The current promise catch block in
hooks/useYouTubeLoginSuccess.ts (the .catch(() => { ... }) attached to the
login/check flow) silently swallows errors; change the catch to accept the error
(e.g., .catch((err) => { ... })) and emit a low-level debug/trace log with the
error (console.debug or the app's logger) while keeping the original behavior
(no user-facing disruption); locate the catch in the useYouTubeLoginSuccess flow
and add the error parameter and a single-line structured debug log message
including the error and context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4becc339-10b8-401f-b3b5-59cfd9bca214

📥 Commits

Reviewing files that changed from the base of the PR and between efcd9d5 and fe53409.

⛔ Files ignored due to path filters (1)
  • types/youtube.ts is excluded by none and included by none
📒 Files selected for processing (6)
  • app/api/[...slug]/route.ts
  • app/api/youtube/channel-info/route.ts
  • hooks/useYouTubeLoginSuccess.ts
  • hooks/useYoutubeChannel.tsx
  • lib/api/elysia/client.ts
  • lib/api/elysia/unwrapResponse.ts
💤 Files with no reviewable changes (1)
  • app/api/youtube/channel-info/route.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/api/elysia/client.ts
  • hooks/useYoutubeChannel.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
lib/api/elysia/routes/youtube/channelInfo.ts (1)

12-15: ⚠️ Potential issue | 🔴 Critical

Authorization bypass: handler ignores authenticated user identity.

The handler destructures { query, status } but not auth, so any authenticated user can request data for any artist_account_id. This was flagged in a previous review. However, the root cause is that authMacro in lib/api/elysia/plugins/auth.ts doesn't expose accountId to handlers.

Once the auth macro is fixed to expose auth.accountId, update this handler:

-  async ({ query, status }) => {
+  async ({ query, status, auth }) => {
     if (!query.artist_account_id) {
       return status(400, { error: "Missing artist_account_id parameter" });
     }
+    
+    if (query.artist_account_id !== auth.accountId) {
+      return status(403, { error: "Unauthorized: cannot access other accounts" });
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/elysia/routes/youtube/channelInfo.ts` around lines 12 - 15, The route
handler currently destructures only { query, status } and therefore allows any
caller to request data for any artist_account_id; after fixing authMacro to
expose auth.accountId, update this handler (the async route function in
channelInfo.ts) to also destructure auth (e.g. async ({ query, status, auth }))
and enforce authorization by verifying auth.accountId exists and equals
query.artist_account_id (return status(403, { error: "Forbidden" }) on
mismatch), and keep the existing 400 check for missing artist_account_id; this
ensures only the authenticated account can request its own channel info.
🧹 Nitpick comments (3)
lib/api/elysia/routes/youtube/channelInfo.ts (2)

51-54: Consider adding proper typing for the channels array.

Using t.Array(t.Any()) loses type safety and results in a less useful OpenAPI schema. Consider defining a proper channel schema based on YouTubeChannelData type.

✨ Define typed channel schema
const youtubeChannelSchema = t.Object({
  id: t.String(),
  title: t.String(),
  description: t.Optional(t.String()),
  thumbnails: t.Optional(t.Any()), // or define thumbnail schema
  // Add other fields as needed
});

// Then in response:
  success: t.Literal(true),
  channels: t.Array(youtubeChannelSchema),
}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/elysia/routes/youtube/channelInfo.ts` around lines 51 - 54, The
response schema currently uses t.Array(t.Any()) for channels which loses type
safety; define a typed schema (e.g., youtubeChannelSchema) that maps the fields
of YouTubeChannelData (id, title, description, thumbnails, etc.) and replace
t.Array(t.Any()) with t.Array(youtubeChannelSchema) in the response object so
the channels property is strongly typed and reflected in OpenAPI; ensure
optional fields use t.Optional(...) and add/adjust thumbnail or nested schemas
as needed to match YouTubeChannelData.

47-49: Schema declares artist_account_id as optional but handler requires it.

The query schema marks artist_account_id as t.Optional(t.String()), but line 13 immediately returns 400 if it's missing. This inconsistency makes the OpenAPI spec misleading.

♻️ Make schema match handler requirements
     query: t.Object({
-      artist_account_id: t.Optional(t.String()),
+      artist_account_id: t.String(),
     }),

With this change, Elysia validates the parameter before the handler runs, so the manual check on line 13-15 becomes unnecessary:

-    if (!query.artist_account_id) {
-      return status(400, { error: "Missing artist_account_id parameter" });
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/elysia/routes/youtube/channelInfo.ts` around lines 47 - 49, The query
schema currently marks artist_account_id as optional (query: t.Object({
artist_account_id: t.Optional(t.String()) })), but the handler still requires it
and returns 400 when missing; update the schema to make artist_account_id
required (t.String()) so Elysia validates it before the handler runs, and then
remove the redundant manual presence check and early 400 return in the handler
that references artist_account_id.
lib/api/elysia/plugins/auth.ts (1)

110-111: Use the recommended authenticated user-fetching method from Privy after token verification.

In an authentication context (after verifyAuthToken()), use privy.users.get({ id_token: token }) instead of _get(). The _get() method is for unauthenticated user lookups and is rate-limited; privy.users.get() is the recommended pattern for authenticated operations and aligns with official Privy best practices for this use case.

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

In `@lib/api/elysia/plugins/auth.ts` around lines 110 - 111, The code currently
calls privyClient.users()._get(verified.user_id) after verifyAuthToken, which
uses an unauthenticated, rate-limited lookup; replace this with the
authenticated pattern privyClient.users().get({ id_token: authToken }) (or
equivalent privy.users.get call) using the same authToken returned/used in
verifyAuthToken so the user is fetched via the recommended authenticated API;
update any variable references from the old _get result to the new get result
and remove the use of privyClient.users()._get in function(s) where
verifyAuthToken and authToken are used.
🤖 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/api/elysia/plugins/auth.ts`:
- Around line 132-167: The auth macro's beforeHandle currently authenticates and
obtains accountId via getAccountIdFromApiKey and getAccountIdFromPrivyAuthToken
but never stores it on the request context, so downstream handlers can't access
the authenticated user; update the authMacro (beforeHandle) to attach the
resolved accountId into the request context using Elysia's supported mechanism
(e.g., derive or state) so handlers can read auth.accountId, ensuring you set
the value before returning and still return 401 on missing/invalid credentials
and propagate errors as currently done.
- Around line 118-129: Summary: The email lookup uses .maybeSingle() on the
account_emails table which can return an arbitrary account_id when duplicate
emails exist, creating an account isolation risk. Fix: replace the
.maybeSingle() call with .single() in the email lookup block (the
supabase.from("account_emails").select(...).eq("email", email) call), and
explicitly handle the duplicate-row error by throwing a clear error instead of
returning a value; alternatively enforce a UNIQUE constraint on
account_emails.email at the DB level or add a deterministic filter (e.g.,
primary_email flag) to disambiguate; follow the same pattern used by
getAccountByEmail.ts and getUserInfo.ts for consistent duplicate detection and
error handling.

---

Duplicate comments:
In `@lib/api/elysia/routes/youtube/channelInfo.ts`:
- Around line 12-15: The route handler currently destructures only { query,
status } and therefore allows any caller to request data for any
artist_account_id; after fixing authMacro to expose auth.accountId, update this
handler (the async route function in channelInfo.ts) to also destructure auth
(e.g. async ({ query, status, auth })) and enforce authorization by verifying
auth.accountId exists and equals query.artist_account_id (return status(403, {
error: "Forbidden" }) on mismatch), and keep the existing 400 check for missing
artist_account_id; this ensures only the authenticated account can request its
own channel info.

---

Nitpick comments:
In `@lib/api/elysia/plugins/auth.ts`:
- Around line 110-111: The code currently calls
privyClient.users()._get(verified.user_id) after verifyAuthToken, which uses an
unauthenticated, rate-limited lookup; replace this with the authenticated
pattern privyClient.users().get({ id_token: authToken }) (or equivalent
privy.users.get call) using the same authToken returned/used in verifyAuthToken
so the user is fetched via the recommended authenticated API; update any
variable references from the old _get result to the new get result and remove
the use of privyClient.users()._get in function(s) where verifyAuthToken and
authToken are used.

In `@lib/api/elysia/routes/youtube/channelInfo.ts`:
- Around line 51-54: The response schema currently uses t.Array(t.Any()) for
channels which loses type safety; define a typed schema (e.g.,
youtubeChannelSchema) that maps the fields of YouTubeChannelData (id, title,
description, thumbnails, etc.) and replace t.Array(t.Any()) with
t.Array(youtubeChannelSchema) in the response object so the channels property is
strongly typed and reflected in OpenAPI; ensure optional fields use
t.Optional(...) and add/adjust thumbnail or nested schemas as needed to match
YouTubeChannelData.
- Around line 47-49: The query schema currently marks artist_account_id as
optional (query: t.Object({ artist_account_id: t.Optional(t.String()) })), but
the handler still requires it and returns 400 when missing; update the schema to
make artist_account_id required (t.String()) so Elysia validates it before the
handler runs, and then remove the redundant manual presence check and early 400
return in the handler that references artist_account_id.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 825c080a-9a4e-4597-ba63-1d9a58698432

📥 Commits

Reviewing files that changed from the base of the PR and between fe53409 and 4b7129d.

⛔ Files ignored due to path filters (2)
  • package.json is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
📒 Files selected for processing (3)
  • lib/api/elysia/app.ts
  • lib/api/elysia/plugins/auth.ts
  • lib/api/elysia/routes/youtube/channelInfo.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/api/elysia/app.ts

Comment on lines +132 to +167
export const authMacro = new Elysia({ name: "auth-macro" }).macro({
auth(enabled: boolean) {
if (!enabled) return;

return {
async beforeHandle({ request, status }) {
const apiKey = request.headers.get("x-api-key");

try {
if (apiKey) {
const accountId = await getAccountIdFromApiKey(apiKey);
if (!accountId) {
return status(401, { error: "Unauthorized" });
}
return;
}

const privyAuthToken = getPrivyAuthTokenFromRequest(request);
if (!privyAuthToken) {
return status(401, { error: "Authentication required" });
}

const accountId = await getAccountIdFromPrivyAuthToken(privyAuthToken);
if (!accountId) {
return status(401, { error: "Unauthorized" });
}
} catch (error) {
return status(401, {
error: "Authentication failed",
details: error instanceof Error ? error.message : "Unknown authentication error",
});
}
},
};
},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Auth macro validates credentials but doesn't expose accountId to route handlers.

The macro successfully authenticates requests and retrieves accountId (lines 142, 154), but this value is never stored in the request context. Route handlers that set auth: true cannot access the authenticated user's identity, making authorization checks impossible.

Routes like youtubeChannelInfoRoute need auth.accountId to verify the user is authorized to access the requested artist_account_id.

🔧 Proposed fix to expose accountId via derive
 export const authMacro = new Elysia({ name: "auth-macro" }).macro({
   auth(enabled: boolean) {
     if (!enabled) return;

     return {
-      async beforeHandle({ request, status }) {
+      async beforeHandle({ request, status, store }) {
         const apiKey = request.headers.get("x-api-key");

         try {
           if (apiKey) {
             const accountId = await getAccountIdFromApiKey(apiKey);
             if (!accountId) {
               return status(401, { error: "Unauthorized" });
             }
+            store.auth = { accountId };
             return;
           }

           const privyAuthToken = getPrivyAuthTokenFromRequest(request);
           if (!privyAuthToken) {
             return status(401, { error: "Authentication required" });
           }

           const accountId = await getAccountIdFromPrivyAuthToken(privyAuthToken);
           if (!accountId) {
             return status(401, { error: "Unauthorized" });
           }
+          store.auth = { accountId };
         } catch (error) {
           return status(401, {
             error: "Authentication failed",
             details: error instanceof Error ? error.message : "Unknown authentication error",
           });
         }
       },
     };
   },
 });

Note: The exact mechanism depends on Elysia's macro API. Using derive or state might be more appropriate than store. Verify against Elysia 1.4.28 documentation.

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

In `@lib/api/elysia/plugins/auth.ts` around lines 132 - 167, The auth macro's
beforeHandle currently authenticates and obtains accountId via
getAccountIdFromApiKey and getAccountIdFromPrivyAuthToken but never stores it on
the request context, so downstream handlers can't access the authenticated user;
update the authMacro (beforeHandle) to attach the resolved accountId into the
request context using Elysia's supported mechanism (e.g., derive or state) so
handlers can read auth.accountId, ensuring you set the value before returning
and still return 401 on missing/invalid credentials and propagate errors as
currently done.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
lib/api/elysia/routes/youtube/channelInfo.ts (1)

9-19: ⚠️ Potential issue | 🔴 Critical

Critical: Authorization bypass - authenticated user can access any artist's YouTube data.

The route authenticates users via auth: true but never verifies that the authenticated user is authorized to access the requested artist_account_id. Any authenticated user can fetch YouTube channel info for any artist account by manipulating the query parameter.

The handler should destructure user from the context and verify authorization:

🔐 Proposed fix to add authorization check
-  async ({ query, status }) => {
+  async ({ query, status, user }) => {
     if (!query.artist_account_id) {
       return status(400, "Missing artist_account_id parameter");
     }

+    if (query.artist_account_id !== user.userId) {
+      return status(403, "Not authorized to access this artist's YouTube data");
+    }
+
     try {
       const tokenValidation = await validateYouTubeTokens(query.artist_account_id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/elysia/routes/youtube/channelInfo.ts` around lines 9 - 19, The route
handler currently only accepts ({ query, status }) and calls
validateYouTubeTokens without checking that the authenticated user is allowed to
access the requested artist_account_id, allowing authorization bypass; update
the handler to destructure the authenticated user from the context (e.g., async
({ query, status, user })) and verify that user.id or user.artist_account_id
(depending on your auth shape) matches query.artist_account_id (or that the user
has admin/scoped access) before calling validateYouTubeTokens; if the check
fails return status(403, "Forbidden") to prevent cross-artist access and keep
validateYouTubeTokens usage (and existing responses) otherwise.
lib/api/elysia/plugins/auth.ts (1)

40-41: ⚠️ Potential issue | 🟡 Minor

Multiple accounts with same email could cause non-deterministic account resolution.

When getAccountDetailsByEmails returns multiple rows (the schema shows isOneToOne: false), the code takes accountDetails[0] arbitrarily. If duplicate emails exist across accounts, authentication could resolve to an unintended account.

Consider adding explicit handling:

🛡️ Proposed defensive check
         const accountDetails = email ? await getAccountDetailsByEmails([email]) : [];
+        if (accountDetails.length > 1) {
+          console.warn(`Multiple accounts found for email, using first match`);
+        }
         const accountId = accountDetails[0]?.account_id ?? null;

Alternatively, ensure at the database level that email has a unique constraint, or use a deterministic selection criterion (e.g., most recently updated).

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

In `@lib/api/elysia/plugins/auth.ts` around lines 40 - 41, The current logic reads
accountDetails = await getAccountDetailsByEmails([email]) and then picks
accountDetails[0] arbitrarily, which is non-deterministic if multiple rows
exist; update the auth flow to detect when accountDetails.length > 1 and handle
it explicitly (e.g., log a warning/error and reject authentication or choose a
deterministic row using a clear criterion such as most recently updated), or
enforce uniqueness at the DB layer; specifically modify the block that computes
accountId (referencing getAccountDetailsByEmails, accountDetails, accountId, and
email) to perform the duplicate-check and either return a deterministic
accountId or fail with a clear error.
🧹 Nitpick comments (2)
lib/api/elysia/routes/youtube/channelInfo.ts (2)

46-47: Response type t.Array(t.Any()) is overly permissive.

Using t.Any() defeats the purpose of typed responses and OpenAPI documentation. Consider defining a proper schema based on YouTubeChannelData:

const youtubeChannelSchema = t.Object({
  title: t.String(),
  description: t.Optional(t.String()),
  thumbnails: t.Optional(t.Object({
    default: t.Optional(t.Object({ url: t.String() })),
  })),
  statistics: t.Optional(t.Object({
    subscriberCount: t.Optional(t.String()),
  })),
  // ... other fields
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/elysia/routes/youtube/channelInfo.ts` around lines 46 - 47, The route
currently returns an overly permissive response type (response: { 200:
t.Array(t.Any()) }); define a proper typed schema (e.g., youtubeChannelSchema)
that mirrors YouTubeChannelData (fields like title, description?,
thumbnails?.default?.url?, statistics?.subscriberCount?, etc.), export or inline
that schema, and replace t.Array(t.Any()) with t.Array(youtubeChannelSchema) in
the route response so the handler (channelInfo route) and OpenAPI docs use the
specific type.

43-45: Query parameter declared as Optional but handler requires it.

The schema declares artist_account_id as t.Optional(t.String()), but line 10-12 immediately returns 400 if it's missing. Consider making it required in the schema for clearer API documentation:

♻️ Proposed schema fix
     query: t.Object({
-      artist_account_id: t.Optional(t.String()),
+      artist_account_id: t.String(),
     }),

This will also provide automatic 400 validation from Elysia, removing the need for the manual check on lines 10-12.

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

In `@lib/api/elysia/routes/youtube/channelInfo.ts` around lines 43 - 45, The query
schema currently marks artist_account_id as optional (query: t.Object({
artist_account_id: t.Optional(t.String()) })) but the route handler still
manually rejects requests when artist_account_id is missing (the manual 400
check around lines 10-12); update the schema to make artist_account_id required
(remove Optional so it is t.String()) so Elysia will validate and return 400
automatically, and then remove the manual presence check in the handler to avoid
redundant validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@lib/api/elysia/plugins/auth.ts`:
- Around line 40-41: The current logic reads accountDetails = await
getAccountDetailsByEmails([email]) and then picks accountDetails[0] arbitrarily,
which is non-deterministic if multiple rows exist; update the auth flow to
detect when accountDetails.length > 1 and handle it explicitly (e.g., log a
warning/error and reject authentication or choose a deterministic row using a
clear criterion such as most recently updated), or enforce uniqueness at the DB
layer; specifically modify the block that computes accountId (referencing
getAccountDetailsByEmails, accountDetails, accountId, and email) to perform the
duplicate-check and either return a deterministic accountId or fail with a clear
error.

In `@lib/api/elysia/routes/youtube/channelInfo.ts`:
- Around line 9-19: The route handler currently only accepts ({ query, status })
and calls validateYouTubeTokens without checking that the authenticated user is
allowed to access the requested artist_account_id, allowing authorization
bypass; update the handler to destructure the authenticated user from the
context (e.g., async ({ query, status, user })) and verify that user.id or
user.artist_account_id (depending on your auth shape) matches
query.artist_account_id (or that the user has admin/scoped access) before
calling validateYouTubeTokens; if the check fails return status(403,
"Forbidden") to prevent cross-artist access and keep validateYouTubeTokens usage
(and existing responses) otherwise.

---

Nitpick comments:
In `@lib/api/elysia/routes/youtube/channelInfo.ts`:
- Around line 46-47: The route currently returns an overly permissive response
type (response: { 200: t.Array(t.Any()) }); define a proper typed schema (e.g.,
youtubeChannelSchema) that mirrors YouTubeChannelData (fields like title,
description?, thumbnails?.default?.url?, statistics?.subscriberCount?, etc.),
export or inline that schema, and replace t.Array(t.Any()) with
t.Array(youtubeChannelSchema) in the route response so the handler (channelInfo
route) and OpenAPI docs use the specific type.
- Around line 43-45: The query schema currently marks artist_account_id as
optional (query: t.Object({ artist_account_id: t.Optional(t.String()) })) but
the route handler still manually rejects requests when artist_account_id is
missing (the manual 400 check around lines 10-12); update the schema to make
artist_account_id required (remove Optional so it is t.String()) so Elysia will
validate and return 400 automatically, and then remove the manual presence check
in the handler to avoid redundant validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 447e8d39-842d-4f8d-ac37-c4f4a2eb1653

📥 Commits

Reviewing files that changed from the base of the PR and between 4b7129d and 1507a6a.

📒 Files selected for processing (8)
  • components/ArtistSetting/StandaloneYoutubeComponent/ChannelInfo.tsx
  • components/YouTube/ChatInputYoutubeButtonPopover/index.tsx
  • hooks/useYouTubeLoginSuccess.ts
  • hooks/useYoutubeChannel.tsx
  • lib/api/elysia/app.ts
  • lib/api/elysia/plugins/auth.ts
  • lib/api/elysia/routes/youtube/channelInfo.ts
  • lib/supabase/account_api_keys/getAccountIdFromApiKey.ts
✅ Files skipped from review due to trivial changes (1)
  • lib/api/elysia/app.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • hooks/useYoutubeChannel.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
lib/api/elysia/routes/youtube/channelInfo.ts (1)

9-12: ⚠️ Potential issue | 🔴 Critical

Enforce account-level authorization before token validation.

The route authenticates the caller (auth: true) but never checks that query.artist_account_id belongs to the authenticated user. That allows authenticated access attempts against other accounts.

🔧 Suggested fix
-  async ({ query, status }) => {
+  async ({ query, status, user }) => {
     try {
+      if (!user || user.userId !== query.artist_account_id) {
+        return status(403, "Unauthorized");
+      }
+
       const tokenValidation = await validateYouTubeTokens(query.artist_account_id);

As per coding guidelines, "Implement built-in security practices for authentication and data handling."

Also applies to: 38-41

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

In `@lib/api/elysia/routes/youtube/channelInfo.ts` around lines 9 - 12, The route
handler calls validateYouTubeTokens(query.artist_account_id) without verifying
the caller owns that artist account; before calling validateYouTubeTokens,
enforce account-level authorization by comparing the authenticated user's id
(from the handler context/status or auth payload available as status, req, or
similar) against query.artist_account_id and return a 403/unauthorized response
if they differ; update the async handler to perform this ownership check (using
the same status/auth object available in the function) for both the initial call
and the repeated check around lines 38-41 to prevent cross-account access.
🤖 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/useYoutubeChannel.tsx`:
- Around line 12-17: The hook useYoutubeChannel currently returns raw channel
data but consumers (notably useYoutubeStatus.ts) expect a response shape with a
success flag; update useYoutubeChannel to always return a unified shape like {
success: boolean, data?: ChannelType, error?: Error } instead of throwing raw
errors — on success return { success: true, data: response.data } and on failure
return { success: false, error: capturedError } (or keep throwing but then
update all consumers such as useYoutubeStatus to handle thrown errors instead);
ensure the function name useYoutubeChannel and its callers check
channelResponse.success and channelResponse.data accordingly so the contract is
consistent across the codebase.

In `@lib/api/elysia/plugins/auth.ts`:
- Around line 14-17: The code in the user construction (user = { userId: await
getAccountIdFromApiKey(apiKey), identifier: apiKey }) stores the raw API
key/token in the request context; remove the raw secret from the context and
replace it with a non-secret identifier such as authMethod and/or a derived
non-sensitive ID (e.g., tokenId, the account ID, or a hashed/truncated
fingerprint) instead; update the same pattern used around lines 25-28 so
functions like getAccountIdFromApiKey are used to populate userId while
identifier no longer holds the raw apiKey/Privy token.

In `@lib/auth/getAccountIdFromPrivyToken.ts`:
- Line 23: Replace the internal underscore method call
privyClient.users()._get(verified.user_id) with the documented public API: call
privyClient.users.get(...) using an id_token or the equivalent authenticated
parameter (or perform a GET /v1/users/{user_id} via the REST client) to fetch
the user; update getAccountIdFromPrivyToken to construct/pass the id_token or
authenticated context and use privyClient.users.get to retrieve the user record
for verified.user_id so you avoid relying on the underscore-prefixed internal
method.
- Around line 30-35: The code currently picks accountDetails[0]?.account_id
which can select an arbitrary row when multiple accounts share the same email;
update getAccountIdFromPrivyToken so it deterministically resolves the right
account by scanning the accountDetails array returned from
getAccountDetailsByEmails and selecting the single record that matches the Privy
user's identifier or primary/verified account metadata (e.g., match on privy
user id / provider_user_id or an is_primary/is_verified flag), assign its
account_id to accountId, and if no unique match is found throw an error instead
of using the first element; replace the current accountDetails[0]?.account_id
usage with this deterministic selection logic.

---

Duplicate comments:
In `@lib/api/elysia/routes/youtube/channelInfo.ts`:
- Around line 9-12: The route handler calls
validateYouTubeTokens(query.artist_account_id) without verifying the caller owns
that artist account; before calling validateYouTubeTokens, enforce account-level
authorization by comparing the authenticated user's id (from the handler
context/status or auth payload available as status, req, or similar) against
query.artist_account_id and return a 403/unauthorized response if they differ;
update the async handler to perform this ownership check (using the same
status/auth object available in the function) for both the initial call and the
repeated check around lines 38-41 to prevent cross-account access.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1dbef680-78f4-488c-a081-85058cc0a441

📥 Commits

Reviewing files that changed from the base of the PR and between 1507a6a and 8a2952f.

⛔ Files ignored due to path filters (3)
  • package.json is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
  • types/youtube.ts is excluded by none and included by none
📒 Files selected for processing (16)
  • app/api/[...slug]/route.ts
  • app/api/youtube/channel-info/route.ts
  • components/ArtistSetting/StandaloneYoutubeComponent/ChannelInfo.tsx
  • components/YouTube/ChatInputYoutubeButtonPopover/index.tsx
  • hooks/useYouTubeLoginSuccess.ts
  • hooks/useYoutubeChannel.tsx
  • hooks/useYoutubeStatus.ts
  • lib/api/elysia/app.ts
  • lib/api/elysia/client.ts
  • lib/api/elysia/plugins/auth.ts
  • lib/api/elysia/routes/index.ts
  • lib/api/elysia/routes/youtube/channelInfo.ts
  • lib/api/elysia/routes/youtube/index.ts
  • lib/auth/getAccountIdFromPrivyToken.ts
  • lib/supabase/account_api_keys/getAccountIdFromApiKey.ts
  • lib/youtube/fetchYouTubeChannel.ts
💤 Files with no reviewable changes (2)
  • lib/youtube/fetchYouTubeChannel.ts
  • app/api/youtube/channel-info/route.ts
✅ Files skipped from review due to trivial changes (7)
  • lib/api/elysia/client.ts
  • lib/api/elysia/routes/youtube/index.ts
  • components/ArtistSetting/StandaloneYoutubeComponent/ChannelInfo.tsx
  • lib/api/elysia/routes/index.ts
  • hooks/useYouTubeLoginSuccess.ts
  • lib/api/elysia/app.ts
  • app/api/[...slug]/route.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • components/YouTube/ChatInputYoutubeButtonPopover/index.tsx
  • hooks/useYoutubeStatus.ts
  • lib/supabase/account_api_keys/getAccountIdFromApiKey.ts

Comment on lines +12 to +17
if (response.error || !response.data) {
throw new Error("Failed to fetch YouTube channel information");
}

return response.data;
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unify the hook response contract across consumers.

This hook now returns raw channel data, but hooks/useYoutubeStatus.ts still reads channelResponse?.success. That mismatch will cause incorrect status derivation. Please normalize the hook return shape (or update all consumers to one agreed shape).

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

In `@hooks/useYoutubeChannel.tsx` around lines 12 - 17, The hook useYoutubeChannel
currently returns raw channel data but consumers (notably useYoutubeStatus.ts)
expect a response shape with a success flag; update useYoutubeChannel to always
return a unified shape like { success: boolean, data?: ChannelType, error?:
Error } instead of throwing raw errors — on success return { success: true,
data: response.data } and on failure return { success: false, error:
capturedError } (or keep throwing but then update all consumers such as
useYoutubeStatus to handle thrown errors instead); ensure the function name
useYoutubeChannel and its callers check channelResponse.success and
channelResponse.data accordingly so the contract is consistent across the
codebase.

Comment on lines +14 to +17
user = {
userId: await getAccountIdFromApiKey(apiKey),
identifier: apiKey,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not store raw API keys/tokens in request context.

identifier currently contains the raw API key or Privy token. Keep only non-secret identity fields (e.g., userId, authMethod) to reduce credential exposure risk.

🔧 Suggested fix
-    let user: { userId: string; identifier: string } | null = null;
+    let user: { userId: string; authMethod: "apiKey" | "privy" } | null = null;
@@
         user = {
           userId: await getAccountIdFromApiKey(apiKey),
-          identifier: apiKey,
+          authMethod: "apiKey",
         };
@@
           user = {
             userId: accountId,
-            identifier: privyToken,
+            authMethod: "privy",
           };

As per coding guidelines, "Implement built-in security practices for authentication and data handling."

Also applies to: 25-28

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

In `@lib/api/elysia/plugins/auth.ts` around lines 14 - 17, The code in the user
construction (user = { userId: await getAccountIdFromApiKey(apiKey), identifier:
apiKey }) stores the raw API key/token in the request context; remove the raw
secret from the context and replace it with a non-secret identifier such as
authMethod and/or a derived non-sensitive ID (e.g., tokenId, the account ID, or
a hashed/truncated fingerprint) instead; update the same pattern used around
lines 25-28 so functions like getAccountIdFromApiKey are used to populate userId
while identifier no longer holds the raw apiKey/Privy token.

}

const verified = await privyClient.utils().auth().verifyAccessToken(privyToken);
const privyUser = await privyClient.users()._get(verified.user_id);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In @privy-io/nodev0.12.0, isprivyClient.users()._get(userId) a public supported API? What is the documented public method to fetch a user by id?

💡 Result:

No, privyClient.users._get(userId) is not a public supported API in the conventional sense. The underscore prefix indicates it is likely an internal or low-level method. The documented public method to fetch a user by ID is privyClient.users.get({ id_token: '...' }) for authenticated users via identity token (recommended), or the REST API endpoint GET /v1/users/{user_id}. Multiple docs pages explicitly show and describe users._get(userId) as the way to get a user by Privy ID for unauthenticated users, but underscore naming suggests it's not "public" in the public API surface. For v0.12.0 specifically (released Mar 30, 2026), no changelog mentions changes to this method, so it remains as documented.

Citations:


Use documented public API instead of underscore-prefixed method.

While users()._get(userId) is documented in Privy's node SDK, the underscore prefix indicates a lower-level API surface not intended for primary use. The documented public alternatives are users.get() with an id_token for authenticated access, or the REST API endpoint GET /v1/users/{user_id}. Using the public API surface reduces coupling to internal implementation details and improves maintainability.

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

In `@lib/auth/getAccountIdFromPrivyToken.ts` at line 23, Replace the internal
underscore method call privyClient.users()._get(verified.user_id) with the
documented public API: call privyClient.users.get(...) using an id_token or the
equivalent authenticated parameter (or perform a GET /v1/users/{user_id} via the
REST client) to fetch the user; update getAccountIdFromPrivyToken to
construct/pass the id_token or authenticated context and use
privyClient.users.get to retrieve the user record for verified.user_id so you
avoid relying on the underscore-prefixed internal method.

Comment on lines +30 to +35
const accountDetails = await getAccountDetailsByEmails([email]);
const accountId = accountDetails[0]?.account_id;

if (!accountId) {
throw new Error("No account found for Privy user email");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Resolve account-id deterministically for authenticated email.

Using accountDetails[0]?.account_id can authenticate against an arbitrary row when multiple records share the same email. In auth code, this is an account isolation risk.

🔧 Suggested fix
-  const accountDetails = await getAccountDetailsByEmails([email]);
-  const accountId = accountDetails[0]?.account_id;
-
-  if (!accountId) {
-    throw new Error("No account found for Privy user email");
-  }
-
-  return accountId;
+  const accountDetails = await getAccountDetailsByEmails([email]);
+  const accountIds = [
+    ...new Set(accountDetails.map((row) => row.account_id).filter((id): id is string => Boolean(id))),
+  ];
+
+  if (accountIds.length !== 1) {
+    throw new Error("Unable to resolve a unique account for Privy user email");
+  }
+
+  return accountIds[0];

As per coding guidelines, "Implement built-in security practices for authentication and data handling."

📝 Committable suggestion

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

Suggested change
const accountDetails = await getAccountDetailsByEmails([email]);
const accountId = accountDetails[0]?.account_id;
if (!accountId) {
throw new Error("No account found for Privy user email");
}
const accountDetails = await getAccountDetailsByEmails([email]);
const accountIds = [
...new Set(accountDetails.map((row) => row.account_id).filter((id): id is string => Boolean(id))),
];
if (accountIds.length !== 1) {
throw new Error("Unable to resolve a unique account for Privy user email");
}
return accountIds[0];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/auth/getAccountIdFromPrivyToken.ts` around lines 30 - 35, The code
currently picks accountDetails[0]?.account_id which can select an arbitrary row
when multiple accounts share the same email; update getAccountIdFromPrivyToken
so it deterministically resolves the right account by scanning the
accountDetails array returned from getAccountDetailsByEmails and selecting the
single record that matches the Privy user's identifier or primary/verified
account metadata (e.g., match on privy user id / provider_user_id or an
is_primary/is_verified flag), assign its account_id to accountId, and if no
unique match is found throw an error instead of using the first element; replace
the current accountDetails[0]?.account_id usage with this deterministic
selection logic.

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