Audited every documented request/response schema against the real
Zod validators and NextResponse.json shapes across all route
families, then fixed drift: missing batch-cook/tags/photos fields on
recipes, wrong comment/rating shapes, meal-plan entry shape (day vs
dayOfWeek, nullable recipeId), shopping-list/pantry response fields,
a phantom GET /webhooks/{id} with no backing route, and numerous
missing 400/401/429 response codes.
Also fixes a real bug found during the audit: the webhook-redelivery
route returned {error: object} instead of the app-wide {error:
string} convention on validation failure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
## 0.27.1 — 2026-07-14 12:05
### Fixed
- API Reference (`/docs`) schemas corrected against the actual code across the whole API — recipes (batch-cook fields, tags, photos were undocumented), comments, ratings, meal plans, shopping lists, pantry, feed, and several missing error responses. A webhook-redelivery route also returned a malformed error body — now matches the rest of the API's `{ error: string }` convention.
// Mirrors CHANGELOG.md at the repo root — update both together.
exportconstAPP_VERSION="0.27.0";
exportconstAPP_VERSION="0.27.1";
exporttypeChangelogEntry={
version: string;
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
};
exportconstCHANGELOG: ChangelogEntry[]=[
{
version:"0.27.1",
date:"2026-07-14 12:05",
fixed:[
"API Reference (`/docs`) schemas corrected against the actual code across the whole API — recipes (batch-cook fields, tags, photos were undocumented), comments, ratings, meal plans, shopping lists, pantry, feed, and several missing error responses. A webhook-redelivery route also returned a malformed error body — now matches the rest of the API's `{ error: string }` convention.",
registry.registerPath({method:"get",path:"/api/v1/recipes",summary:"List your own recipes",description:"Returns a lean summary per recipe — no ingredients/steps/photos/batchDishes (use GET /recipes/{id} for full detail).",security,request:{query: z.object({limit: z.coerce.number().max(100).default(20),offset: z.coerce.number().default(0),visibility: z.enum(["private","unlisted","public"]).optional()})},responses:{200:{description:"Paginated",content:{"application/json":{schema: PaginatedRecipes}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/recipes/bulk/export",summary:"Export several recipes as one Markdown document",security,request:{body:{content:{"application/json":{schema: BulkIdsRef}},required: true}},responses:{200:{description:"Markdown",content:{"application/json":{schema: z.object({markdown: z.string()})}}},404:{description:"None of the ids belong to you",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/recipes/{id}/fork",summary:"Fork (or duplicate your own) a recipe",description:"Rate-limited: 20 req/min. Source must be your own, public, or unlisted.",security,request:{params: idParam},responses:{201:{description:"New recipe id",content:{"application/json":{schema: z.object({id: z.string()})}}},403:{description:"Recipe limit reached for your tier",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/recipes/bulk/export",summary:"Export several recipes as one Markdown document",security,request:{body:{content:{"application/json":{schema: BulkIdsRef}},required: true}},responses:{200:{description:"Markdown",content:{"application/json":{schema: z.object({markdown: z.string()})}}},400:{description:"Invalid request",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"None of the ids belong to you",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/recipes/{id}/fork",summary:"Fork (or duplicate your own) a recipe",description:"Rate-limited: 20 req/min. Source must be your own, public, or unlisted.",security,request:{params: idParam},responses:{201:{description:"New recipe id",content:{"application/json":{schema: z.object({id: z.string()})}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},403:{description:"Recipe limit reached for your tier",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/recipes/{id}/cooked",summary:"Log that you cooked this recipe",description:"Optionally deducts matching ingredients from your pantry.",security,request:{params: idParam,body:{content:{"application/json":{schema: z.object({servings: z.number().int().min(1).max(1000).optional(),notes: z.string().max(2000).optional(),deductFromPantry: z.boolean().default(true),batchDishId: z.string().optional()})}}}},responses:{201:{description:"Logged",content:{"application/json":{schema: z.object({logged: z.boolean()})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/nutrition",summary:"Get cached nutrition estimate",security,request:{params: idParam},responses:{200:{description:"Nutrition data or null",content:{"application/json":{schema: z.object({nutrition: z.record(z.string(),z.unknown()).nullable()})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/recipes/{id}/nutrition",summary:"Compute a fresh AI nutrition estimate (author only)",description:"Rate-limited: 10 req/min. Consumes AI quota.",security,request:{params: idParam},responses:{200:{description:"Nutrition data",content:{"application/json":{schema: z.object({nutrition: z.record(z.string(),z.unknown())})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"Rate limited or AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/notes",summary:"Get your own private note on a recipe",security,request:{params: idParam},responses:{200:{description:"Note or null",content:{"application/json":{schema: z.object({note: RecipeNoteRef})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"put",path:"/api/v1/recipes/{id}/notes",summary:"Set (or clear, with empty content) your private note",security,request:{params: idParam,body:{content:{"application/json":{schema: z.object({content: z.string().max(5000)})}},required: true}},responses:{200:{description:"Saved",content:{"application/json":{schema: z.object({note: RecipeNoteRef})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/reviews",summary:"List reviews with text or a photo",security,request:{params: idParam},responses:{200:{description:"Reviews",content:{"application/json":{schema: z.object({data: z.array(RecipeReviewRef)})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/versions",summary:"List version history (owner only)",security,request:{params: idParam},responses:{200:{description:"Versions, newest first",content:{"application/json":{schema: z.array(RecipeVersionSummaryRef)}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/versions/{versionId}",summary:"Get a specific version snapshot (owner only)",security,request:{params: z.object({id: z.string(),versionId: z.string()})},responses:{200:{description:"Snapshot",content:{"application/json":{schema: z.record(z.string(),z.unknown())}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/recipes/{id}/versions/{versionId}",summary:"Restore a version (owner only)",security,request:{params: z.object({id: z.string(),versionId: z.string()}),body:{content:{"application/json":{schema: z.object({action: z.literal("restore")})}},required: true}},responses:{200:{description:"Restored",content:{"application/json":{schema: z.object({ok: z.boolean()})}}},400:{description:"Invalid action",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/nutrition",summary:"Get cached nutrition estimate",security,request:{params: idParam},responses:{200:{description:"Nutrition data or null",content:{"application/json":{schema: z.object({nutrition: z.record(z.string(),z.unknown()).nullable()})}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/recipes/{id}/nutrition",summary:"Compute a fresh AI nutrition estimate (author only)",description:"Rate-limited: 10 req/min. Consumes AI quota.",security,request:{params: idParam},responses:{200:{description:"Nutrition data",content:{"application/json":{schema: z.object({nutrition: z.record(z.string(),z.unknown())})}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"Rate limited or AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/notes",summary:"Get your own private note on a recipe",security,request:{params: idParam},responses:{200:{description:"Note or null",content:{"application/json":{schema: z.object({note: RecipeNoteRef})}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"put",path:"/api/v1/recipes/{id}/notes",summary:"Set (or clear, with empty content) your private note",security,request:{params: idParam,body:{content:{"application/json":{schema: z.object({content: z.string().max(5000)})}},required: true}},responses:{200:{description:"Saved",content:{"application/json":{schema: z.object({note: RecipeNoteRef})}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/reviews",summary:"List reviews with text or a photo (capped at 50)",security,request:{params: idParam},responses:{200:{description:"Reviews",content:{"application/json":{schema: z.object({data: z.array(RecipeReviewRef)})}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/versions",summary:"List version history (owner only)",security,request:{params: idParam},responses:{200:{description:"Versions, newest first",content:{"application/json":{schema: z.array(RecipeVersionSummaryRef)}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/versions/{versionId}",summary:"Get a specific version snapshot (owner only)",security,request:{params: z.object({id: z.string(),versionId: z.string()})},responses:{200:{description:"Snapshot",content:{"application/json":{schema: z.record(z.string(),z.unknown())}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/recipes/{id}/versions/{versionId}",summary:"Restore a version (owner only)",security,request:{params: z.object({id: z.string(),versionId: z.string()}),body:{content:{"application/json":{schema: z.object({action: z.literal("restore")})}},required: true}},responses:{200:{description:"Restored",content:{"application/json":{schema: z.object({ok: z.boolean()})}}},400:{description:"Invalid action",content:{"application/json":{schema: ApiErrorRef}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/recipes/{id}/comments/{commentId}/reactions",summary:"Get reaction counts (+ your own reactions, if authenticated)",security:[],request:{params: z.object({id: z.string(),commentId: z.string()})},responses:{200:{description:"Counts",content:{"application/json":{schema: z.object({counts: z.record(z.string(),z.number()),userReactions: z.array(z.string())})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/recipes/{id}/comments/{commentId}/reactions",summary:"Toggle a reaction on a comment",description:"Rate-limited: 60 req/min.",security,request:{params: z.object({id: z.string(),commentId: z.string()}),body:{content:{"application/json":{schema: z.object({type:z.enum(["like","love","laugh","wow","sad","fire"])})}},required: true}},responses:{200:{description:"Updated counts",content:{"application/json":{schema: z.object({counts: z.record(z.string(),z.number()),added: z.boolean()})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
@@ -183,14 +295,25 @@ export function generateOpenApiSpec(): object {
registry.registerPath({method:"post",path:"/api/v1/ai/translate/{id}",summary:"Translate a recipe (author only) and save as a new draft",description:"Consumes AI quota.",security,request:{params: idParam,body:{content:{"application/json":{schema: z.object({targetLanguage: z.string().min(2).max(50),provider: z.enum(["openai","anthropic","openrouter","ollama"]).optional(),model: z.string().optional()})}},required: true}},responses:{200:{description:"New recipe id",content:{"application/json":{schema: z.object({id: z.string()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/ai/variations/{id}",summary:"Suggest creative variations on a recipe (author only)",description:"Consumes AI quota.",security,request:{params: idParam,body:{content:{"application/json":{schema: z.object({count: z.number().int().min(1).max(5).default(3),directions: z.string().max(500).optional(),provider: z.enum(["openai","anthropic","openrouter","ollama"]).optional(),model: z.string().optional()})}}}},responses:{200:{description:"Variations",content:{"application/json":{schema: z.object({variations: z.array(z.record(z.string(),z.unknown()))})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/ai/batch-cook/generate",summary:"Generate a batch-cooking plan (multiple dishes) as a new recipe",description:"Rate-limited: 3 req/min. Consumes AI quota.",security,request:{body:{content:{"application/json":{schema: z.object({dinners: z.number().int().min(0).max(6).default(4),lunches: z.number().int().min(0).max(6).default(0),servings: z.number().int().min(1).max(12).default(4),dietaryPrefs: z.string().max(200).optional(),difficulty: z.enum(["easy","medium","hard"]).optional(),description: z.string().max(500).optional()})}},required: true}},responses:{200:{description:"New recipe id",content:{"application/json":{schema: z.object({id: z.string()})}}},400:{description:"Validation error or no dinners/lunches chosen",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"Rate limited or AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/ai/meal-plan/generate",summary:"Generate a week's meal plan (creates a draft recipe per entry)",description:"Rate-limited: 3 req/min. Consumes AI quota. Charges your tier's recipe limit for each generated entry (refunded if the limit is exceeded).",security,request:{body:{content:{"application/json":{schema: z.object({weekStart: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),dietaryPrefs: z.string().max(200).optional(),servings: z.number().int().min(1).max(20).default(2),days: z.array(z.enum(["mon","tue","wed","thu","fri","sat","sun"])).min(1).max(7).optional(),usePantry: z.boolean().default(false),pantryMode: z.boolean().default(false),difficulty: z.enum(["easy","medium","hard"]).optional(),targetNutritionGoals: z.boolean().default(false)})}},required: true}},responses:{200:{description:"Created entries",content:{"application/json":{schema: z.object({weekStart: z.string(),entries: z.array(z.object({id: z.string(),day: z.string(),mealType: z.string(),recipeId: z.string(),recipeTitle: z.string()}))})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},403:{description:"Recipe limit reached for your tier",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"Rate limited or AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/ai/meal-plan/generate",summary:"Generate a week's meal plan (creates a draft recipe per entry)",description:"Rate-limited: 3 req/min. Consumes AI quota. Charges your tier's recipe limit for each generated entry (refunded if the limit is exceeded).",security,request:{body:{content:{"application/json":{schema: z.object({weekStart: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),dietaryPrefs: z.string().max(200).optional(),servings: z.number().int().min(1).max(20).default(2),days: z.array(z.enum(["mon","tue","wed","thu","fri","sat","sun"])).min(1).max(7).default(["mon","tue","wed","thu","fri","sat","sun"]),usePantry: z.boolean().default(false),pantryMode: z.boolean().default(false),difficulty: z.enum(["easy","medium","hard"]).optional(),targetNutritionGoals: z.boolean().default(false)})}},required: true}},responses:{200:{description:"Created entries",content:{"application/json":{schema: z.object({weekStart: z.string(),entries: z.array(z.object({id: z.string(),day: z.string(),mealType: z.string(),recipeId: z.string(),recipeTitle: z.string()}))})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},403:{description:"Recipe limit reached for your tier",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"Rate limited or AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/ai/recipe-ideas",summary:"Generate 6 diverse recipe idea cards (not saved)",description:"Rate-limited: 10 req/min. Consumes AI quota.",security,request:{body:{content:{"application/json":{schema: z.object({prompt: z.string().max(300).optional()})}}}},responses:{200:{description:"Six recipe ideas",content:{"application/json":{schema: z.array(z.object({title: z.string(),description: z.string(),tags: z.array(z.string()).max(4),difficulty: z.enum(["easy","medium","hard"]),totalMins: z.number().int().optional()}))}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"Rate limited or AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/ai/cooking-chat",summary:"Ask the general (not recipe-specific) cooking assistant a question",description:"Rate-limited: 30 req/min. Consumes AI quota. Returns a single JSON response (not streamed); persists the exchange to chat history.",security,request:{body:{content:{"application/json":{schema: z.object({question: z.string().min(1).max(500)})}},required: true}},responses:{200:{description:"Answer",content:{"application/json":{schema: z.object({answer: z.string()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"Rate limited or AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/ai/recipe-chat",summary:"Ask a question about a specific recipe (author only)",description:"Rate-limited: 30 req/min. Consumes AI quota. Returns a single JSON response (not streamed); persists the exchange to chat history.",security,request:{body:{content:{"application/json":{schema: z.object({recipeId: z.string().uuid(),question: z.string().min(1).max(500)})}},required: true}},responses:{200:{description:"Answer",content:{"application/json":{schema: z.object({answer: z.string()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Recipe not found",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"Rate limited or AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/ai/chat-history",summary:"List your AI chat history (general assistant and/or per-recipe)",description:"Omit both recipeId and scope to search across everything; pass scope=general to restrict to the homepage assistant (no recipe).",security,request:{query: z.object({recipeId: z.string().uuid().optional(),scope: z.enum(["general"]).optional(),q: z.string().max(200).optional(),limit: z.coerce.number().int().min(1).max(100).default(30)})},responses:{200:{description:"Messages",content:{"application/json":{schema: z.object({data: z.array(AiChatMessageRef)})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/ai/chat-history",summary:"Clear your AI chat history (general assistant and/or per-recipe)",security,request:{query: z.object({recipeId: z.string().uuid().optional(),scope: z.enum(["general"]).optional()})},responses:{200:{description:"Cleared",content:{"application/json":{schema: z.object({ok: z.boolean()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/collections/{id}",summary:"Delete a collection (owner only)",security,request:{params: idParam},responses:{204:{description:"Deleted"},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/collections/{id}/fork",summary:"Fork a public (or your own) collection",security,request:{params: idParam},responses:{201:{description:"New collection id",content:{"application/json":{schema: z.object({id: z.string()})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/collections/{id}/favorite",summary:"Favorite a collection",security,request:{params: idParam},responses:{200:{description:"Favorited",content:{"application/json":{schema: z.object({favorited: z.boolean()})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/collections/{id}/favorite",summary:"Unfavorite a collection",security,request:{params: idParam},responses:{200:{description:"Unfavorited",content:{"application/json":{schema: z.object({favorited: z.boolean()})}}}}});
registry.registerPath({method:"get",path:"/api/v1/collections/{id}/members",summary:"List collection members (owner only)",security,request:{params: idParam},responses:{200:{description:"Members",content:{"application/json":{schema: z.array(CollectionMemberRef)}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/collections/{id}/members",summary:"Invite a member by email or userId (owner only)",security,request:{params: idParam,body:{content:{"application/json":{schema: InviteMemberRef}},required: true}},responses:{201:{description:"Member id",content:{"application/json":{schema: z.object({id: z.string()})}}},404:{description:"User not found",content:{"application/json":{schema: ApiErrorRef}}},409:{description:"Already a member",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/collections/{id}/members",summary:"Remove a member (owner or the member themselves)",security,request:{params: idParam,query: z.object({memberId: z.string()})},responses:{204:{description:"Removed"},403:{description:"Forbidden",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/collections/{id}/members",summary:"Invite a member by email or userId (owner only)",security,request:{params: idParam,body:{content:{"application/json":{schema: InviteMemberRef}},required: true}},responses:{201:{description:"Member id",content:{"application/json":{schema: z.object({id: z.string()})}}},400:{description:"Validation error or cannot invite yourself",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"User not found",content:{"application/json":{schema: ApiErrorRef}}},409:{description:"Already a member",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/collections/{id}/members",summary:"Remove a member (owner or the member themselves)",security,request:{params: idParam,query: z.object({memberId: z.string()})},responses:{204:{description:"Removed"},400:{description:"memberId required",content:{"application/json":{schema: ApiErrorRef}}},403:{description:"Forbidden",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/meal-plans/{weekStart}",summary:"Get your own meal plan for a week",security,request:{params: weekStartParam},responses:{200:{description:"Meal plan with entries",content:{"application/json":{schema: MealPlanRef}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/meal-plans/{weekStart}",summary:"Create the meal plan for a week (idempotent)",security,request:{params: weekStartParam},responses:{200:{description:"Already existed",content:{"application/json":{schema: z.object({id: z.string(),weekStart: z.string()})}}},201:{description:"Created",content:{"application/json":{schema: z.object({id: z.string(),weekStart: z.string()})}}}}});
@@ -246,23 +369,23 @@ export function generateOpenApiSpec(): object {
registry.registerPath({method:"get",path:"/api/v1/meal-plans/{weekStart}/entries",summary:"List entries for a week (lean — polled for live updates)",security,request:{params: weekStartParam},responses:{200:{description:"Entries",content:{"application/json":{schema: z.object({entries: z.array(MealPlanEntryLeanRef)})}}}}});
registry.registerPath({method:"post",path:"/api/v1/meal-plans/{weekStart}/entries",summary:"Add/replace an entry for a day+mealType slot",description:"Creates the week's plan if it doesn't exist yet. Replaces any existing entry in the same slot.",security,request:{params: weekStartParam,body:{content:{"application/json":{schema: CreateMealPlanEntryRef}},required: true}},responses:{201:{description:"Entry id",content:{"application/json":{schema: z.object({id: z.string()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Recipe or dish not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/meal-plans/{weekStart}/entries/{entryId}",summary:"Delete one entry",security,request:{params: z.object({weekStart: z.string(),entryId: z.string()})},responses:{204:{description:"Deleted"},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/meal-plans/{weekStart}/entries/bulk",summary:"Bulk-delete entries (clear a day or the whole week)",security,request:{params: weekStartParam,body:{content:{"application/json":{schema: BulkIdsRef}},required: true}},responses:{200:{description:"OK",content:{"application/json":{schema: z.object({ok: z.boolean()})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/meal-plans/{weekStart}/entries/bulk",summary:"Bulk-delete entries (clear a day or the whole week)",security,request:{params: weekStartParam,body:{content:{"application/json":{schema: BulkIdsRef}},required: true}},responses:{200:{description:"OK",content:{"application/json":{schema: z.object({ok: z.boolean()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/meal-plans/{weekStart}/members",summary:"List collaborators on a week's plan (owner only)",security,request:{params: weekStartParam},responses:{200:{description:"Members",content:{"application/json":{schema: z.array(MealPlanMemberRef)}}}}});
registry.registerPath({method:"post",path:"/api/v1/meal-plans/{weekStart}/members",summary:"Invite a collaborator by email or userId (owner only)",description:"Creates the week's plan if it doesn't exist yet.",security,request:{params: weekStartParam,body:{content:{"application/json":{schema: InviteMemberRef}},required: true}},responses:{201:{description:"Member id",content:{"application/json":{schema: z.object({id: z.string(),mealPlanId: z.string()})}}},404:{description:"User not found",content:{"application/json":{schema: ApiErrorRef}}},409:{description:"Already a member",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/meal-plans/{weekStart}/members",summary:"Remove a collaborator (owner or the member themselves)",security,request:{params: weekStartParam,query: z.object({memberId: z.string()})},responses:{204:{description:"Removed"},403:{description:"Forbidden",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/meal-plans/{weekStart}/members",summary:"Invite a collaborator by email or userId (owner only)",description:"Creates the week's plan if it doesn't exist yet.",security,request:{params: weekStartParam,body:{content:{"application/json":{schema: InviteMemberRef}},required: true}},responses:{201:{description:"Member id",content:{"application/json":{schema: z.object({id: z.string(),mealPlanId: z.string()})}}},400:{description:"Cannot invite yourself",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"User not found",content:{"application/json":{schema: ApiErrorRef}}},409:{description:"Already a member",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/meal-plans/{weekStart}/members",summary:"Remove a collaborator (owner or the member themselves)",security,request:{params: weekStartParam,query: z.object({memberId: z.string()})},responses:{204:{description:"Removed"},400:{description:"memberId required",content:{"application/json":{schema: ApiErrorRef}}},403:{description:"Forbidden",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/meal-plans/{weekStart}/nutrition",summary:"Weekly nutrition summary vs. your daily goals",security,request:{params: weekStartParam},responses:{200:{description:"Summary",content:{"application/json":{schema: WeeklyNutritionRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/meal-plans/{weekStart}/export/ics",summary:"Download the week's plan as a .ics calendar file",security,request:{params: weekStartParam},responses:{200:{description:"iCalendar file",content:{"text/calendar":{schema: z.string()}}}}});
registry.registerPath({method:"get",path:"/api/v1/meal-plans/{weekStart}/export/ics",summary:"Download the week's plan as a .ics calendar file",security,request:{params: weekStartParam},responses:{200:{description:"iCalendar file",content:{"text/calendar":{schema: z.string()}}},400:{description:"Invalid weekStart",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/meal-plans/shared/{mealPlanId}",summary:"View a meal plan you're a collaborator on",security,request:{params: z.object({mealPlanId: z.string()})},responses:{200:{description:"Plan with entries, your role, and the owner's name",content:{"application/json":{schema: z.record(z.string(),z.unknown())}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/meal-plans/shared/{mealPlanId}/entries",summary:"List entries on a shared plan (lean — polled for live updates)",security,request:{params: z.object({mealPlanId: z.string()})},responses:{200:{description:"Entries",content:{"application/json":{schema: z.object({entries: z.array(MealPlanEntryLeanRef)})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/meal-plans/shared/{mealPlanId}/entries",summary:"Add/replace an entry on a shared plan (editor role required)",security,request:{params: z.object({mealPlanId: z.string()}),body:{content:{"application/json":{schema: CreateMealPlanEntryRef.omit({batchDishId: true})}},required: true}},responses:{201:{description:"Entry id",content:{"application/json":{schema: z.object({id: z.string()})}}},403:{description:"Forbidden (viewer role)",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found / recipe not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/meal-plans/shared/{mealPlanId}/entries",summary:"Delete one or more entries on a shared plan (editor role required)",security,request:{params: z.object({mealPlanId: z.string()}),query: z.object({entryId: z.string().optional(),ids: z.string().optional().describe("comma-separated entry ids, for clearing a day/week")})},responses:{204:{description:"Deleted"},403:{description:"Forbidden",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/meal-plans/shared/{mealPlanId}/entries",summary:"Add/replace an entry on a shared plan (editor role required)",security,request:{params: z.object({mealPlanId: z.string()}),body:{content:{"application/json":{schema: CreateMealPlanEntryRef.omit({batchDishId: true})}},required: true}},responses:{201:{description:"Entry id",content:{"application/json":{schema: z.object({id: z.string()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},403:{description:"Forbidden (viewer role)",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found / recipe not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/meal-plans/shared/{mealPlanId}/entries",summary:"Delete one or more entries on a shared plan (editor role required)",security,request:{params: z.object({mealPlanId: z.string()}),query: z.object({entryId: z.string().optional(),ids: z.string().optional().describe("comma-separated entry ids, for clearing a day/week")})},responses:{204:{description:"Deleted"},400:{description:"entryId or ids required",content:{"application/json":{schema: ApiErrorRef}}},403:{description:"Forbidden",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/pantry/{id}",summary:"Delete a pantry item",security,request:{params: idParam},responses:{204:{description:"Deleted"},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/pantry/bulk",summary:"Add several pantry items at once, merging quantities into existing matching items",security,request:{body:{content:{"application/json":{schema: z.object({items: z.array(z.object({rawName: z.string().min(1).max(200),quantity: z.string().optional(),unit: z.string().max(50).optional()})).min(1).max(100)})}},required: true}},responses:{200:{description:"OK",content:{"application/json":{schema: z.object({ok: z.boolean()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/pantry/scan/barcode",summary:"Look up a barcode via Open Food Facts to prefill a pantry item",security,request:{body:{content:{"application/json":{schema: z.object({barcode: z.string().trim().min(4).max(32).regex(/^[0-9]+$/)})}},required: true}},responses:{200:{description:"Lookup result",content:{"application/json":{schema: z.object({found: z.boolean(),rawName: z.string().optional(),quantity: z.string().optional(),unit: z.string().optional()})}}},502:{description:"Lookup service unavailable",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/pantry/scan/photo",summary:"Identify pantry items from a photo using AI vision",description:"Consumes AI quota.",security,request:{body:{content:{"application/json":{schema: z.object({imageBase64: z.string().max(14_000_000),mimeType: z.enum(["image/jpeg","image/png","image/webp"])})}},required: true}},responses:{200:{description:"Detected items",content:{"application/json":{schema: z.record(z.string(),z.unknown())}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/pantry/scan/barcode",summary:"Look up a barcode via Open Food Facts to prefill a pantry item",description:"Rate-limited: 20 req/min.",security,request:{body:{content:{"application/json":{schema: z.object({barcode: z.string().trim().min(4).max(32).regex(/^[0-9]+$/)})}},required: true}},responses:{200:{description:"Lookup result",content:{"application/json":{schema: z.object({found: z.boolean(),rawName: z.string().optional(),quantity: z.string().optional(),unit: z.string().optional()})}}},429:{description:"Rate limited",content:{"application/json":{schema: ApiErrorRef}}},502:{description:"Lookup service unavailable",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/pantry/scan/photo",summary:"Identify pantry items from a photo using AI vision",description:"Rate-limited: 10 req/min. Consumes AI quota.",security,request:{body:{content:{"application/json":{schema: z.object({imageBase64: z.string().max(14_000_000),mimeType: z.enum(["image/jpeg","image/png","image/webp"])})}},required: true}},responses:{200:{description:"Detected items",content:{"application/json":{schema: z.record(z.string(),z.unknown())}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"Rate limited or AI quota exhausted",content:{"application/json":{schema: ApiErrorRef}}}}});
// --- Users: profile, export, prefs, nutrition, social ---
@@ -340,13 +463,13 @@ export function generateOpenApiSpec(): object {
registry.registerPath({method:"delete",path:"/api/v1/users/{username}/follow",summary:"Unfollow a user",security,request:{params: usernameParam},responses:{200:{description:"Not following",content:{"application/json":{schema: z.object({following: z.boolean()})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/users/{username}/block",summary:"Block a user",description:"Also removes any existing follow relationship in either direction.",security,request:{params: usernameParam},responses:{200:{description:"Blocked",content:{"application/json":{schema: z.object({blocked: z.boolean()})}}},400:{description:"Cannot block yourself",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/users/{username}/block",summary:"Unblock a user",security,request:{params: usernameParam},responses:{200:{description:"Unblocked",content:{"application/json":{schema: z.object({blocked: z.boolean()})}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/shopping-lists",summary:"List shopping lists",description:"Returns all of your lists, unpaginated.",security,responses:{200:{description:"Lists",content:{"application/json":{schema: z.array(ShoppingListRef)}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/shopping-lists/{id}",summary:"Get a shopping list with items",security,request:{params: idParam},responses:{200:{description:"List",content:{"application/json":{schema: ShoppingListRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
@@ -376,12 +500,12 @@ export function generateOpenApiSpec(): object {
registry.registerPath({method:"put",path:"/api/v1/shopping-lists/{id}/items/{itemId}",summary:"Update one item (check/uncheck, rename, re-quantify, move aisle)",description:"No auth required if the list is public-editable.",security,request:{params: z.object({id: z.string(),itemId: z.string()}),body:{content:{"application/json":{schema: UpdateShoppingListItemRef}},required: true}},responses:{200:{description:"Updated",content:{"application/json":{schema: z.object({updated: z.boolean()})}}},403:{description:"Forbidden",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/shopping-lists/{id}/items/{itemId}",summary:"Delete one item",description:"No auth required if the list is public-editable.",security,request:{params: z.object({id: z.string(),itemId: z.string()})},responses:{200:{description:"Deleted",content:{"application/json":{schema: z.object({deleted: z.boolean()})}}},403:{description:"Forbidden",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/shopping-lists/{id}/members",summary:"Invite a collaborator by email or userId (owner only)",security,request:{params: idParam,body:{content:{"application/json":{schema: InviteMemberRef}},required: true}},responses:{201:{description:"Member id",content:{"application/json":{schema: z.object({id: z.string()})}}},404:{description:"User not found",content:{"application/json":{schema: ApiErrorRef}}},409:{description:"Already a member",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/shopping-lists/{id}/members",summary:"Remove a collaborator (owner or the member themselves)",security,request:{params: idParam,query: z.object({memberId: z.string()})},responses:{204:{description:"Removed"},403:{description:"Forbidden",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/shopping-lists/{id}/members",summary:"Invite a collaborator by email or userId (owner only)",security,request:{params: idParam,body:{content:{"application/json":{schema: InviteMemberRef}},required: true}},responses:{201:{description:"Member id",content:{"application/json":{schema: z.object({id: z.string()})}}},400:{description:"Validation error or cannot invite yourself",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"User not found",content:{"application/json":{schema: ApiErrorRef}}},409:{description:"Already a member",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/shopping-lists/{id}/members",summary:"Remove a collaborator (owner or the member themselves)",security,request:{params: idParam,query: z.object({memberId: z.string()})},responses:{204:{description:"Removed"},400:{description:"memberId required",content:{"application/json":{schema: ApiErrorRef}}},403:{description:"Forbidden",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/shopping-lists/{id}/export",summary:"Export items in a normalized grocery-provider format",security,request:{params: idParam},responses:{200:{description:"Export payload",content:{"application/json":{schema: GroceryExportRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/shopping-lists/{id}/export/instacart",summary:"Create an Instacart shopping-list link",description:"501 if Instacart isn't configured server-side.",security,request:{params: idParam},responses:{200:{description:"Link",content:{"application/json":{schema: z.record(z.string(),z.unknown())}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}},501:{description:"Not configured",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/api-keys",summary:"List API keys",security,responses:{200:{description:"Keys (hash never returned)",content:{"application/json":{schema: z.array(ApiKeyRef)}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/api-keys",summary:"Create API key",security,request:{body:{content:{"application/json":{schema: z.object({name: z.string().min(1).max(100),scope: z.enum(["full","read"]).default("full").describe("read-only keys can only make GET requests")})}},required: true}},responses:{201:{description:"Key created — shown once",content:{"application/json":{schema: CreateApiKeyResponseRef}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/api-keys",summary:"Create API key",security,request:{body:{content:{"application/json":{schema: z.object({name: z.string().min(1).max(100),scope: z.enum(["full","read"]).default("full").describe("read-only keys can only make GET requests")})}},required: true}},responses:{201:{description:"Key created — shown once",content:{"application/json":{schema: CreateApiKeyResponseRef}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},403:{description:"API key limit reached (max 10)",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/api-keys/{id}",summary:"Revoke API key",security,request:{params: idParam},responses:{204:{description:"Revoked"},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
// --- AI keys: users' own third-party provider keys (BYOK), used instead of app-wide AI credentials ---
@@ -416,7 +540,6 @@ export function generateOpenApiSpec(): object {
registry.registerPath({method:"get",path:"/api/v1/webhooks",summary:"List your webhooks",description:"The signing secret is never included — it is only ever returned once, at creation time.",security,responses:{200:{description:"Webhooks",content:{"application/json":{schema: z.array(WebhookRef)}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/webhooks",summary:"Create a webhook",description:"Validates the URL against SSRF targets. Generates a signing secret returned once in this response only.",security,request:{body:{content:{"application/json":{schema: CreateWebhookRef}},required: true}},responses:{201:{description:"Created — includes the signing secret (write-once)",content:{"application/json":{schema: CreatedWebhookRef}}},400:{description:"Validation error or blocked URL",content:{"application/json":{schema: ApiErrorRef}}},401:{description:"Unauthorized",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"get",path:"/api/v1/webhooks/{id}",summary:"Get a webhook",security,request:{params: idParam},responses:{200:{description:"Webhook",content:{"application/json":{schema: WebhookRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"patch",path:"/api/v1/webhooks/{id}",summary:"Update a webhook's URL, events, or active state",description:"Validates the URL against SSRF targets if changed.",security,request:{params: idParam,body:{content:{"application/json":{schema: UpdateWebhookRef}},required: true}},responses:{200:{description:"Updated",content:{"application/json":{schema: WebhookRef}}},400:{description:"Validation error, blocked URL, or no fields to update",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"delete",path:"/api/v1/webhooks/{id}",summary:"Delete a webhook",security,request:{params: idParam},responses:{204:{description:"Deleted"},404:{description:"Not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/reports",summary:"Report a recipe, comment, or user for moderation",description:"Rate-limited: 10 req/min.",security,request:{body:{content:{"application/json":{schema: CreateReportRef}},required: true}},responses:{201:{description:"Created",content:{"application/json":{schema: z.object({ok: z.boolean()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Target not found",content:{"application/json":{schema: ApiErrorRef}}}}});
registry.registerPath({method:"post",path:"/api/v1/reports",summary:"Report a recipe, comment, or user for moderation",description:"Rate-limited: 10 req/min.",security,request:{body:{content:{"application/json":{schema: CreateReportRef}},required: true}},responses:{201:{description:"Created",content:{"application/json":{schema: z.object({ok: z.boolean()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Target not found",content:{"application/json":{schema: ApiErrorRef}}},429:{description:"Rate limited",content:{"application/json":{schema: ApiErrorRef}}}}});
// --- Comments: edit & delete ---
registry.registerPath({method:"put",path:"/api/v1/comments/{id}",summary:"Edit your own comment",security,request:{params: idParam,body:{content:{"application/json":{schema: z.object({content: z.string().min(1).max(5000)})}},required: true}},responses:{200:{description:"Updated",content:{"application/json":{schema: z.object({updated: z.boolean()})}}},400:{description:"Validation error",content:{"application/json":{schema: ApiErrorRef}}},404:{description:"Not found or not yours",content:{"application/json":{schema: ApiErrorRef}}}}});
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.