Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8caa0c9
fix: make organizationId validation lenient instead of rejecting
sweetmantech Jan 16, 2026
fc4039f
feat: add Bearer token auth support to createChatHandler
sweetmantech Jan 16, 2026
0b726f3
feat: add /api/artist-agents endpoint for fetching artist agents
sweetmantech Jan 16, 2026
d1e5675
revert: rollback validateChatRequest lenient org behavior
sweetmantech Jan 16, 2026
0fbf0a4
revert: rollback createChatHandler changes
sweetmantech Jan 16, 2026
add518d
feat: add GET /api/agent-templates endpoint with Bearer auth
sweetmantech Jan 16, 2026
08f3ab7
chore: delete duplicate getSocialPlatformByLink.ts in artistAgents
sweetmantech Jan 16, 2026
69fca2d
feat: add POST /api/agent-templates/favorites endpoint
sweetmantech Jan 16, 2026
59c7af1
feat: add GET /api/agent-creator endpoint
sweetmantech Jan 16, 2026
ba22bba
refactor: move getAgentTemplateSharesByTemplateIds to lib/supabase
sweetmantech Jan 16, 2026
a240172
refactor: rename getSharedTemplatesForUser to getSharedTemplatesForAc…
sweetmantech Jan 16, 2026
6e48b24
refactor: use selectAgentTemplateShares in getSharedTemplatesForAccount
sweetmantech Jan 16, 2026
7371e58
refactor: rename getUserAccessibleTemplates to getAccountTemplates
sweetmantech Jan 16, 2026
fa1b91d
refactor: move getUserTemplateFavorites to lib/supabase
sweetmantech Jan 16, 2026
b818e58
refactor: move addAgentTemplateFavorite to lib/supabase
sweetmantech Jan 16, 2026
ede2f06
refactor: simplify selectAgentTemplateShares query and types (DRY)
sweetmantech Jan 16, 2026
3608c28
refactor: simplify selectAgentTemplateFavorites query and types (DRY)
sweetmantech Jan 16, 2026
d72faf5
refactor: update insertAgentTemplateFavorite to return record with pr…
sweetmantech Jan 16, 2026
65ac49d
refactor: extract validation from toggleAgentTemplateFavoriteHandler …
sweetmantech Jan 16, 2026
c6f1dfb
refactor: move ADMIN_EMAILS from lib/admin.ts to lib/const.ts
sweetmantech Jan 16, 2026
ed3759c
refactor: move listAgentTemplatesForUser to lib/supabase
sweetmantech Jan 16, 2026
b118128
refactor: extract Supabase queries from getArtistAgents to lib/supaba…
sweetmantech Jan 16, 2026
283132e
feat: add GET /api/ai/models endpoint
sweetmantech Jan 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/api/agent-creator/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getAgentCreatorHandler } from "@/lib/agentCreator/getAgentCreatorHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A NextResponse with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* GET /api/agent-creator
*
* Fetch agent creator information for display in the UI.
*
* This is a public endpoint that does not require authentication.
*
* Query parameters:
* - creatorId: Required - The account ID of the agent creator
*
* @param request - The request object
* @returns A NextResponse with creator info (name, image, is_admin) or an error
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
return getAgentCreatorHandler(request);
}
34 changes: 34 additions & 0 deletions app/api/agent-templates/favorites/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { toggleAgentTemplateFavoriteHandler } from "@/lib/agentTemplates/toggleAgentTemplateFavoriteHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A NextResponse with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* POST /api/agent-templates/favorites
*
* Toggle a template's favorite status for the authenticated user.
*
* Authentication: Authorization Bearer token required.
*
* Request body:
* - templateId: string - The template ID to favorite/unfavorite
* - isFavourite: boolean - true to add, false to remove
*
* @param request - The request object
* @returns A NextResponse with success or an error
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
return toggleAgentTemplateFavoriteHandler(request);
}
33 changes: 33 additions & 0 deletions app/api/agent-templates/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getAgentTemplatesHandler } from "@/lib/agentTemplates/getAgentTemplatesHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A NextResponse with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* GET /api/agent-templates
*
* Fetch agent templates accessible to the authenticated user.
*
* Authentication: Authorization Bearer token required.
*
* Query parameters:
* - userId: Optional user ID (defaults to authenticated user)
*
* @param request - The request object
* @returns A NextResponse with the templates array or an error
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
return getAgentTemplatesHandler(request);
}
34 changes: 34 additions & 0 deletions app/api/ai/models/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getAiModelsHandler } from "@/lib/aiModels/getAiModelsHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A NextResponse with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* GET /api/ai/models
*
* Fetch the list of available AI models from the Vercel AI Gateway.
*
* This is a public endpoint that does not require authentication.
* It returns models suitable for chat, filtering out embed models.
*
* @returns A NextResponse with { models: GatewayLanguageModelEntry[] } or an error
*/
export async function GET(): Promise<NextResponse> {
return getAiModelsHandler();
}

// Disable caching to always serve the latest model list.
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export const revalidate = 0;
34 changes: 34 additions & 0 deletions app/api/artist-agents/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { getArtistAgentsHandler } from "@/lib/artistAgents/getArtistAgentsHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A NextResponse with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}
Comment on lines +11 to +16
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.


/**
* GET /api/artist-agents
*
* Fetch artist agents by social IDs.
*
* Authentication: x-api-key header OR Authorization Bearer token required.
* Exactly one authentication mechanism must be provided.
*
* Query parameters:
* - socialId: One or more social IDs (can be repeated, e.g., ?socialId=123&socialId=456)
*
* @param request - The request object
* @returns A NextResponse with the agents array or an error
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
return getArtistAgentsHandler(request);
}
21 changes: 21 additions & 0 deletions lib/__tests__/const.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect } from "vitest";
import { ADMIN_EMAILS } from "@/lib/const";

describe("lib/const", () => {
describe("ADMIN_EMAILS", () => {
it("should export ADMIN_EMAILS as an array", () => {
expect(Array.isArray(ADMIN_EMAILS)).toBe(true);
});

it("should contain at least one admin email", () => {
expect(ADMIN_EMAILS.length).toBeGreaterThan(0);
});

it("should contain valid email strings", () => {
for (const email of ADMIN_EMAILS) {
expect(typeof email).toBe("string");
expect(email).toMatch(/@/);
}
});
});
});
153 changes: 153 additions & 0 deletions lib/agentCreator/__tests__/getAgentCreatorHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";
import { getAgentCreatorHandler } from "../getAgentCreatorHandler";

import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails";
import { ADMIN_EMAILS } from "@/lib/const";

// Mock dependencies
vi.mock("@/lib/supabase/accounts/getAccountWithDetails", () => ({
getAccountWithDetails: vi.fn(),
}));

vi.mock("@/lib/const", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/const")>();
return {
...actual,
ADMIN_EMAILS: ["admin@example.com"],
};
});

vi.mock("@/lib/networking/getCorsHeaders", () => ({
getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })),
}));

/**
* Creates a mock request with creatorId query param
*
* @param creatorId
*/
function createMockRequest(creatorId?: string): NextRequest {
const url = new URL("http://localhost/api/agent-creator");
if (creatorId) {
url.searchParams.set("creatorId", creatorId);
}
return {
url: url.toString(),
nextUrl: {
searchParams: url.searchParams,
},
} as unknown as NextRequest;
}

describe("getAgentCreatorHandler", () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe("validation", () => {
it("returns 400 when creatorId is missing", async () => {
const request = createMockRequest();
const response = await getAgentCreatorHandler(request);
const json = await response.json();

expect(response.status).toBe(400);
expect(json.message).toBe("Missing creatorId");
});
});

describe("successful responses", () => {
it("returns creator info with name and image", async () => {
vi.mocked(getAccountWithDetails).mockResolvedValue({
id: "creator-123",
name: "Test Creator",
created_at: "2024-01-01T00:00:00Z",
organization_id: null,
image: "https://example.com/avatar.jpg",
email: "testcreator@example.com",
wallet: null,
account_id: "creator-123",
});

const request = createMockRequest("creator-123");
const response = await getAgentCreatorHandler(request);
const json = await response.json();

expect(response.status).toBe(200);
expect(json.creator).toEqual({
name: "Test Creator",
image: "https://example.com/avatar.jpg",
is_admin: false,
});
expect(getAccountWithDetails).toHaveBeenCalledWith("creator-123");
});

it("returns is_admin true for admin emails", async () => {
vi.mocked(getAccountWithDetails).mockResolvedValue({
id: "admin-123",
name: "Admin User",
created_at: "2024-01-01T00:00:00Z",
organization_id: null,
image: "https://example.com/admin.jpg",
email: "admin@example.com", // This matches ADMIN_EMAILS mock
wallet: null,
account_id: "admin-123",
});

const request = createMockRequest("admin-123");
const response = await getAgentCreatorHandler(request);
const json = await response.json();

expect(response.status).toBe(200);
expect(json.creator.is_admin).toBe(true);
});

it("returns null values when account has no name or image", async () => {
vi.mocked(getAccountWithDetails).mockResolvedValue({
id: "creator-456",
name: null,
created_at: "2024-01-01T00:00:00Z",
organization_id: null,
image: undefined,
email: "noinfo@example.com",
wallet: null,
account_id: "creator-456",
});

const request = createMockRequest("creator-456");
const response = await getAgentCreatorHandler(request);
const json = await response.json();

expect(response.status).toBe(200);
expect(json.creator).toEqual({
name: null,
image: null,
is_admin: false,
});
});
});

describe("error handling", () => {
it("returns 404 when creator not found", async () => {
vi.mocked(getAccountWithDetails).mockResolvedValue(null);

const request = createMockRequest("nonexistent-123");
const response = await getAgentCreatorHandler(request);
const json = await response.json();

expect(response.status).toBe(404);
expect(json.message).toBe("Creator not found");
});

it("returns 400 when database query throws", async () => {
vi.mocked(getAccountWithDetails).mockRejectedValue(new Error("Database error"));

const request = createMockRequest("creator-123");
const response = await getAgentCreatorHandler(request);
const json = await response.json();

expect(response.status).toBe(400);
expect(json.message).toBe("Database error");
});
});
});
Loading