From 3d241089ee42c18ff14edfa46901cadec69e2be6 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 8 Jan 2026 10:56:51 -0500 Subject: [PATCH 1/5] feat: make room_id required in send_email tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The room_id parameter is now required to ensure all outbound emails include the chat link footer, enabling email thread continuity. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- lib/emails/sendEmailSchema.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/emails/sendEmailSchema.ts b/lib/emails/sendEmailSchema.ts index 7411dd4d..42c0920e 100644 --- a/lib/emails/sendEmailSchema.ts +++ b/lib/emails/sendEmailSchema.ts @@ -28,8 +28,7 @@ export const sendEmailSchema = z.object({ .string() .describe( "Room ID to include in the email footer link. Use the active_conversation_id from context.", - ) - .optional(), + ), }); export type SendEmailInput = z.infer; From daa25c966679ee66b84c858b0782cc5e706c2a4a Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 9 Jan 2026 09:44:44 -0500 Subject: [PATCH 2/5] Add POST /api/chats endpoint for creating chat rooms - Add createChatHandler in lib/chats/ - Add POST route at app/api/chats/ - Account ID inferred from API key - Optional artistId and chatId params - chatId auto-generated if not provided Co-Authored-By: Claude Opus 4.5 --- app/api/chats/route.ts | 35 +++++++++++++++++ lib/chats/createChatHandler.ts | 71 ++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 app/api/chats/route.ts create mode 100644 lib/chats/createChatHandler.ts diff --git a/app/api/chats/route.ts b/app/api/chats/route.ts new file mode 100644 index 00000000..4e311045 --- /dev/null +++ b/app/api/chats/route.ts @@ -0,0 +1,35 @@ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { createChatHandler } from "@/lib/chats/createChatHandler"; + +/** + * 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/chats + * + * Create a new chat room. + * + * Authentication: x-api-key header required. + * The account ID is inferred from the API key. + * + * Optional body parameters: + * - artistId: UUID of the artist account the chat is associated with + * - chatId: UUID for the new chat (auto-generated if not provided) + * + * @param request - The request object + * @returns A NextResponse with the created chat or an error + */ +export async function POST(request: NextRequest): Promise { + return createChatHandler(request); +} diff --git a/lib/chats/createChatHandler.ts b/lib/chats/createChatHandler.ts new file mode 100644 index 00000000..b8f08d81 --- /dev/null +++ b/lib/chats/createChatHandler.ts @@ -0,0 +1,71 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; +import { insertRoom } from "@/lib/supabase/rooms/insertRoom"; +import { generateUUID } from "@/lib/uuid/generateUUID"; + +interface CreateChatBody { + artistId?: string; + chatId?: string; +} + +/** + * Handler for creating a new chat room. + * + * Requires authentication via x-api-key header. + * The account ID is inferred from the API key. + * + * @param request - The NextRequest object + * @returns A NextResponse with the created chat or an error + */ +export async function createChatHandler(request: NextRequest): Promise { + try { + const accountIdOrError = await getApiKeyAccountId(request); + if (accountIdOrError instanceof NextResponse) { + return accountIdOrError; + } + + const accountId = accountIdOrError; + + let body: CreateChatBody = {}; + try { + body = await request.json(); + } catch { + // Empty body is valid - all params are optional + } + + const { artistId, chatId } = body; + + const roomId = chatId || generateUUID(); + + const chat = await insertRoom({ + id: roomId, + account_id: accountId, + artist_id: artistId || null, + topic: null, + }); + + return NextResponse.json( + { + status: "success", + chat, + }, + { + status: 200, + headers: getCorsHeaders(), + }, + ); + } catch (error) { + console.error("[ERROR] createChatHandler:", error); + return NextResponse.json( + { + status: "error", + message: "Failed to create chat", + }, + { + status: 500, + headers: getCorsHeaders(), + }, + ); + } +} From f8baa3a59710cafbd5d204757fd69fb9bc164d97 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 9 Jan 2026 09:47:01 -0500 Subject: [PATCH 3/5] Add input validation pattern to CLAUDE.md and chats endpoint - Document validate function pattern using Zod in CLAUDE.md - Add validateCreateChatBody.ts for POST /api/chats - Update createChatHandler to use the validate function Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 65 +++++++++++++++++++++++++++++ lib/chats/createChatHandler.ts | 15 +++---- lib/chats/validateCreateChatBody.ts | 37 ++++++++++++++++ 3 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 lib/chats/validateCreateChatBody.ts diff --git a/CLAUDE.md b/CLAUDE.md index 42327a58..20dc37f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,71 @@ pnpm format:check # Check formatting - All API routes should have JSDoc comments - Run `pnpm lint` before committing +## Input Validation + +All API endpoints should use a **validate function** for input parsing. Use Zod for schema validation. + +### Pattern + +Create a `validateBody.ts` or `validateQuery.ts` file: + +```typescript +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { z } from "zod"; + +// Define the schema +export const createExampleBodySchema = z.object({ + name: z.string({ message: "name is required" }).min(1, "name cannot be empty"), + id: z.string().uuid("id must be a valid UUID").optional(), +}); + +// Export the inferred type +export type CreateExampleBody = z.infer; + +/** + * Validates request body for POST /api/example. + * + * @param body - The request body + * @returns A NextResponse with an error if validation fails, or the validated body if validation passes. + */ +export function validateCreateExampleBody(body: unknown): NextResponse | CreateExampleBody { + const result = createExampleBodySchema.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; +} +``` + +### Usage in Handler + +```typescript +const validated = validateCreateExampleBody(body); +if (validated instanceof NextResponse) { + return validated; +} +// validated is now typed as CreateExampleBody +``` + +### Naming Convention + +- `validateBody.ts` - For POST/PUT request bodies +- `validateQuery.ts` - For GET query parameters + ## Constants (`lib/const.ts`) All shared constants live in `lib/const.ts`: diff --git a/lib/chats/createChatHandler.ts b/lib/chats/createChatHandler.ts index b8f08d81..a9c0d6f8 100644 --- a/lib/chats/createChatHandler.ts +++ b/lib/chats/createChatHandler.ts @@ -3,11 +3,7 @@ import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; import { insertRoom } from "@/lib/supabase/rooms/insertRoom"; import { generateUUID } from "@/lib/uuid/generateUUID"; - -interface CreateChatBody { - artistId?: string; - chatId?: string; -} +import { validateCreateChatBody } from "@/lib/chats/validateCreateChatBody"; /** * Handler for creating a new chat room. @@ -27,14 +23,19 @@ export async function createChatHandler(request: NextRequest): Promise; + +/** + * Validates request body for POST /api/chats. + * + * @param body - The request body + * @returns A NextResponse with an error if validation fails, or the validated body if validation passes. + */ +export function validateCreateChatBody(body: unknown): NextResponse | CreateChatBody { + const result = createChatBodySchema.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; +} From 2f03d7fca2a89a6331ad3cf8bb7cba0565c2c721 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 9 Jan 2026 09:52:22 -0500 Subject: [PATCH 4/5] Remove try-catch around request.json(), follow existing pattern Co-Authored-By: Claude Opus 4.5 --- lib/chats/createChatHandler.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/chats/createChatHandler.ts b/lib/chats/createChatHandler.ts index a9c0d6f8..ff0a301c 100644 --- a/lib/chats/createChatHandler.ts +++ b/lib/chats/createChatHandler.ts @@ -23,12 +23,7 @@ export async function createChatHandler(request: NextRequest): Promise Date: Fri, 9 Jan 2026 09:56:21 -0500 Subject: [PATCH 5/5] Add safeParseJson utility for optional request bodies - Create safeParseJson helper that returns {} if body is empty/invalid - Use in createChatHandler so body is not required - All params are optional, so empty body should work Co-Authored-By: Claude Opus 4.5 --- lib/chats/createChatHandler.ts | 3 ++- lib/networking/safeParseJson.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 lib/networking/safeParseJson.ts diff --git a/lib/chats/createChatHandler.ts b/lib/chats/createChatHandler.ts index ff0a301c..ad1c592f 100644 --- a/lib/chats/createChatHandler.ts +++ b/lib/chats/createChatHandler.ts @@ -4,6 +4,7 @@ import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; import { insertRoom } from "@/lib/supabase/rooms/insertRoom"; import { generateUUID } from "@/lib/uuid/generateUUID"; import { validateCreateChatBody } from "@/lib/chats/validateCreateChatBody"; +import { safeParseJson } from "@/lib/networking/safeParseJson"; /** * Handler for creating a new chat room. @@ -23,7 +24,7 @@ export async function createChatHandler(request: NextRequest): Promise { + try { + return await request.json(); + } catch { + return {}; + } +}