Skip to content

Conversation

@sweetmantech
Copy link
Contributor

@sweetmantech sweetmantech commented Jan 16, 2026

Summary

  • Add Bearer token auth support to createChatHandler for /api/chats endpoint
  • Support both x-api-key and Authorization: Bearer authentication (exactly one required)
  • Enable frontend clients to call this endpoint directly with Privy JWT tokens
  • Add 4 new tests for Bearer token authentication

Test plan

  • Verify existing API key auth still works
  • Verify Bearer token auth works with valid Privy JWT
  • Verify 401 returned when no auth provided
  • Verify 401 returned when both auth mechanisms provided

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added templates API endpoint with access control for owned, public, and shared templates
    • Added template favorites functionality (add/remove/toggle)
    • Added artist agents API supporting API key and bearer token authentication
    • Added public creator information endpoint with admin detection
    • Added template sharing capabilities for accounts

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

sweetmantech and others added 2 commits January 15, 2026 21:27
When a user provides an organizationId they don't have access to
(e.g., stale localStorage value), ignore it instead of returning 403.
This prevents blocking chat requests due to stale org selections.

The orgId will be null in this case, and a warning is logged.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Support both x-api-key and Authorization Bearer token for /api/chats endpoint
- Enforce exactly one auth mechanism (same pattern as /api/chat)
- Add 4 new tests for Bearer token authentication
- Enable frontend clients to call this endpoint directly with Privy JWT

Part of chat/create migration from Recoup-Chat to recoup-api.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@vercel
Copy link

vercel bot commented Jan 16, 2026

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

Project Deployment Review Updated (UTC)
recoup-api Error Error Jan 16, 2026 8:33pm

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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

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

📝 Walkthrough

Walkthrough

This PR introduces three new API endpoints for managing agent templates, artist agents, and agent creator information. It includes authentication/authorization flows, database functions for template retrieval with sharing and favoriting support, type definitions, and comprehensive test coverage across all features.

Changes

Cohort / File(s) Summary
Agent Templates - API Routes
app/api/agent-templates/route.ts, app/api/agent-templates/favorites/route.ts
Adds OPTIONS and GET/POST handlers with CORS support. Delegates to getAgentTemplatesHandler and toggleAgentTemplateFavoriteHandler.
Agent Templates - Request Handlers
lib/agentTemplates/getAgentTemplatesHandler.ts, lib/agentTemplates/toggleAgentTemplateFavoriteHandler.ts
Authenticates Bearer token, validates query/body params, calls service functions, returns JSON with CORS headers and error handling.
Agent Templates - Data Fetching
lib/agentTemplates/listAgentTemplatesForUser.ts, lib/agentTemplates/getAccountTemplates.ts, lib/agentTemplates/getSharedTemplatesForAccount.ts, lib/agentTemplates/getSharedEmailsForTemplates.ts
Queries agent templates by userId/sharing, deduplicates, enriches with favorites and shared emails metadata.
Agent Templates - Favorites Operations
lib/agentTemplates/addAgentTemplateFavorite.ts, lib/agentTemplates/removeAgentTemplateFavorite.ts
Adds/removes favorite entries in agent_template_favorites table; handles duplicate constraints.
Agent Templates - Type Definitions
lib/agentTemplates/types.ts
Defines AgentTemplateRow interface with optional computed fields is_favourite and shared_emails.
Agent Templates - Tests
lib/agentTemplates/__tests__/*
Comprehensive test suites (5 files, 900+ lines) covering authentication, authorization, data fetching, deduplication, error handling, and favorite toggling.
Artist Agents - API & Handler
app/api/artist-agents/route.ts, lib/artistAgents/getArtistAgentsHandler.ts, lib/artistAgents/getArtistAgents.ts
Exposes GET endpoint with API key or Bearer token auth; validates socialId params; fetches and transforms artist agents by platform type.
Artist Agents - Tests
lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts
Tests authentication flows (API key, Bearer token), validation, error handling, and response structure.
Agent Creator - API & Handler
app/api/agent-creator/route.ts, lib/agentCreator/getAgentCreatorHandler.ts
Public GET endpoint returning creator details (name, image, admin status) with CORS headers.
Admin Configuration
lib/admin.ts
Exports ADMIN_EMAILS array for admin identification in creator endpoint.
Agent Creator - Tests
lib/agentCreator/__tests__/getAgentCreatorHandler.test.ts
Tests query validation, admin detection, null handling, error cases (404, 400).
Supabase - Agent Template Shares
lib/supabase/agent_template_shares/selectAgentTemplateShares.ts, lib/supabase/agent_template_shares/__tests__/selectAgentTemplateShares.test.ts
Queries agent_template_shares with optional userId/templateIds filters and template joins; includes comprehensive test coverage for query construction and deduplication.
Supabase - Agent Template Favorites
lib/supabase/agent_template_favorites/selectAgentTemplateFavorites.ts, lib/supabase/agent_template_favorites/__tests__/selectAgentTemplateFavorites.test.ts
Fetches favorite template IDs for a user as a Set; includes early-return for invalid userId.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant APIRoute as API Route<br/>/agent-templates
    participant Handler as getAgentTemplatesHandler
    participant Auth as Auth Service
    participant TemplateDB as Template Query
    participant ShareDB as Share/Email Query
    participant CORS as CORS Headers

    Client->>APIRoute: GET /api/agent-templates
    APIRoute->>Handler: Delegate request
    Handler->>Auth: Extract & validate Bearer token
    Auth-->>Handler: User ID
    Handler->>TemplateDB: listAgentTemplatesForUser(userId)
    TemplateDB-->>Handler: Templates
    Handler->>ShareDB: getSharedEmailsForTemplates(templateIds)
    ShareDB-->>Handler: Email map
    Handler->>Handler: Enrich templates with shared_emails
    Handler->>CORS: getCorsHeaders()
    CORS-->>Handler: CORS headers
    Handler-->>APIRoute: NextResponse (200, templates + CORS)
    APIRoute-->>Client: JSON response
Loading
sequenceDiagram
    participant Client
    participant APIRoute as API Route<br/>/artist-agents
    participant Handler as getArtistAgentsHandler
    participant Auth as Auth Service
    participant AgentDB as Agent Query
    participant CORS as CORS Headers

    Client->>APIRoute: GET /api/artist-agents?socialId=...
    APIRoute->>Handler: Delegate request
    Handler->>Auth: Validate x-api-key OR Bearer token
    Auth-->>Handler: Auth status
    Handler->>Handler: Validate socialId param present
    Handler->>AgentDB: getArtistAgents(socialIds)
    AgentDB-->>Handler: Agents with platform type
    Handler->>CORS: getCorsHeaders()
    CORS-->>Handler: CORS headers
    Handler-->>APIRoute: NextResponse (200, agents + CORS)
    APIRoute-->>Client: JSON response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 Templates hop, agents dance in arrays,
Favorites toggled through thoughtful pathways,
With CORS headers and auth so fine,
These endpoints combine in design most divine!
—Your Code Rabbit 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title claims to add Bearer token auth to /api/chats endpoint, but the changes exclusively implement agent templates and artist agents APIs with no modifications to any /api/chats endpoint. Update the PR title to accurately reflect the actual changes: e.g., 'feat: add agent templates, artist agents, and agent creator APIs with authentication' or break into multiple PRs by feature.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 86.84% which is sufficient. The required threshold is 80.00%.

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


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: 0

Caution

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

⚠️ Outside diff range comments (1)
lib/chats/createChatHandler.ts (1)

72-82: Guard accountId override to API key authentication only.

The validateOverrideAccountId function handles a null apiKey by returning a 500 error. However, since this feature is designed for organization API keys (per function documentation), Bearer-authenticated users attempting an override should not reach this function. Add the hasApiKey guard to prevent this semantically incorrect scenario:

Suggested guard
     // Handle accountId override for org API keys
-    if (bodyAccountId) {
+    if (bodyAccountId && hasApiKey) {
       const validated = await validateOverrideAccountId({
         apiKey: request.headers.get("x-api-key"),
         targetAccountId: bodyAccountId,
       });
       if (validated instanceof NextResponse) {
         return validated;
       }
       accountId = validated.accountId;
     }
🧹 Nitpick comments (1)
lib/chats/__tests__/createChatHandler.test.ts (1)

317-393: Consider adding a test for Bearer auth with bodyAccountId override.

The current tests don't cover the scenario where a Bearer-authenticated request includes an accountId in the body. This would exercise the validateOverrideAccountId path with a null API key, which may be an edge case worth verifying.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88ecb4d and fc4039f.

📒 Files selected for processing (4)
  • lib/chat/__tests__/validateChatRequest.test.ts
  • lib/chat/validateChatRequest.ts
  • lib/chats/__tests__/createChatHandler.test.ts
  • lib/chats/createChatHandler.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Apply Single Responsibility Principle (SRP) - one exported function per file
Apply DRY principle (Don't Repeat Yourself) - extract shared logic into reusable utilities

Files:

  • lib/chats/__tests__/createChatHandler.test.ts
  • lib/chat/validateChatRequest.ts
  • lib/chat/__tests__/validateChatRequest.test.ts
  • lib/chats/createChatHandler.ts
!(lib/supabase)/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

NEVER import @/lib/supabase/serverClient outside of lib/supabase/ directory

Files:

  • lib/chats/__tests__/createChatHandler.test.ts
  • lib/chat/validateChatRequest.ts
  • lib/chat/__tests__/validateChatRequest.test.ts
  • lib/chats/createChatHandler.ts
lib/@(auth|chats|emails|ai|x402|trigger)/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

When needing database access in domain folders (lib/auth/, lib/chats/, etc.), first check if a function exists in lib/supabase/[table_name]/, create it if not, then import and use it

Files:

  • lib/chats/__tests__/createChatHandler.test.ts
  • lib/chats/createChatHandler.ts
🧠 Learnings (1)
📚 Learning: 2026-01-14T22:12:03.883Z
Learnt from: CR
Repo: Recoupable-com/Recoup-API PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-14T22:12:03.883Z
Learning: Applies to lib/@(auth|chats|emails|ai|x402|trigger)/**/*.{ts,tsx} : When needing database access in domain folders (lib/auth/, lib/chats/, etc.), first check if a function exists in `lib/supabase/[table_name]/`, create it if not, then import and use it

Applied to files:

  • lib/chats/__tests__/createChatHandler.test.ts
🧬 Code graph analysis (2)
lib/chats/__tests__/createChatHandler.test.ts (6)
lib/auth/getAuthenticatedAccountId.ts (1)
  • getAuthenticatedAccountId (13-50)
lib/networking/safeParseJson.ts (1)
  • safeParseJson (10-16)
lib/supabase/rooms/insertRoom.ts (1)
  • insertRoom (8-14)
lib/chats/createChatHandler.ts (1)
  • createChatHandler (23-126)
lib/auth/getApiKeyAccountId.ts (1)
  • getApiKeyAccountId (14-77)
lib/chats/generateChatTitle.ts (1)
  • generateChatTitle (11-21)
lib/chats/createChatHandler.ts (3)
lib/networking/getCorsHeaders.ts (1)
  • getCorsHeaders (6-12)
lib/auth/getApiKeyAccountId.ts (1)
  • getApiKeyAccountId (14-77)
lib/auth/getAuthenticatedAccountId.ts (1)
  • getAuthenticatedAccountId (13-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review
🔇 Additional comments (7)
lib/chats/createChatHandler.ts (2)

25-43: LGTM on the auth mechanism enforcement.

The XOR logic correctly enforces exactly one authentication method. Clear error message guides API consumers.


45-61: LGTM on the conditional auth path selection.

Clean branching between API key and Bearer token authentication, properly propagating errors from both helpers.

lib/chats/__tests__/createChatHandler.test.ts (3)

56-94: Well-structured mock request helpers.

The helper functions cleanly cover all auth scenarios: API key only, Bearer only, no auth, and both auth mechanisms.


318-346: Good coverage for Bearer token authentication path.

Tests correctly verify that Bearer auth uses getAuthenticatedAccountId and does not call getApiKeyAccountId.


395-421: Auth enforcement tests are comprehensive.

Both edge cases (no auth and dual auth) are covered with correct assertions.

lib/chat/validateChatRequest.ts (1)

150-159: Lenient organizationId handling aligns with PR objectives.

The change from 403 rejection to warning-and-ignore is intentional for handling stale localStorage values. The warning log provides visibility for debugging without breaking the user experience.

lib/chat/__tests__/validateChatRequest.test.ts (1)

497-511: Test correctly updated to reflect lenient organizationId handling.

The test now properly verifies that invalid organization access results in success with orgId: null rather than a 403 error, matching the implementation change.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@vercel vercel bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

Bearer token users attempting to override their accountId will receive a 500 error instead of being prevented from doing so at the validation layer. The accountId override should only be allowed for API key authentication.

View Details
📝 Patch Details
diff --git a/lib/chats/__tests__/createChatHandler.test.ts b/lib/chats/__tests__/createChatHandler.test.ts
index e2d6558..8720d4c 100644
--- a/lib/chats/__tests__/createChatHandler.test.ts
+++ b/lib/chats/__tests__/createChatHandler.test.ts
@@ -390,6 +390,40 @@ describe("createChatHandler", () => {
       expect(json.status).toBe("success");
       expect(generateChatTitle).toHaveBeenCalledWith(firstMessage);
     });
+
+    it("ignores accountId override attempt with Bearer auth (does not call validateOverrideAccountId)", async () => {
+      const bearerAccountId = "bearer-account-123";
+      const targetAccountId = "123e4567-e89b-12d3-a456-426614174001";
+      const artistId = "123e4567-e89b-12d3-a456-426614174000";
+
+      vi.mocked(getAuthenticatedAccountId).mockResolvedValue(bearerAccountId);
+      vi.mocked(safeParseJson).mockResolvedValue({
+        artistId,
+        accountId: targetAccountId,
+      });
+      vi.mocked(insertRoom).mockResolvedValue({
+        id: "generated-uuid-123",
+        account_id: bearerAccountId,
+        artist_id: artistId,
+        topic: null,
+      });
+
+      const request = createMockBearerRequest();
+      const response = await createChatHandler(request);
+      const json = await response.json();
+
+      expect(response.status).toBe(200);
+      expect(json.status).toBe("success");
+      // validateOverrideAccountId should NOT be called for Bearer token auth
+      expect(validateOverrideAccountId).not.toHaveBeenCalled();
+      // The room should be created with the Bearer token's own accountId, not the requested override
+      expect(insertRoom).toHaveBeenCalledWith({
+        id: "generated-uuid-123",
+        account_id: bearerAccountId,
+        artist_id: artistId,
+        topic: null,
+      });
+    });
   });
 
   describe("auth mechanism enforcement", () => {
diff --git a/lib/chats/createChatHandler.ts b/lib/chats/createChatHandler.ts
index 53e156b..9eb6d21 100644
--- a/lib/chats/createChatHandler.ts
+++ b/lib/chats/createChatHandler.ts
@@ -69,8 +69,8 @@ export async function createChatHandler(request: NextRequest): Promise<NextRespo
 
     const { artistId, chatId, accountId: bodyAccountId, firstMessage } = validated;
 
-    // Handle accountId override for org API keys
-    if (bodyAccountId) {
+    // Handle accountId override for org API keys (only for API key auth)
+    if (bodyAccountId && hasApiKey) {
       const validated = await validateOverrideAccountId({
         apiKey: request.headers.get("x-api-key"),
         targetAccountId: bodyAccountId,

Analysis

Bearer token users receive 500 error when providing accountId override

What fails: In createChatHandler.ts (lines 72-82), when a Bearer token authenticated user provides an accountId parameter in the request body, the code unconditionally calls validateOverrideAccountId() with apiKey: null. This causes validateOverrideAccountId() to return a 500 error with message "Failed to validate API key" instead of preventing the override at the validation layer.

How to reproduce:

  1. Create a chat request with Bearer token authentication (via Authorization header)
  2. Include accountId in the request body
  3. Expected: Either silently ignore the accountId field (consistent with other lenient field handling) or return a 400/403 error explaining that accountId overrides are only for API key auth
  4. Actual: Returns 500 error "Failed to validate API key"

Root cause: The accountId override validation was not guarded by if (hasApiKey) check like it is in the correct implementation in validateChatRequest.ts (lines 171-180). When Bearer token users include an accountId, the code calls validateOverrideAccountId() with apiKey: null, which immediately returns a 500 error at line 23-29 of validateOverrideAccountId.ts.

Fix implemented: Added && hasApiKey condition to line 73 of createChatHandler.ts to only execute accountId override validation for API key authentication, matching the pattern used in validateChatRequest.ts. Bearer token users now have their accountId silently ignored in the override check, consistent with lenient organization handling patterns in the codebase.

Test added: Added test case in createChatHandler.test.ts that verifies Bearer token authentication with accountId override is handled correctly without calling validateOverrideAccountId() and returns 200 with the Bearer token's own accountId.

Add new endpoint to fetch artist agents by social IDs with Bearer token
and API key authentication support.

- GET /api/artist-agents?socialId=xxx&socialId=yyy
- Supports Bearer token auth (for frontend clients)
- Supports x-api-key auth (for API consumers)
- Returns aggregated agents by platform type

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link

@vercel vercel bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

Bearer token authenticated requests that include an accountId override in the request body will fail with a 500 error instead of being properly handled or rejected with a clear authorization error.

View Details
📝 Patch Details
diff --git a/lib/chats/__tests__/createChatHandler.test.ts b/lib/chats/__tests__/createChatHandler.test.ts
index e2d6558..ba595e2 100644
--- a/lib/chats/__tests__/createChatHandler.test.ts
+++ b/lib/chats/__tests__/createChatHandler.test.ts
@@ -390,6 +390,40 @@ describe("createChatHandler", () => {
       expect(json.status).toBe("success");
       expect(generateChatTitle).toHaveBeenCalledWith(firstMessage);
     });
+
+    it("ignores accountId override when using Bearer token auth (only supported with API keys)", async () => {
+      const bearerAccountId = "123e4567-e89b-12d3-a456-426614174000";
+      const targetAccountId = "223e4567-e89b-12d3-a456-426614174001";
+      const artistId = "323e4567-e89b-12d3-a456-426614174002";
+
+      vi.mocked(getAuthenticatedAccountId).mockResolvedValue(bearerAccountId);
+      vi.mocked(safeParseJson).mockResolvedValue({
+        artistId,
+        accountId: targetAccountId,  // <-- Override in body with Bearer token
+      });
+      vi.mocked(insertRoom).mockResolvedValue({
+        id: "generated-uuid-123",
+        account_id: bearerAccountId,  // <-- Uses Bearer token's account, not override
+        artist_id: artistId,
+        topic: null,
+      });
+
+      const request = createMockBearerRequest();
+      const response = await createChatHandler(request);
+      const json = await response.json();
+
+      // FIX: Bearer token auth ignores accountId override and uses authenticated account
+      // validateOverrideAccountId should NOT be called for Bearer token auth
+      expect(response.status).toBe(200);
+      expect(json.status).toBe("success");
+      expect(validateOverrideAccountId).not.toHaveBeenCalled();
+      expect(insertRoom).toHaveBeenCalledWith({
+        id: "generated-uuid-123",
+        account_id: bearerAccountId,  // Uses Bearer token's account
+        artist_id: artistId,
+        topic: null,
+      });
+    });
   });
 
   describe("auth mechanism enforcement", () => {
diff --git a/lib/chats/createChatHandler.ts b/lib/chats/createChatHandler.ts
index 53e156b..70d5794 100644
--- a/lib/chats/createChatHandler.ts
+++ b/lib/chats/createChatHandler.ts
@@ -69,8 +69,8 @@ export async function createChatHandler(request: NextRequest): Promise<NextRespo
 
     const { artistId, chatId, accountId: bodyAccountId, firstMessage } = validated;
 
-    // Handle accountId override for org API keys
-    if (bodyAccountId) {
+    // Handle accountId override for org API keys (only with API key auth)
+    if (bodyAccountId && hasApiKey) {
       const validated = await validateOverrideAccountId({
         apiKey: request.headers.get("x-api-key"),
         targetAccountId: bodyAccountId,

Analysis

Bearer token + accountId override causes 500 error in createChatHandler()

What fails: createChatHandler() in lib/chats/createChatHandler.ts returns a 500 "Failed to validate API key" error when a Bearer token authenticated request includes an accountId override in the request body.

How to reproduce:

// Create request with Bearer token auth
const request = createMockBearerRequest("test-bearer-token");

// Parse body with accountId override
const bodyData = {
  artistId: "323e4567-e89b-12d3-a456-426614174002",
  accountId: "223e4567-e89b-12d3-a456-426614174001",  // Override
};

// Handler calls validateOverrideAccountId with null apiKey for Bearer tokens
// validateOverrideAccountId checks `if (!apiKey)` and returns 500 error

What happens: Returns HTTP 500 with error message "Failed to validate API key"

Expected behavior: Bearer token requests should either skip accountId override validation (since overrides are only for org API keys per code comment at line 72) or return a proper 403 authorization error. Currently passes null apiKey to validateOverrideAccountId() which treats it as a 500 server error instead of 403 access denied.

Root cause: Lines 72-77 in createChatHandler.ts call validateOverrideAccountId() unconditionally whenever bodyAccountId is present, without checking if the authentication method is API key or Bearer token. The function is documented as "Handle accountId override for org API keys" but executes regardless of auth type.

Fix: Guard the accountId override validation with a check for hasApiKey, matching the pattern already correctly implemented in lib/chat/validateChatRequest.ts lines 123-132. Only API key authenticated requests should attempt override validation; Bearer token requests ignore the override and use the authenticated account.

Verification: Added test case "ignores accountId override when using Bearer token auth (only supported with API keys)" to lib/chats/__tests__/createChatHandler.test.ts which confirms the fix works correctly. All 516 tests in the suite pass after the fix.

sweetmantech and others added 2 commits January 16, 2026 12:25
Reverts the changes that made organizationId validation lenient.
Restores the original strict behavior that rejects requests when
user is not a member of the specified organization.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Restores the original createChatHandler implementation.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
type: new String(
agent.agent_status.length > 1
? "wrapped"
: getSocialPlatformByLink(agent.agent_status[0].social.profile_url),
Copy link

@vercel vercel bot Jan 16, 2026

Choose a reason for hiding this comment

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

Suggested change
: getSocialPlatformByLink(agent.agent_status[0].social.profile_url),
: agent.agent_status.length > 0
? getSocialPlatformByLink(agent.agent_status[0].social.profile_url)
: "NONE",

The code accesses agent.agent_status[0] without verifying the array has at least one element. If an agent has no agent_status records, this will throw a runtime error.

View Details

Analysis

Missing null check causes runtime error when agent has no status records

What fails: getArtistAgents() function crashes when an agent has no associated agent_status records

How to reproduce: When Supabase returns an agent with an empty agent_status array (which occurs when a relationship join finds no related records, per Supabase documentation), the ternary expression on line 45-47 accesses agent.agent_status[0] without checking if the array is empty:

agent.agent_status.length > 1
  ? "wrapped"
  : getSocialPlatformByLink(agent.agent_status[0].social.profile_url),

When agent.agent_status.length === 0, agent.agent_status[0] is undefined, causing the error:

Result: TypeError: Cannot read properties of undefined (reading 'social')

Expected: Should gracefully handle agents with no status records by returning "NONE" or similar default value, consistent with the behavior of getSocialPlatformByLink() which returns "NONE" for missing URLs

Fix: Added check for non-empty array before accessing element:

agent.agent_status.length > 1
  ? "wrapped"
  : agent.agent_status.length > 0
    ? getSocialPlatformByLink(agent.agent_status[0].social.profile_url)
    : "NONE"

Migrate agent templates listing from Recoup-Chat to recoup-api with:
- Bearer token authentication via Privy
- Combined query for owned, public, and shared templates
- User favorites tracking
- Shared emails for private templates

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Updated getArtistAgents.ts to import from lib/artists/getSocialPlatformByLink.ts
- Deleted duplicate lib/artistAgents/getSocialPlatformByLink.ts

YAGNI cleanup: the canonical location for this utility is lib/artists/

Co-Authored-By: Claude Opus 4.5 <[email protected]>
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

🤖 Fix all issues with AI agents
In `@app/api/agent-templates/route.ts`:
- Around line 25-27: The handler currently accepts a caller-supplied userId
query param which can be used to read another user's private templates; update
the GET route handler in route.ts to validate that the requested userId equals
the authenticated account id (e.g., compare query userId to session.user.id) or
allow it only when the authenticated account has an admin flag (e.g., isAdmin on
the session/user object), otherwise return a 403 and do not pass the unverified
userId into the template retrieval function (e.g.,
getTemplates/getTemplatesForUser); ensure all DB queries use the verified id
variable after this check.
- Around line 31-32: The GET route currently forwards raw query parameters to
getAgentTemplatesHandler; add Zod validation by creating a schema function
validateAgentTemplatesQuery that parses the NextRequest.query (validating userId
as required/optional per API contract) and use it at the top of export async
function GET(request: NextRequest) to validate/normalize inputs before calling
getAgentTemplatesHandler; if validation fails, return an appropriate
NextResponse error (400) and on success pass the validated result (not raw
query) into getAgentTemplatesHandler.

In `@app/api/artist-agents/route.ts`:
- Around line 11-16: The CORS preflight handler OPTIONS calls getCorsHeaders()
but that function's Access-Control-Allow-Headers doesn't include the x-api-key
header, causing browser preflight failures; update the getCorsHeaders()
implementation to include "x-api-key" (and consider canonical casing like
"X-API-Key" or include both lowercase and canonical form) in the
Access-Control-Allow-Headers value so the OPTIONS response from the OPTIONS()
handler allows clients to send the x-api-key header to the GET handler.

In `@lib/agentTemplates/getAgentTemplatesHandler.ts`:
- Around line 30-35: The userId is being overridden from the request query
without authorization checks; update getAgentTemplatesHandler to enforce that
the requested userId equals the authenticated accountId (or verify caller has an
admin/impersonation privilege) before passing it to getUserAccessibleTemplates,
otherwise ignore the query param and use accountId; if cross-user access is
intentional add a clarifying comment about admin-only behavior and the required
permission check (reference variables: userId, accountId, request.url and
function: getUserAccessibleTemplates).

In `@lib/agentTemplates/getAgentTemplateSharesByTemplateIds.ts`:
- Line 1: The file getAgentTemplateSharesByTemplateIds imports supabase directly
from "@/lib/supabase/serverClient", which violates the rule against importing
serverClient outside lib/supabase; to fix, either move the
getAgentTemplateSharesByTemplateIds function into the lib/supabase/ directory or
create a wrapper/export (e.g., a function like getSupabaseClient or
getAgentTemplateSharesByTemplateIdsDb) inside lib/supabase that encapsulates the
serverClient import and call that wrapper from this module, then update the
import to reference the new wrapper instead of "@/lib/supabase/serverClient".

In `@lib/agentTemplates/getSharedTemplatesForUser.ts`:
- Line 1: The file imports the Supabase client directly via "import supabase
from '@/lib/supabase/serverClient'", which violates the guideline forbidding
direct imports outside lib/supabase; replace that direct import by using the
sanctioned accessor exported from the lib/supabase module (for example import {
getServerClient } or similar factory exported by your lib/supabase index) and
call it inside getSharedTemplatesForUser to obtain the client instance; update
references to the "supabase" symbol in getSharedTemplatesForUser to use the
returned client from the sanctioned accessor so the file no longer imports
serverClient directly.

In `@lib/agentTemplates/listAgentTemplatesForUser.ts`:
- Line 1: The code currently imports the DB client directly from
"@/lib/supabase/serverClient" in listAgentTemplatesForUser.ts; move that DB
access into a new helper exported from the lib/supabase/agent_templates/
namespace (e.g., create and export getAgentTemplatesForUser(userId: string) in
lib/supabase/agent_templates) that imports serverClient internally and performs
the same query against the agent_templates table, then change
listAgentTemplatesForUser.ts to import and call getAgentTemplatesForUser instead
of importing serverClient directly.

In `@lib/artistAgents/getArtistAgents.ts`:
- Around line 36-41: The second Supabase query in getArtistAgents (the call that
does supabase.from("agents").select(...).in("id", agentIds)) currently only
destructures data as agents and returns [] on falsy data, which can hide DB
errors; change the call to destructure both data and error (e.g., { data:
agents, error }) and mirror the first query's handling by logging the error
(using the same logger used earlier) and returning early or throwing as
appropriate instead of silently returning [], so that database errors are
surfaced and diagnosable.

In `@lib/artistAgents/getSocialPlatformByLink.ts`:
- Around line 1-21: This file contains a duplicate implementation of
getSocialPlatformByLink; remove the duplicate implementation and switch callers
(e.g., the code in getArtistAgents) to import and use the single canonical
getSocialPlatformByLink utility exported from the existing artists utility
module instead of the local copy—update the import in getArtistAgents to
reference the canonical export and delete the duplicate file/export to satisfy
DRY.
♻️ Duplicate comments (1)
lib/artistAgents/getArtistAgents.ts (1)

43-51: Runtime error when agent_status array is empty.

When agent.agent_status.length === 0, the condition length > 1 is false, so it falls through to access agent.agent_status[0].social.profile_url, which throws TypeError: Cannot read properties of undefined.

🐛 Proposed fix
   const transformedAgents = agents.map((agent) => ({
     type: new String(
       agent.agent_status.length > 1
         ? "wrapped"
-        : getSocialPlatformByLink(agent.agent_status[0].social.profile_url),
+        : agent.agent_status.length > 0
+          ? getSocialPlatformByLink(agent.agent_status[0].social.profile_url)
+          : "NONE",
     ).toLowerCase(),
     agentId: agent.id,
     updated_at: agent.updated_at,
   }));
🧹 Nitpick comments (7)
lib/artistAgents/getArtistAgentsHandler.ts (2)

58-73: Missing Zod schema validation for query parameters.

Per coding guidelines, API endpoints should use a validate function with Zod for input parsing. The socialId parameters should be validated with a schema.

♻️ Suggested implementation
import { z } from "zod";

const querySchema = z.object({
  socialId: z.array(z.string().uuid()).min(1, "At least one socialId is required"),
});

// In handler:
const { searchParams } = new URL(request.url);
const socialIds = searchParams.getAll("socialId");

const parsed = querySchema.safeParse({ socialId: socialIds });
if (!parsed.success) {
  return NextResponse.json(
    {
      status: "error",
      message: parsed.error.errors[0]?.message || "Invalid input",
    },
    { status: 400, headers: getCorsHeaders() },
  );
}

As per coding guidelines, all API endpoints should use Zod for schema validation.


43-56: Consider extracting authentication logic to reduce duplication.

The auth check pattern (try both mechanisms, return early on error) is repeated in handlers. Consider extracting a shared helper that handles the "exactly one auth mechanism" logic.

lib/agentTemplates/getSharedTemplatesForUser.ts (1)

8-10: Consider adding input validation for userId.

The function accepts userId without validation. An empty string or whitespace-only value would still execute the query. Consider adding a guard similar to getAgentTemplateSharesByTemplateIds:

♻️ Suggested validation
 export async function getSharedTemplatesForUser(
   userId: string,
 ): Promise<AgentTemplateRow[]> {
+  if (!userId || userId.trim() === "") return [];
+
   const { data, error } = await supabase
lib/agentTemplates/getAgentTemplatesHandler.ts (1)

47-53: Consider simplifying the template mapping.

Public templates are assigned an empty shared_emails array. If the frontend doesn't need this field for public templates, you could omit it entirely to reduce response payload size. However, keeping it for consistency is also a valid choice.

lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts (1)

171-184: Consider adding test for when both auth mechanisms are provided.

The PR objectives mention: "Verify 401 returned when both auth mechanisms provided." The current tests cover "no auth" but not the "both auth" scenario. Consider adding a test case:

🧪 Suggested additional test
it("returns 401 when both auth mechanisms are provided", async () => {
  const url = new URL("http://localhost/api/artist-agents");
  url.searchParams.append("socialId", "social-1");
  const request = {
    url: url.toString(),
    headers: {
      get: (name: string) => {
        if (name === "x-api-key") return "test-api-key";
        if (name === "authorization") return "Bearer test-token";
        return null;
      },
    },
  } as unknown as NextRequest;

  const response = await getArtistAgentsHandler(request);
  const json = await response.json();

  expect(response.status).toBe(401);
  expect(json.status).toBe("error");
  expect(json.message).toBe(
    "Exactly one of x-api-key or Authorization must be provided",
  );
});
lib/agentTemplates/listAgentTemplatesForUser.ts (1)

1-23: Extract the repeated select field list.

The column list is duplicated in both branches; consider a shared constant to keep it in sync. As per coding guidelines, apply DRY to shared logic.

♻️ Proposed refactor
 import supabase from "@/lib/supabase/serverClient";
 
+const TEMPLATE_FIELDS =
+  "id, title, description, prompt, tags, creator, is_private, created_at, favorites_count, updated_at";
+
 export async function listAgentTemplatesForUser(userId?: string | null) {
   if (userId && userId !== "undefined") {
     const { data, error } = await supabase
       .from("agent_templates")
-      .select(
-        "id, title, description, prompt, tags, creator, is_private, created_at, favorites_count, updated_at",
-      )
+      .select(TEMPLATE_FIELDS)
       .or(`creator.eq.${userId},is_private.eq.false`)
       .order("title");
     if (error) throw error;
     return data ?? [];
   }
 
   const { data, error } = await supabase
     .from("agent_templates")
-    .select(
-      "id, title, description, prompt, tags, creator, is_private, created_at, favorites_count, updated_at",
-    )
+    .select(TEMPLATE_FIELDS)
     .eq("is_private", false)
     .order("title");
app/api/agent-templates/route.ts (1)

11-16: SRP guideline: one exported handler per file.

This route exports both OPTIONS and GET. If the guideline is strict, move CORS preflight handling into a shared helper or document an exception for Next.js route files. As per coding guidelines, keep one exported function per file where feasible.

Also applies to: 31-32

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc4039f and add518d.

📒 Files selected for processing (15)
  • app/api/agent-templates/route.ts
  • app/api/artist-agents/route.ts
  • lib/agentTemplates/__tests__/getAgentTemplatesHandler.test.ts
  • lib/agentTemplates/getAgentTemplateSharesByTemplateIds.ts
  • lib/agentTemplates/getAgentTemplatesHandler.ts
  • lib/agentTemplates/getSharedEmailsForTemplates.ts
  • lib/agentTemplates/getSharedTemplatesForUser.ts
  • lib/agentTemplates/getUserAccessibleTemplates.ts
  • lib/agentTemplates/getUserTemplateFavorites.ts
  • lib/agentTemplates/listAgentTemplatesForUser.ts
  • lib/agentTemplates/types.ts
  • lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts
  • lib/artistAgents/getArtistAgents.ts
  • lib/artistAgents/getArtistAgentsHandler.ts
  • lib/artistAgents/getSocialPlatformByLink.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Apply Single Responsibility Principle (SRP) - one exported function per file
Apply DRY principle (Don't Repeat Yourself) - extract shared logic into reusable utilities

Files:

  • lib/agentTemplates/getUserAccessibleTemplates.ts
  • lib/artistAgents/getArtistAgentsHandler.ts
  • lib/artistAgents/getSocialPlatformByLink.ts
  • lib/agentTemplates/getSharedEmailsForTemplates.ts
  • lib/agentTemplates/getAgentTemplatesHandler.ts
  • app/api/artist-agents/route.ts
  • lib/agentTemplates/types.ts
  • lib/artistAgents/getArtistAgents.ts
  • lib/agentTemplates/getUserTemplateFavorites.ts
  • app/api/agent-templates/route.ts
  • lib/agentTemplates/getSharedTemplatesForUser.ts
  • lib/agentTemplates/getAgentTemplateSharesByTemplateIds.ts
  • lib/agentTemplates/__tests__/getAgentTemplatesHandler.test.ts
  • lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts
  • lib/agentTemplates/listAgentTemplatesForUser.ts
!(lib/supabase)/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

NEVER import @/lib/supabase/serverClient outside of lib/supabase/ directory

Files:

  • lib/agentTemplates/getUserAccessibleTemplates.ts
  • lib/artistAgents/getArtistAgentsHandler.ts
  • lib/artistAgents/getSocialPlatformByLink.ts
  • lib/agentTemplates/getSharedEmailsForTemplates.ts
  • lib/agentTemplates/getAgentTemplatesHandler.ts
  • app/api/artist-agents/route.ts
  • lib/agentTemplates/types.ts
  • lib/artistAgents/getArtistAgents.ts
  • lib/agentTemplates/getUserTemplateFavorites.ts
  • app/api/agent-templates/route.ts
  • lib/agentTemplates/getSharedTemplatesForUser.ts
  • lib/agentTemplates/getAgentTemplateSharesByTemplateIds.ts
  • lib/agentTemplates/__tests__/getAgentTemplatesHandler.test.ts
  • lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts
  • lib/agentTemplates/listAgentTemplatesForUser.ts
app/api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

app/api/**/*.{ts,tsx}: All API routes should have JSDoc comments
All API endpoints should use a validate function for input parsing with Zod for schema validation

Files:

  • app/api/artist-agents/route.ts
  • app/api/agent-templates/route.ts
🧠 Learnings (2)
📚 Learning: 2026-01-14T22:12:03.883Z
Learnt from: CR
Repo: Recoupable-com/Recoup-API PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-14T22:12:03.883Z
Learning: Applies to app/api/**/*.{ts,tsx} : All API routes should have JSDoc comments

Applied to files:

  • app/api/agent-templates/route.ts
📚 Learning: 2026-01-14T22:12:03.883Z
Learnt from: CR
Repo: Recoupable-com/Recoup-API PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-14T22:12:03.883Z
Learning: Applies to lib/supabase/**/*.{ts,tsx} : Complex Supabase queries with joins should be named `get[Descriptive].ts`

Applied to files:

  • lib/agentTemplates/getSharedTemplatesForUser.ts
🧬 Code graph analysis (9)
lib/agentTemplates/getUserAccessibleTemplates.ts (4)
lib/agentTemplates/listAgentTemplatesForUser.ts (1)
  • listAgentTemplatesForUser (3-25)
lib/agentTemplates/getSharedTemplatesForUser.ts (1)
  • getSharedTemplatesForUser (8-44)
lib/agentTemplates/getUserTemplateFavorites.ts (1)
  • getUserTemplateFavorites (7-20)
lib/agentTemplates/types.ts (1)
  • AgentTemplateRow (1-16)
lib/artistAgents/getArtistAgentsHandler.ts (3)
lib/networking/getCorsHeaders.ts (1)
  • getCorsHeaders (6-12)
lib/auth/getApiKeyAccountId.ts (1)
  • getApiKeyAccountId (14-77)
lib/auth/getAuthenticatedAccountId.ts (1)
  • getAuthenticatedAccountId (13-50)
lib/artistAgents/getSocialPlatformByLink.ts (1)
lib/artists/getSocialPlatformByLink.ts (1)
  • getSocialPlatformByLink (7-19)
lib/agentTemplates/getSharedEmailsForTemplates.ts (2)
lib/agentTemplates/getAgentTemplateSharesByTemplateIds.ts (1)
  • getAgentTemplateSharesByTemplateIds (9-25)
lib/supabase/account_emails/selectAccountEmails.ts (1)
  • selectAccountEmails (12-48)
app/api/artist-agents/route.ts (2)
lib/networking/getCorsHeaders.ts (1)
  • getCorsHeaders (6-12)
lib/artistAgents/getArtistAgentsHandler.ts (1)
  • getArtistAgentsHandler (19-100)
lib/artistAgents/getArtistAgents.ts (1)
lib/artists/getSocialPlatformByLink.ts (1)
  • getSocialPlatformByLink (7-19)
lib/agentTemplates/getSharedTemplatesForUser.ts (1)
lib/agentTemplates/types.ts (1)
  • AgentTemplateRow (1-16)
lib/agentTemplates/getAgentTemplateSharesByTemplateIds.ts (1)
lib/agentTemplates/types.ts (1)
  • AgentTemplateShare (18-22)
lib/agentTemplates/__tests__/getAgentTemplatesHandler.test.ts (4)
lib/auth/getAuthenticatedAccountId.ts (1)
  • getAuthenticatedAccountId (13-50)
lib/agentTemplates/getAgentTemplatesHandler.ts (1)
  • getAgentTemplatesHandler (18-78)
lib/agentTemplates/getUserAccessibleTemplates.ts (1)
  • getUserAccessibleTemplates (6-41)
lib/agentTemplates/getSharedEmailsForTemplates.ts (1)
  • getSharedEmailsForTemplates (4-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review
🔇 Additional comments (10)
lib/agentTemplates/getUserTemplateFavorites.ts (1)

7-20: LGTM!

Clean implementation following single responsibility principle. The function correctly queries favorites and returns a deduplicated Set. Error propagation via throw is appropriate for this utility.

lib/artistAgents/getArtistAgents.ts (1)

53-59: Aggregation keeps "last in iteration order", not "latest by timestamp".

The comment says "latest one for each type wins" but the code simply overwrites without comparing updated_at. The actual behavior is deterministic based on Supabase's returned order, which may not be by timestamp. If true latest-wins is intended, explicit sorting or comparison is needed.

If the current behavior (last in returned order) is intentional, consider updating the comment for clarity. Otherwise, add explicit sorting:

+  // Sort by updated_at ascending so later entries overwrite earlier ones
+  transformedAgents.sort((a, b) => 
+    new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime()
+  );
+
   transformedAgents.forEach((agent) => {
app/api/artist-agents/route.ts (1)

32-34: LGTM!

Clean delegation pattern to the handler. JSDoc documentation is present and accurate.

lib/agentTemplates/getAgentTemplateSharesByTemplateIds.ts (1)

9-25: Function logic is correct.

The implementation properly validates input, handles empty arrays, performs the query, and has appropriate error handling with logging before re-throwing. The type casting to AgentTemplateShare[] aligns with the type definition in lib/agentTemplates/types.ts.

lib/agentTemplates/getSharedEmailsForTemplates.ts (1)

1-50: LGTM! Well-structured utility function.

The implementation properly:

  • Composes existing utilities (getAgentTemplateSharesByTemplateIds, selectAccountEmails) instead of duplicating logic
  • Handles edge cases (empty input, no shares)
  • Correctly maps users with multiple email addresses to templates
  • Deduplicates the final email lists

This follows both SRP and DRY principles. As per coding guidelines, this approach correctly avoids direct Supabase client imports by using wrapper functions.

lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts (1)

1-200: Good test coverage overall.

The test suite comprehensively covers:

  • Input validation (missing socialId)
  • API key authentication (success and failure)
  • Bearer token authentication (success and failure)
  • No-auth rejection
  • Error handling (500 on internal errors)

The mock setup is clean and the assertions verify both status codes and response bodies.

lib/agentTemplates/getSharedTemplatesForUser.ts (1)

14-17: No changes needed - favorites_count is correctly defined in the type.

The AgentTemplateRow interface in lib/agentTemplates/types.ts includes favorites_count: number | null; at line 9. The query correctly selects this field, and there is no type mismatch. The code is type-safe as written.

Likely an incorrect or invalid review comment.

lib/agentTemplates/getUserAccessibleTemplates.ts (1)

6-40: Clean aggregation of owned/shared/favorite templates.

Logic is clear and the favorite annotation is consistently applied for authenticated and anonymous users.

lib/agentTemplates/__tests__/getAgentTemplatesHandler.test.ts (1)

62-225: Solid coverage for auth, shared-email, and error paths.

Mocks are clear and the assertions match the handler behavior.

lib/agentTemplates/types.ts (1)

1-22: Types look good and align with handler usage.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +11 to +16
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}
Copy link

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

🏁 Script executed:

#!/bin/bash
# Find and examine the getCorsHeaders function
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "getCorsHeaders" | head -20

Repository: Recoupable-com/Recoup-API

Length of output: 747


🏁 Script executed:

#!/bin/bash
# Look for the getCorsHeaders implementation
rg -A 20 "export.*getCorsHeaders|function getCorsHeaders" --type ts --type tsx

Repository: Recoupable-com/Recoup-API

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Check for the specific file mentioned in the review
cat lib/networking/getCorsHeaders.ts 2>/dev/null || echo "File not found at expected location"

Repository: Recoupable-com/Recoup-API

Length of output: 414


🏁 Script executed:

#!/bin/bash
# Search for Access-Control-Allow-Headers in the codebase
rg "Access-Control-Allow-Headers" --type ts --type tsx -B 2 -A 2

Repository: Recoupable-com/Recoup-API

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Search for x-api-key usage in the codebase
rg "x-api-key|X-Api-Key|X-API-KEY" --type ts -i

Repository: Recoupable-com/Recoup-API

Length of output: 7484


🏁 Script executed:

#!/bin/bash
# Check the artist-agents route specifically
cat app/api/artist-agents/route.ts

Repository: Recoupable-com/Recoup-API

Length of output: 1087


🏁 Script executed:

#!/bin/bash
# Search for any authentication patterns in api routes
rg "Authorization|x-api-key" app/api/ --type ts -B 2 -A 2 | head -50

Repository: Recoupable-com/Recoup-API

Length of output: 2749


Add x-api-key to the CORS allowed headers list.

The endpoint's JSDoc states it accepts x-api-key header for authentication, but the getCorsHeaders() function returns Access-Control-Allow-Headers: "Content-Type, Authorization, X-Requested-With" without including x-api-key. Browser-based clients attempting to send the x-api-key header will fail the CORS preflight check before reaching the GET handler.

🤖 Prompt for AI Agents
In `@app/api/artist-agents/route.ts` around lines 11 - 16, The CORS preflight
handler OPTIONS calls getCorsHeaders() but that function's
Access-Control-Allow-Headers doesn't include the x-api-key header, causing
browser preflight failures; update the getCorsHeaders() implementation to
include "x-api-key" (and consider canonical casing like "X-API-Key" or include
both lowercase and canonical form) in the Access-Control-Allow-Headers value so
the OPTIONS response from the OPTIONS() handler allows clients to send the
x-api-key header to the GET handler.

Comment on lines 30 to 35
// Parse userId from query params (optional)
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId") || accountId;

// Fetch templates accessible to the user
const templates = await getUserAccessibleTemplates(userId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential authorization issue: userId query parameter allows fetching any user's templates.

The comment on line 13 states the userId "must match authenticated user," but the code on line 32 allows overriding userId with any value from the query parameter without validation. This could allow an authenticated user to fetch templates for any other user.

If this is intentional behavior (e.g., for admin use cases), update the comment to clarify. Otherwise, add authorization enforcement:

🔒 Suggested fix to enforce authorization
     // Parse userId from query params (optional)
     const { searchParams } = new URL(request.url);
-    const userId = searchParams.get("userId") || accountId;
+    const requestedUserId = searchParams.get("userId");
+    
+    // If userId is specified, verify it matches the authenticated user
+    if (requestedUserId && requestedUserId !== accountId) {
+      return NextResponse.json(
+        { status: "error", message: "Forbidden: Cannot access other user's templates" },
+        { status: 403, headers: getCorsHeaders() }
+      );
+    }
+    
+    const userId = requestedUserId || accountId;
🤖 Prompt for AI Agents
In `@lib/agentTemplates/getAgentTemplatesHandler.ts` around lines 30 - 35, The
userId is being overridden from the request query without authorization checks;
update getAgentTemplatesHandler to enforce that the requested userId equals the
authenticated accountId (or verify caller has an admin/impersonation privilege)
before passing it to getUserAccessibleTemplates, otherwise ignore the query
param and use accountId; if cross-user access is intentional add a clarifying
comment about admin-only behavior and the required permission check (reference
variables: userId, accountId, request.url and function:
getUserAccessibleTemplates).

@@ -0,0 +1,25 @@
import supabase from "@/lib/supabase/serverClient";
Copy link

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

🏁 Script executed:

find . -name "listAgentTemplatesForUser.ts" -type f

Repository: Recoupable-com/Recoup-API

Length of output: 118


🏁 Script executed:

cat -n lib/agentTemplates/listAgentTemplatesForUser.ts

Repository: Recoupable-com/Recoup-API

Length of output: 1048


🏁 Script executed:

find lib/supabase -type d -name "agent_templates"

Repository: Recoupable-com/Recoup-API

Length of output: 51


🏁 Script executed:

find lib/supabase -type f -name "*[Aa]gent*" 2>/dev/null

Repository: Recoupable-com/Recoup-API

Length of output: 51


🏁 Script executed:

ls -la lib/supabase/ | head -20

Repository: Recoupable-com/Recoup-API

Length of output: 1294


🏁 Script executed:

find lib/supabase -type f -name "*.ts" | head -20

Repository: Recoupable-com/Recoup-API

Length of output: 1157


🏁 Script executed:

find lib/supabase -type d -name "*agent*" -o -type d -name "*template*"

Repository: Recoupable-com/Recoup-API

Length of output: 51


🏁 Script executed:

ls -la lib/ | grep -i agent

Repository: Recoupable-com/Recoup-API

Length of output: 253


🏁 Script executed:

find lib/supabase -type d | grep -i template

Repository: Recoupable-com/Recoup-API

Length of output: 51


🏁 Script executed:

find lib/supabase -type d -name "*agent*"

Repository: Recoupable-com/Recoup-API

Length of output: 51


🏁 Script executed:

ls -la lib/agentTemplates/

Repository: Recoupable-com/Recoup-API

Length of output: 865


🏁 Script executed:

ls -la lib/supabase/ | grep -i agent

Repository: Recoupable-com/Recoup-API

Length of output: 51


🏁 Script executed:

find lib/supabase -type f \( -name "*.ts" -o -name "*.tsx" \) -path "*/agent_templates/*"

Repository: Recoupable-com/Recoup-API

Length of output: 51


🏁 Script executed:

ls -d lib/supabase/*/ | sort

Repository: Recoupable-com/Recoup-API

Length of output: 790


🏁 Script executed:

rg "agent_templates" lib/supabase --type ts --type tsx -l

Repository: Recoupable-com/Recoup-API

Length of output: 96


🏁 Script executed:

rg "agent_templates" lib/supabase --type ts -l

Repository: Recoupable-com/Recoup-API

Length of output: 51


🏁 Script executed:

rg "serverClient" lib/agentTemplates --type ts

Repository: Recoupable-com/Recoup-API

Length of output: 477


🏁 Script executed:

cat -n lib/agentTemplates/getUserTemplateFavorites.ts

Repository: Recoupable-com/Recoup-API

Length of output: 665


Move database access to lib/supabase/ directory.

This file imports @/lib/supabase/serverClient outside of the lib/supabase/ directory, violating the architectural boundary. Create a helper function in lib/supabase/agent_templates/ to handle this query and import from there instead.

🤖 Prompt for AI Agents
In `@lib/agentTemplates/listAgentTemplatesForUser.ts` at line 1, The code
currently imports the DB client directly from "@/lib/supabase/serverClient" in
listAgentTemplatesForUser.ts; move that DB access into a new helper exported
from the lib/supabase/agent_templates/ namespace (e.g., create and export
getAgentTemplatesForUser(userId: string) in lib/supabase/agent_templates) that
imports serverClient internally and performs the same query against the
agent_templates table, then change listAgentTemplatesForUser.ts to import and
call getAgentTemplatesForUser instead of importing serverClient directly.

Comment on lines 36 to 41
const { data: agents } = await supabase
.from("agents")
.select("*, agent_status(*, social:socials(*))")
.in("id", agentIds);

if (!agents) return [];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent error on second query could mask database issues.

The second Supabase query ignores errors entirely. Unlike the first query which logs and returns early on error, this silently returns an empty array, potentially masking legitimate database failures.

🐛 Proposed fix
-  const { data: agents } = await supabase
+  const { data: agents, error: agentsError } = await supabase
     .from("agents")
     .select("*, agent_status(*, social:socials(*))")
     .in("id", agentIds);
 
+  if (agentsError) {
+    console.error("Error fetching agents:", agentsError);
+    return [];
+  }
+
   if (!agents) return [];
🤖 Prompt for AI Agents
In `@lib/artistAgents/getArtistAgents.ts` around lines 36 - 41, The second
Supabase query in getArtistAgents (the call that does
supabase.from("agents").select(...).in("id", agentIds)) currently only
destructures data as agents and returns [] on falsy data, which can hide DB
errors; change the call to destructure both data and error (e.g., { data:
agents, error }) and mirror the first query's handling by logging the error
(using the same logger used earlier) and returning early or throwing as
appropriate instead of silently returning [], so that database errors are
surfaced and diagnosable.

Comment on lines 27 to 38
return [];
}

if (!data) return [];

const agentIds = [...new Set(data.map(ele => ele.agent.id))];

const { data: agents } = await supabase
.from("agents")
.select("*, agent_status(*, social:socials(*))")
.in("id", agentIds);

Copy link

Choose a reason for hiding this comment

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

Suggested change
return [];
}
if (!data) return [];
const agentIds = [...new Set(data.map(ele => ele.agent.id))];
const { data: agents } = await supabase
.from("agents")
.select("*, agent_status(*, social:socials(*))")
.in("id", agentIds);
throw error;
}
if (!data) return [];
const agentIds = [...new Set(data.map(ele => ele.agent.id))];
const { data: agents, error: agentError } = await supabase
.from("agents")
.select("*, agent_status(*, social:socials(*))")
.in("id", agentIds);
if (agentError) {
console.error("Error fetching agents:", agentError);
throw agentError;
}

Database errors are silently converted to empty arrays instead of being thrown, which masks failures and returns HTTP 200 with empty results instead of HTTP 500 errors. This deviates from the error-handling pattern used elsewhere in the codebase (e.g., getAgentTemplateSharesByTemplateIds.ts).

View Details

Analysis

Database errors silently converted to empty arrays in getArtistAgents()

What fails: getArtistAgents() in lib/artistAgents/getArtistAgents.ts returns empty arrays when Supabase queries fail, instead of throwing errors. This causes the handler to return HTTP 200 with status: "success" instead of HTTP 500 error.

How to reproduce: When getArtistAgents() is called and the first Supabase query fails:

  • Line 25-27: if (error) { console.error(...); return []; } - error is silently converted to empty array
  • The handler receives [] and returns HTTP 200 with empty results

When the second Supabase query fails:

  • Line 38: Only data is destructured, error is completely ignored
  • If the query fails, agents is undefined and the check if (!agents) return [] treats it as "no data"
  • The handler receives [] and returns HTTP 200

Result: Database failures are indistinguishable from legitimate empty results. HTTP 200 with empty agents is returned even when database is down.

Expected: Per the pattern established by similar functions (listAgentTemplatesForUser, getSharedTemplatesForUser, getArtistsByOrganization) and the handler's error handling design (which has a try-catch expecting errors to be thrown), database errors should be thrown so the handler can catch them and return HTTP 500.

Fix implemented:

  • Line 27: Changed return [] to throw error when first query fails
  • Line 38: Added error: agentError to destructuring in second query
  • Line 43-46: Added error check for second query with throw agentError

This aligns error handling with the codebase pattern and the handler's try-catch design. The existing test at line 173-182 of getArtistAgentsHandler.test.ts expects these errors to be thrown.

Comment on lines +171 to +184
describe("auth mechanism enforcement", () => {
it("returns 401 when no auth is provided", async () => {
const request = createMockNoAuthRequest(["social-1"]);
const response = await getArtistAgentsHandler(request);
const json = await response.json();

expect(response.status).toBe(401);
expect(json.status).toBe("error");
expect(json.message).toBe(
"Exactly one of x-api-key or Authorization must be provided",
);
expect(getArtistAgents).not.toHaveBeenCalled();
});
});
Copy link

Choose a reason for hiding this comment

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

Missing test case for the authentication requirement that "exactly one of x-api-key or Authorization must be provided." The handler enforces this but the test coverage is incomplete.

View Details
📝 Patch Details
diff --git a/lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts b/lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts
index 4db4532..b252577 100644
--- a/lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts
+++ b/lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts
@@ -71,6 +71,28 @@ function createMockNoAuthRequest(socialIds: string[] = []): NextRequest {
   } as unknown as NextRequest;
 }
 
+/**
+ * Creates a mock request with both API key and Bearer token auth
+ */
+function createMockBothAuthRequest(
+  socialIds: string[] = [],
+  apiKey = "test-api-key",
+  token = "test-bearer-token",
+): NextRequest {
+  const url = new URL("http://localhost/api/artist-agents");
+  socialIds.forEach((id) => url.searchParams.append("socialId", id));
+  return {
+    url: url.toString(),
+    headers: {
+      get: (name: string) => {
+        if (name === "x-api-key") return apiKey;
+        if (name === "authorization") return `Bearer ${token}`;
+        return null;
+      },
+    },
+  } as unknown as NextRequest;
+}
+
 describe("getArtistAgentsHandler", () => {
   beforeEach(() => {
     vi.clearAllMocks();
@@ -181,6 +203,21 @@ describe("getArtistAgentsHandler", () => {
       );
       expect(getArtistAgents).not.toHaveBeenCalled();
     });
+
+    it("returns 401 when both auth mechanisms are provided", async () => {
+      const request = createMockBothAuthRequest(["social-1"]);
+      const response = await getArtistAgentsHandler(request);
+      const json = await response.json();
+
+      expect(response.status).toBe(401);
+      expect(json.status).toBe("error");
+      expect(json.message).toBe(
+        "Exactly one of x-api-key or Authorization must be provided",
+      );
+      expect(getApiKeyAccountId).not.toHaveBeenCalled();
+      expect(getAuthenticatedAccountId).not.toHaveBeenCalled();
+      expect(getArtistAgents).not.toHaveBeenCalled();
+    });
   });
 
   describe("error handling", () => {

Analysis

Missing test case for authentication requirement - both auth mechanisms provided

What fails: Test coverage is incomplete for the authentication requirement stated in the PR description: "Returns 401 if both auth mechanisms provided." The getArtistAgentsHandler correctly implements this enforcement at line 19-26, but the test suite did not verify this specific scenario.

How to reproduce:

# Run the test suite before the fix
pnpm test -- lib/artistAgents/__tests__/getArtistAgentsHandler.test.ts

# Count test cases in "auth mechanism enforcement" describe block:
# Before fix: 1 test case (no auth provided)
# After fix: 2 test cases (no auth provided + both auth provided)

Result: The handler correctly rejects requests with both x-api-key and authorization headers simultaneously by returning HTTP 401 with message "Exactly one of x-api-key or Authorization must be provided", but there was no test verifying this behavior.

Expected: All critical security/validation requirements should have corresponding test cases. The implementation correctly enforces "exactly one of x-api-key or Authorization must be provided" (line 19 in getArtistAgentsHandler.ts: if ((hasApiKey && hasAuth) || (!hasApiKey && !hasAuth))), and this behavior must be tested.

Fix: Added createMockBothAuthRequest() helper function and a new test case "returns 401 when both auth mechanisms are provided" in the "auth mechanism enforcement" describe block that verifies:

  • HTTP 401 response status
  • Error message matches the requirement
  • Authentication functions are not called
  • Handler returns early without processing the request

Test suite now has complete coverage: no auth (401) + API key auth (200 success, 401 invalid) + Bearer token auth (200 success, 401 invalid) + both auth mechanisms (401).

- Create toggleAgentTemplateFavoriteHandler with Bearer token auth
- Add addAgentTemplateFavorite and removeAgentTemplateFavorite helpers
- Add 8 unit tests for the new handler

Co-Authored-By: Claude Opus 4.5 <[email protected]>
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: 2

🤖 Fix all issues with AI agents
In `@lib/agentTemplates/addAgentTemplateFavorite.ts`:
- Around line 1-24: The addAgentTemplateFavorite function currently imports
supabase directly and performs the insert; refactor by creating a helper in the
central supabase module (e.g., export an insertAgentTemplateFavorite or
upsertAgentTemplateFavorite helper that wraps
supabase.from("agent_template_favorites").insert(...).select(...)) and then
update addAgentTemplateFavorite to call that helper instead of importing
supabase directly; preserve the unique-violation handling (ignore error.code ===
"23505") and keep the function signature and returned { success: true }
behavior.

In `@lib/agentTemplates/removeAgentTemplateFavorite.ts`:
- Around line 1-19: The removeAgentTemplateFavorite function directly imports
supabase; move the DB delete into a new helper under lib/supabase named
deleteAgentTemplateFavorites (matching the table agent_template_favorites).
Implement the actual supabase .delete() logic inside
deleteAgentTemplateFavorites (accepting templateId and userId and returning any
error/ok), then update removeAgentTemplateFavorite to call that helper, handle
the returned error (throw on failure) and return { success: true } on success;
reference the existing function name removeAgentTemplateFavorite and the table
agent_template_favorites when locating code to change.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between add518d and 69fca2d.

📒 Files selected for processing (6)
  • app/api/agent-templates/favorites/route.ts
  • lib/agentTemplates/__tests__/toggleAgentTemplateFavoriteHandler.test.ts
  • lib/agentTemplates/addAgentTemplateFavorite.ts
  • lib/agentTemplates/removeAgentTemplateFavorite.ts
  • lib/agentTemplates/toggleAgentTemplateFavoriteHandler.ts
  • lib/artistAgents/getArtistAgents.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/artistAgents/getArtistAgents.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Apply Single Responsibility Principle (SRP) - one exported function per file
Apply DRY principle (Don't Repeat Yourself) - extract shared logic into reusable utilities

Files:

  • lib/agentTemplates/toggleAgentTemplateFavoriteHandler.ts
  • lib/agentTemplates/__tests__/toggleAgentTemplateFavoriteHandler.test.ts
  • app/api/agent-templates/favorites/route.ts
  • lib/agentTemplates/removeAgentTemplateFavorite.ts
  • lib/agentTemplates/addAgentTemplateFavorite.ts
!(lib/supabase)/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

NEVER import @/lib/supabase/serverClient outside of lib/supabase/ directory

Files:

  • lib/agentTemplates/toggleAgentTemplateFavoriteHandler.ts
  • lib/agentTemplates/__tests__/toggleAgentTemplateFavoriteHandler.test.ts
  • app/api/agent-templates/favorites/route.ts
  • lib/agentTemplates/removeAgentTemplateFavorite.ts
  • lib/agentTemplates/addAgentTemplateFavorite.ts
app/api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

app/api/**/*.{ts,tsx}: All API routes should have JSDoc comments
All API endpoints should use a validate function for input parsing with Zod for schema validation

Files:

  • app/api/agent-templates/favorites/route.ts
🧠 Learnings (1)
📚 Learning: 2026-01-14T22:12:03.883Z
Learnt from: CR
Repo: Recoupable-com/Recoup-API PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-14T22:12:03.883Z
Learning: Applies to lib/supabase/**/*.{ts,tsx} : Supabase delete functions should be named `delete[TableName].ts`

Applied to files:

  • lib/agentTemplates/removeAgentTemplateFavorite.ts
🧬 Code graph analysis (3)
lib/agentTemplates/toggleAgentTemplateFavoriteHandler.ts (4)
lib/auth/getAuthenticatedAccountId.ts (1)
  • getAuthenticatedAccountId (13-50)
lib/networking/getCorsHeaders.ts (1)
  • getCorsHeaders (6-12)
lib/agentTemplates/addAgentTemplateFavorite.ts (1)
  • addAgentTemplateFavorite (11-27)
lib/agentTemplates/removeAgentTemplateFavorite.ts (1)
  • removeAgentTemplateFavorite (11-26)
lib/agentTemplates/__tests__/toggleAgentTemplateFavoriteHandler.test.ts (4)
lib/auth/getAuthenticatedAccountId.ts (1)
  • getAuthenticatedAccountId (13-50)
lib/agentTemplates/toggleAgentTemplateFavoriteHandler.ts (1)
  • toggleAgentTemplateFavoriteHandler (24-96)
lib/agentTemplates/addAgentTemplateFavorite.ts (1)
  • addAgentTemplateFavorite (11-27)
lib/agentTemplates/removeAgentTemplateFavorite.ts (1)
  • removeAgentTemplateFavorite (11-26)
app/api/agent-templates/favorites/route.ts (2)
lib/networking/getCorsHeaders.ts (1)
  • getCorsHeaders (6-12)
lib/agentTemplates/toggleAgentTemplateFavoriteHandler.ts (1)
  • toggleAgentTemplateFavoriteHandler (24-96)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review
🔇 Additional comments (3)
app/api/agent-templates/favorites/route.ts (1)

11-33: LGTM — clean CORS handling and delegation.

lib/agentTemplates/__tests__/toggleAgentTemplateFavoriteHandler.test.ts (1)

59-179: Good coverage for auth, validation, and toggle paths.

lib/agentTemplates/toggleAgentTemplateFavoriteHandler.ts (1)

36-65: Create a validateToggleFavoritBody.ts validation file instead of manual checks.

The codebase uses dedicated validation files with Zod schemas for all request bodies. Define the schema and validation function in lib/agentTemplates/validateToggleFavoritBody.ts, then use it in the handler.

🔧 Suggested refactor

Create lib/agentTemplates/validateToggleFavoritBody.ts:

+import { NextResponse } from "next/server";
+import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
+import { z } from "zod";
+
+export const toggleFavoritBodySchema = z.object({
+  templateId: z.string().min(1, "templateId is required"),
+  isFavourite: z.boolean(),
+});
+
+export type ToggleFavoritBody = z.infer<typeof toggleFavoritBodySchema>;
+
+export function validateToggleFavoritBody(body: unknown): NextResponse | ToggleFavoritBody {
+  const result = toggleFavoritBodySchema.safeParse(body);
+
+  if (!result.success) {
+    const firstError = result.error.issues[0];
+    return NextResponse.json(
+      {
+        status: "error",
+        missing_fields: firstError.path,
+        error: firstError.message,
+      },
+      {
+        status: 400,
+        headers: getCorsHeaders(),
+      },
+    );
+  }
+
+  return result.data;
+}

Then in toggleAgentTemplateFavoriteHandler.ts:

+import { validateToggleFavoritBody } from "./validateToggleFavoritBody";
@@
-    const body: ToggleFavoriteRequestBody = await request.json();
-    const { templateId, isFavourite } = body;
-
-    // Validate required fields
-    if (!templateId) {
-      return NextResponse.json(
-        {
-          status: "error",
-          message: "Missing templateId",
-        },
-        {
-          status: 400,
-          headers: getCorsHeaders(),
-        },
-      );
-    }
-
-    if (typeof isFavourite !== "boolean") {
-      return NextResponse.json(
-        {
-          status: "error",
-          message: "Missing isFavourite",
-        },
-        {
-          status: 400,
-          headers: getCorsHeaders(),
-        },
-      );
-    }
+    const bodyOrError = await validateToggleFavoritBody(await request.json());
+    if (bodyOrError instanceof NextResponse) {
+      return bodyOrError;
+    }
+    const { templateId, isFavourite } = bodyOrError;

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 1 to 24
import supabase from "@/lib/supabase/serverClient";

/**
* Adds an agent template to a user's favorites.
*
* @param templateId - The ID of the template to favorite
* @param userId - The ID of the user adding the favorite
* @returns An object with success: true
* @throws Error if the database operation fails (except for duplicate entries)
*/
export async function addAgentTemplateFavorite(
templateId: string,
userId: string,
): Promise<{ success: true }> {
const { error } = await supabase
.from("agent_template_favorites")
.insert({ template_id: templateId, user_id: userId })
.select("template_id")
.maybeSingle();

// Ignore unique violation (23505) - user already favorited this template
if (error && error.code !== "23505") {
throw error;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid importing serverClient outside lib/supabase.

This file directly imports the Supabase server client from a non-lib/supabase path. Please move the insert logic into a lib/supabase helper and call that helper here.

🔧 Suggested direction
-import supabase from "@/lib/supabase/serverClient";
+import { insertAgentTemplateFavorite } from "@/lib/supabase/insertAgentTemplateFavorite";
@@
-  const { error } = await supabase
-    .from("agent_template_favorites")
-    .insert({ template_id: templateId, user_id: userId })
-    .select("template_id")
-    .maybeSingle();
-
-  // Ignore unique violation (23505) - user already favorited this template
-  if (error && error.code !== "23505") {
-    throw error;
-  }
+  await insertAgentTemplateFavorite(templateId, userId);

As per coding guidelines.

🤖 Prompt for AI Agents
In `@lib/agentTemplates/addAgentTemplateFavorite.ts` around lines 1 - 24, The
addAgentTemplateFavorite function currently imports supabase directly and
performs the insert; refactor by creating a helper in the central supabase
module (e.g., export an insertAgentTemplateFavorite or
upsertAgentTemplateFavorite helper that wraps
supabase.from("agent_template_favorites").insert(...).select(...)) and then
update addAgentTemplateFavorite to call that helper instead of importing
supabase directly; preserve the unique-violation handling (ignore error.code ===
"23505") and keep the function signature and returned { success: true }
behavior.

Comment on lines +1 to +19
import supabase from "@/lib/supabase/serverClient";

/**
* Removes an agent template from a user's favorites.
*
* @param templateId - The ID of the template to unfavorite
* @param userId - The ID of the user removing the favorite
* @returns An object with success: true
* @throws Error if the database operation fails
*/
export async function removeAgentTemplateFavorite(
templateId: string,
userId: string,
): Promise<{ success: true }> {
const { error } = await supabase
.from("agent_template_favorites")
.delete()
.eq("template_id", templateId)
.eq("user_id", userId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move Supabase access behind lib/supabase helpers.

Importing @/lib/supabase/serverClient from lib/agentTemplates violates the rule that only lib/supabase/** may access the server client directly. Move the delete into a lib/supabase helper and call it here.

🔧 Suggested direction
-import supabase from "@/lib/supabase/serverClient";
+import { deleteAgentTemplateFavorite } from "@/lib/supabase/deleteAgentTemplateFavorite";
@@
-  const { error } = await supabase
-    .from("agent_template_favorites")
-    .delete()
-    .eq("template_id", templateId)
-    .eq("user_id", userId);
-
-  if (error) {
-    throw error;
-  }
+  await deleteAgentTemplateFavorite(templateId, userId);

As per coding guidelines. Based on learnings, delete helpers under lib/supabase should be named delete[TableName].ts.

🤖 Prompt for AI Agents
In `@lib/agentTemplates/removeAgentTemplateFavorite.ts` around lines 1 - 19, The
removeAgentTemplateFavorite function directly imports supabase; move the DB
delete into a new helper under lib/supabase named deleteAgentTemplateFavorites
(matching the table agent_template_favorites). Implement the actual supabase
.delete() logic inside deleteAgentTemplateFavorites (accepting templateId and
userId and returning any error/ok), then update removeAgentTemplateFavorite to
call that helper, handle the returned error (throw on failure) and return {
success: true } on success; reference the existing function name
removeAgentTemplateFavorite and the table agent_template_favorites when locating
code to change.

Comment on lines 32 to 35
const userId = searchParams.get("userId") || accountId;

// Fetch templates accessible to the user
const templates = await getUserAccessibleTemplates(userId);
Copy link

@vercel vercel bot Jan 16, 2026

Choose a reason for hiding this comment

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

Suggested change
const userId = searchParams.get("userId") || accountId;
// Fetch templates accessible to the user
const templates = await getUserAccessibleTemplates(userId);
const userId = searchParams.get("userId");
// Validate that userId (if provided) matches the authenticated user
if (userId && userId !== accountId) {
return NextResponse.json(
{
status: "error",
message: "Forbidden: userId parameter must match the authenticated user",
},
{
status: 403,
headers: getCorsHeaders(),
},
);
}
// Fetch templates accessible to the user
const templates = await getUserAccessibleTemplates(accountId);

Authorization bypass vulnerability: An authenticated user can fetch another user's private agent templates by passing a different userId in the query parameter.

View Details

Analysis

Authorization bypass in getAgentTemplatesHandler allows fetching other users' private templates

What fails: getAgentTemplatesHandler accepts an optional userId query parameter but does not validate it matches the authenticated user, allowing any authenticated user to fetch another user's private agent templates.

How to reproduce:

1. User A authenticates and receives JWT token (accountId = A)
2. User A makes request: GET /api/agent-templates?userId=B
3. Handler extracts userId=B from query parameter
4. Calls getUserAccessibleTemplates(B) which returns all templates where creator=B (including private) plus all public templates
5. User A receives User B's private templates

What happens: Returns HTTP 200 with User B's private templates in the response, including templates marked as is_private: true.

Expected behavior: Should either return HTTP 403 Forbidden (if userId doesn't match authenticated user) or return only public templates and templates explicitly shared with the authenticated user.

Root cause: The handler documentation says "userId: Optional user ID to fetch templates for (must match authenticated user)" but the code does not enforce this constraint. The listAgentTemplatesForUser(userId) function executes .or('creator.eq. which returns all templates where creator equals the specified userId regardless of privacy setting.

Fix: Added validation to reject requests where the provided userId parameter differs from the authenticated user's accountId, returning HTTP 403 Forbidden with appropriate error message.

Add public endpoint for fetching agent creator info (name, image, is_admin)
to be used by the frontend for displaying agent creator attribution.

- Create new /api/agent-creator route with CORS support
- Add getAgentCreatorHandler with proper error handling
- Add ADMIN_EMAILS config for admin detection
- Add 6 unit tests for the handler

Co-Authored-By: Claude Opus 4.5 <[email protected]>
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: 2

🤖 Fix all issues with AI agents
In `@lib/agentCreator/getAgentCreatorHandler.ts`:
- Around line 60-68: In getAgentCreatorHandler's catch block, stop returning the
raw error message and HTTP 400; instead log the caught error (e) internally and
return NextResponse.json({ message: "Internal server error" }, { status: 500,
headers: getCorsHeaders() }) so unexpected DB/internal errors don't leak
details. Keep the existing getCorsHeaders() usage and ensure you still return a
JSON response but with the generic 500 message.
- Around line 19-29: Add a Zod-based validator similar to validateArtistsQuery:
create validateAgentCreatorQuery.ts exporting a Zod schema (e.g.,
agentCreatorQuerySchema = z.object({ creatorId: z.string().min(1) })) and a
function validateAgentCreatorQuery(params: URLSearchParams) that returns parsed
data or throws a ZodError; then import and call validateAgentCreatorQuery in
getAgentCreatorHandler (replace the manual if (!creatorId) check) and on
validation failure return a JSON response matching the project error format {
status: "error", missing_fields: [...], error: "..." } (extract missing fields
from the ZodError) while preserving CORS headers via getCorsHeaders().
🧹 Nitpick comments (2)
lib/admin.ts (1)

1-5: Centralize ADMIN_EMAILS in lib/const.ts.

This looks like a shared constant; the repo convention is to keep shared constants in lib/const.ts. Consider moving it there (and re-exporting if needed) to keep the pattern consistent.

Based on learnings, please align shared constants with lib/const.ts.

app/api/agent-creator/route.ts (1)

11-32: Confirm SRP exception for multi-method route exports.

The SRP guideline says one exported function per file, but this Next.js route exports both OPTIONS and GET. Please confirm an exception for route handlers or update the guideline to acknowledge this Next.js pattern.

As per coding guidelines, please verify the intended rule for route files.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69fca2d and 59c7af1.

📒 Files selected for processing (4)
  • app/api/agent-creator/route.ts
  • lib/admin.ts
  • lib/agentCreator/__tests__/getAgentCreatorHandler.test.ts
  • lib/agentCreator/getAgentCreatorHandler.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Apply Single Responsibility Principle (SRP) - one exported function per file
Apply DRY principle (Don't Repeat Yourself) - extract shared logic into reusable utilities

Files:

  • lib/agentCreator/getAgentCreatorHandler.ts
  • lib/agentCreator/__tests__/getAgentCreatorHandler.test.ts
  • lib/admin.ts
  • app/api/agent-creator/route.ts
!(lib/supabase)/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

NEVER import @/lib/supabase/serverClient outside of lib/supabase/ directory

Files:

  • lib/agentCreator/getAgentCreatorHandler.ts
  • lib/agentCreator/__tests__/getAgentCreatorHandler.test.ts
  • lib/admin.ts
  • app/api/agent-creator/route.ts
app/api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

app/api/**/*.{ts,tsx}: All API routes should have JSDoc comments
All API endpoints should use a validate function for input parsing with Zod for schema validation

Files:

  • app/api/agent-creator/route.ts
🧠 Learnings (2)
📚 Learning: 2026-01-14T22:12:03.883Z
Learnt from: CR
Repo: Recoupable-com/Recoup-API PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-14T22:12:03.883Z
Learning: Applies to lib/const.ts : All shared constants should be defined in `lib/const.ts`

Applied to files:

  • lib/admin.ts
📚 Learning: 2026-01-14T22:12:03.883Z
Learnt from: CR
Repo: Recoupable-com/Recoup-API PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-14T22:12:03.883Z
Learning: Applies to app/api/**/*.{ts,tsx} : All API routes should have JSDoc comments

Applied to files:

  • app/api/agent-creator/route.ts
🧬 Code graph analysis (2)
lib/agentCreator/getAgentCreatorHandler.ts (3)
lib/networking/getCorsHeaders.ts (1)
  • getCorsHeaders (6-12)
lib/supabase/accounts/getAccountWithDetails.ts (1)
  • getAccountWithDetails (23-42)
lib/admin.ts (1)
  • ADMIN_EMAILS (5-5)
lib/agentCreator/__tests__/getAgentCreatorHandler.test.ts (2)
lib/agentCreator/getAgentCreatorHandler.ts (1)
  • getAgentCreatorHandler (18-70)
lib/supabase/accounts/getAccountWithDetails.ts (1)
  • getAccountWithDetails (23-42)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review
🔇 Additional comments (1)
lib/agentCreator/__tests__/getAgentCreatorHandler.test.ts (1)

1-149: Good coverage for validation, success, admin flag, and error paths.

Tests are clear and hit the main behavior branches.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +19 to +29
const creatorId = request.nextUrl.searchParams.get("creatorId");

if (!creatorId) {
return NextResponse.json(
{ message: "Missing creatorId" },
{
status: 400,
headers: getCorsHeaders(),
},
);
}
Copy link

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

🏁 Script executed:

# Search for existing validate functions and Zod patterns
rg -n "validate\(" --type ts --type tsx | head -20

Repository: Recoupable-com/Recoup-API

Length of output: 96


🏁 Script executed:

# Search for Zod usage patterns
rg -n "\\bz\\.|\\bzod\\b" --type ts --type tsx | head -30

Repository: Recoupable-com/Recoup-API

Length of output: 96


🏁 Script executed:

# Check the actual file to understand its purpose and context
cat -n lib/agentCreator/getAgentCreatorHandler.ts

Repository: Recoupable-com/Recoup-API

Length of output: 2389


🏁 Script executed:

# Check if there's an app/api directory and how API endpoints are structured
find . -path "./app/api/*" -name "*.ts" -o -path "./app/api/*" -name "*.tsx" | head -10

Repository: Recoupable-com/Recoup-API

Length of output: 385


🏁 Script executed:

# Search for validate functions without specifying file type
rg -n "validate\(" --max-count 20

Repository: Recoupable-com/Recoup-API

Length of output: 51


🏁 Script executed:

# Search for Zod usage
rg -n "from \"zod\"|import.*zod" | head -20

Repository: Recoupable-com/Recoup-API

Length of output: 1462


🏁 Script executed:

# Search for where getAgentCreatorHandler is imported/used
rg -n "getAgentCreatorHandler"

Repository: Recoupable-com/Recoup-API

Length of output: 1372


🏁 Script executed:

# Check app/api route patterns to see how validation is done there
fd "route.ts" app/api | head -5 | xargs -I {} head -40 {}

Repository: Recoupable-com/Recoup-API

Length of output: 4918


🏁 Script executed:

# Check one of the validate function patterns to understand the convention
cat -n lib/artists/validateArtistsQuery.ts

Repository: Recoupable-com/Recoup-API

Length of output: 1633


🏁 Script executed:

# Check one of the handler files that uses validation
rg -l "validateArtistsQuery" --type ts | head -1 | xargs cat -n

Repository: Recoupable-com/Recoup-API

Length of output: 1633


🏁 Script executed:

# See if any validation files exist for agentCreator
fd "validate" lib/agentCreator/

Repository: Recoupable-com/Recoup-API

Length of output: 51


Create a validation function for creatorId parameter following the codebase pattern.

The endpoint must validate the required creatorId query parameter using Zod. Create lib/agentCreator/validateAgentCreatorQuery.ts with a schema and validation function (following the pattern used in lib/artists/validateArtistsQuery.ts), then call it from the handler. Update the error response format to match the codebase standard: { status: "error", missing_fields: [...], error: "..." }.

🤖 Prompt for AI Agents
In `@lib/agentCreator/getAgentCreatorHandler.ts` around lines 19 - 29, Add a
Zod-based validator similar to validateArtistsQuery: create
validateAgentCreatorQuery.ts exporting a Zod schema (e.g.,
agentCreatorQuerySchema = z.object({ creatorId: z.string().min(1) })) and a
function validateAgentCreatorQuery(params: URLSearchParams) that returns parsed
data or throws a ZodError; then import and call validateAgentCreatorQuery in
getAgentCreatorHandler (replace the manual if (!creatorId) check) and on
validation failure return a JSON response matching the project error format {
status: "error", missing_fields: [...], error: "..." } (extract missing fields
from the ZodError) while preserving CORS headers via getCorsHeaders().

Comment on lines +60 to +68
} catch (e) {
const message = e instanceof Error ? e.message : "failed";
return NextResponse.json(
{ message },
{
status: 400,
headers: getCorsHeaders(),
},
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Return 500 with a generic message on unexpected errors.

A thrown DB error is currently surfaced as a 400 with the raw message. This should be a 500, and the message should be generic to avoid leaking internals.

Suggested fix
-  } catch (e) {
-    const message = e instanceof Error ? e.message : "failed";
-    return NextResponse.json(
-      { message },
-      {
-        status: 400,
-        headers: getCorsHeaders(),
-      },
-    );
-  }
+  } catch (_e) {
+    return NextResponse.json(
+      { message: "Internal server error" },
+      {
+        status: 500,
+        headers: getCorsHeaders(),
+      },
+    );
+  }
🤖 Prompt for AI Agents
In `@lib/agentCreator/getAgentCreatorHandler.ts` around lines 60 - 68, In
getAgentCreatorHandler's catch block, stop returning the raw error message and
HTTP 400; instead log the caught error (e) internally and return
NextResponse.json({ message: "Internal server error" }, { status: 500, headers:
getCorsHeaders() }) so unexpected DB/internal errors don't leak details. Keep
the existing getCorsHeaders() usage and ensure you still return a JSON response
but with the generic 500 message.

- Create lib/supabase/agent_template_shares/selectAgentTemplateShares.ts
- Add 5 unit tests for the new function
- Update getSharedEmailsForTemplates.ts to use the new function
- Remove orphaned AgentTemplateShare type from types.ts
- Delete old lib/agentTemplates/getAgentTemplateSharesByTemplateIds.ts

This follows the established Supabase directory pattern of
lib/supabase/{table_name}/select{TableName}.ts

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactor to supabase lib

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactor to supabase lib.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactor to supabase lib.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactor to supabase lib.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactor to supabase lib.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactor to a supabase lib.

lib/admin.ts Outdated
Copy link
Contributor Author

Choose a reason for hiding this comment

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

delete this file and move the contents to the existing const file.

Comment on lines 29 to 65
const accountIdOrError = await getAuthenticatedAccountId(request);
if (accountIdOrError instanceof NextResponse) {
return accountIdOrError;
}

const accountId = accountIdOrError;

// Parse request body
const body: ToggleFavoriteRequestBody = await request.json();
const { templateId, isFavourite } = body;

// Validate required fields
if (!templateId) {
return NextResponse.json(
{
status: "error",
message: "Missing templateId",
},
{
status: 400,
headers: getCorsHeaders(),
},
);
}

if (typeof isFavourite !== "boolean") {
return NextResponse.json(
{
status: "error",
message: "Missing isFavourite",
},
{
status: 400,
headers: getCorsHeaders(),
},
);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

SRP - extract validation logic

…count

Renamed file and function to use 'Account' instead of 'User' for consistency
with codebase naming conventions. This is a pure rename refactor - no logic
changes were made.

- Created getSharedTemplatesForAccount.ts with renamed function
- Updated getUserAccessibleTemplates.ts to use new function
- Added 6 unit tests for the renamed function

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Replace direct Supabase query with selectAgentTemplateShares function:
- Extend selectAgentTemplateShares with userId and includeTemplates params
- Add AgentTemplateShareWithTemplate interface for typed template joins
- Update getSharedTemplatesForAccount to use the new function
- Add 6 new unit tests for userId and includeTemplates functionality

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Renamed function and file for consistency with codebase 'Account' naming
conventions. Updated handler and tests to use the new function.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Create selectAgentTemplateFavorites in lib/supabase/agent_template_favorites/
- Add 6 unit tests for new function
- Update getAccountTemplates to use new function
- Delete old getUserTemplateFavorites.ts

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@sweetmantech sweetmantech changed the base branch from main to test January 16, 2026 19:37
Move addAgentTemplateFavorite to lib/supabase/agent_template_favorites/insertAgentTemplateFavorite.ts to follow the Supabase directory pattern established in the codebase. Added 5 unit tests for the new function.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
created_at: string;
}

export interface AgentTemplateShareWithTemplate extends AgentTemplateShare {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

SRP - use supabase schema

}

const BASE_SELECT = "template_id, user_id, created_at";
const TEMPLATE_JOIN_SELECT = `
Copy link
Contributor Author

Choose a reason for hiding this comment

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

use * instead of defining specific fields.

}: {
templateId: string;
userId: string;
}): Promise<{ success: true }> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

return the full row.

userId,
}: {
userId?: string;
}): Promise<Set<string>> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

return the full rows and reference supabase schema

- Use select('*') instead of explicit column lists
- Replace custom interfaces with Tables<'agent_template_shares'>
- Replace AgentTemplateShareWithTemplate with database types
- Update tests to verify new select patterns

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Use select('*') instead of explicit column selection
- Replace custom TemplateFavorite interface with Tables<'agent_template_favorites'>
- Return full records instead of Set<string>
- Update getAccountTemplates to convert records to Set for backward compatibility

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…oper types (DRY)

Applied DRY principle:
- Changed from select('template_id') to select('*')
- Return type changed from { success: true } to Tables<'agent_template_favorites'> | null
- Export AgentTemplateFavorite type alias for consistency
- Returns null for duplicate entries instead of success object

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…(SRP)

- Create validateToggleAgentTemplateFavoriteBody.ts with zod schema
- Export ToggleAgentTemplateFavoriteBody type
- Update handler to use new validation function
- Add 12 unit tests for validation function
- Update handler tests for new error message format

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Consolidate admin configuration into shared constants file:
- Move ADMIN_EMAILS constant to lib/const.ts
- Update imports in getAgentCreatorHandler
- Add unit tests for ADMIN_EMAILS in const.test.ts
- Delete now-empty lib/admin.ts

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Create selectAgentTemplates.ts in lib/supabase/agent_templates/
- Use Tables<'agent_templates'> type and select('*') for DRY
- Update getAccountTemplates.ts to import from new location
- Add 7 unit tests for selectAgentTemplates
- Delete lib/agentTemplates/listAgentTemplatesForUser.ts

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…se (SRP)

Follow SRP by extracting 2 Supabase queries into separate files:
- selectAgentStatusBySocialIds: agent_status query with joined agents
- selectAgentsWithStatusAndSocials: agents query with joined agent_status and socials

Updated getArtistAgents.ts to use the new functions. Added 21 unit tests
(6 for selectAgentStatusBySocialIds, 6 for selectAgentsWithStatusAndSocials,
9 for getArtistAgents).

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add new public endpoint to fetch available AI models from Vercel AI Gateway.
- Create getAiModelsHandler with 4 unit tests
- Returns models filtered to exclude embed models
- Public endpoint, no auth required
- Follows established handler/route pattern

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants