@@ -0,0 +1,141 @@
import {
OpenApiGeneratorV31 ,
OpenAPIRegistry ,
extendZodWithOpenApi ,
} from "@asteasolutions/zod-to-openapi" ;
import { z } from "zod" ;
// Nothing at module level — Next.js evaluates route modules at build time to
// determine static vs dynamic, which would run registry.register() before
// extendZodWithOpenApi patches the Zod prototype. Wrap everything lazily.
let cachedSpec : object | null = null ;
export function generateOpenApiSpec ( ) : object {
if ( cachedSpec ) return cachedSpec ;
extendZodWithOpenApi ( z ) ;
const registry = new OpenAPIRegistry ( ) ;
const security : Array < Record < string , string [ ] > > = [ { SessionCookie : [ ] } , { BearerApiKey : [ ] } ] ;
registry . registerComponent ( "securitySchemes" , "SessionCookie" , { type : "apiKey" , in : "cookie" , name : "better-auth.session_token" } ) ;
registry . registerComponent ( "securitySchemes" , "BearerApiKey" , { type : "http" , scheme : "bearer" , bearerFormat : "ek_..." } ) ;
const DietaryTagsRef = registry . register ( "DietaryTags" , z . object ( {
vegan : z.boolean ( ) . optional ( ) , vegetarian : z.boolean ( ) . optional ( ) ,
glutenFree : z.boolean ( ) . optional ( ) , dairyFree : z.boolean ( ) . optional ( ) ,
nutFree : z.boolean ( ) . optional ( ) , halal : z.boolean ( ) . optional ( ) , kosher : z.boolean ( ) . optional ( ) ,
} ) ) ;
const RecipeIngredientRef = registry . register ( "RecipeIngredient" , z . object ( {
id : z.string ( ) , rawName : z.string ( ) , quantity : z.number ( ) . nullable ( ) ,
unit : z.string ( ) . nullable ( ) , note : z.string ( ) . nullable ( ) , order : z.number ( ) . int ( ) ,
} ) ) ;
const RecipeStepRef = registry . register ( "RecipeStep" , z . object ( {
id : z.string ( ) , order : z.number ( ) . int ( ) , instruction : z.string ( ) , timerSeconds : z.number ( ) . int ( ) . nullable ( ) ,
} ) ) ;
const RecipeRef = registry . register ( "Recipe" , z . object ( {
id : z.string ( ) , authorId : z.string ( ) , title : z.string ( ) , description : z.string ( ) . nullable ( ) ,
baseServings : z.number ( ) . int ( ) , visibility : z.enum ( [ "private" , "unlisted" , "public" ] ) ,
difficulty : z.enum ( [ "easy" , "medium" , "hard" ] ) . nullable ( ) ,
prepMins : z.number ( ) . int ( ) . nullable ( ) , cookMins : z.number ( ) . int ( ) . nullable ( ) ,
dietaryTags : DietaryTagsRef , aiGenerated : z.boolean ( ) ,
ingredients : z.array ( RecipeIngredientRef ) , steps : z.array ( RecipeStepRef ) ,
createdAt : z.string ( ) . datetime ( ) , updatedAt : z.string ( ) . datetime ( ) ,
} ) ) ;
const CreateRecipeRef = registry . register ( "CreateRecipe" , z . object ( {
title : z.string ( ) . min ( 1 ) . max ( 200 ) , description : z.string ( ) . optional ( ) ,
baseServings : z.number ( ) . int ( ) . positive ( ) , visibility : z.enum ( [ "private" , "unlisted" , "public" ] ) ,
difficulty : z.enum ( [ "easy" , "medium" , "hard" ] ) . optional ( ) ,
prepMins : z.number ( ) . int ( ) . positive ( ) . optional ( ) , cookMins : z.number ( ) . int ( ) . positive ( ) . optional ( ) ,
dietaryTags : DietaryTagsRef.optional ( ) ,
ingredients : z.array ( z . object ( { rawName : z.string ( ) , quantity : z.number ( ) . nullable ( ) , unit : z.string ( ) . nullable ( ) , note : z.string ( ) . nullable ( ) , order : z.number ( ) . int ( ) } ) ) ,
steps : z.array ( z . object ( { order : z.number ( ) . int ( ) , instruction : z.string ( ) , timerSeconds : z.number ( ) . int ( ) . nullable ( ) } ) ) ,
} ) ) ;
const ApiErrorRef = registry . register ( "ApiError" , z . object ( { error : z.string ( ) } ) ) ;
const Pagination = z . object ( { page : z.coerce.number ( ) . default ( 1 ) , limit : z.coerce.number ( ) . default ( 20 ) } ) ;
const CommentRef = registry . register ( "Comment" , z . object ( {
id : z.string ( ) , recipeId : z.string ( ) , userId : z.string ( ) ,
body : z.string ( ) , parentId : z.string ( ) . nullable ( ) , createdAt : z.string ( ) . datetime ( ) ,
} ) ) ;
const CollectionRef = registry . register ( "Collection" , z . object ( {
id : z.string ( ) , userId : z.string ( ) , name : z.string ( ) ,
description : z.string ( ) . nullable ( ) , isPublic : z.boolean ( ) , createdAt : z.string ( ) . datetime ( ) ,
} ) ) ;
const MealPlanRef = registry . register ( "MealPlan" , z . object ( {
weekStart : z.string ( ) ,
entries : z.array ( z . object ( { id : z.string ( ) , dayOfWeek : z.number ( ) , mealType : z.string ( ) , recipeId : z.string ( ) , servings : z.number ( ) } ) ) ,
} ) ) ;
const PantryItemRef = registry . register ( "PantryItem" , z . object ( {
id : z.string ( ) , rawName : z.string ( ) , quantity : z.string ( ) . nullable ( ) ,
unit : z.string ( ) . nullable ( ) , expiresAt : z.string ( ) . datetime ( ) . nullable ( ) ,
} ) ) ;
const ShoppingListRef = registry . register ( "ShoppingList" , z . object ( {
id : z.string ( ) , name : z.string ( ) , generatedAt : z.string ( ) . datetime ( ) . nullable ( ) , createdAt : z.string ( ) . datetime ( ) ,
items : z.array ( z . object ( { id : z.string ( ) , rawName : z.string ( ) , quantity : z.string ( ) . nullable ( ) , unit : z.string ( ) . nullable ( ) , aisle : z.string ( ) . nullable ( ) , checked : z.boolean ( ) } ) ) ,
} ) ) ;
const ApiKeyRef = registry . register ( "ApiKey" , z . object ( {
id : z.string ( ) , name : z.string ( ) , lastUsedAt : z.string ( ) . datetime ( ) . nullable ( ) , createdAt : z.string ( ) . datetime ( ) ,
} ) ) ;
const CreateApiKeyResponseRef = registry . register ( "CreateApiKeyResponse" , z . object ( {
id : z.string ( ) , name : z.string ( ) , key : z.string ( ) . describe ( "Full key — shown once" ) , createdAt : z.string ( ) . datetime ( ) ,
} ) ) ;
const AiGeneratedRef = registry . register ( "AiGeneratedRecipe" , z . object ( {
title : z.string ( ) , description : z.string ( ) , baseServings : z.number ( ) ,
difficulty : z.enum ( [ "easy" , "medium" , "hard" ] ) ,
prepMins : z.number ( ) . nullable ( ) , cookMins : z.number ( ) . nullable ( ) ,
dietaryTags : DietaryTagsRef ,
ingredients : z.array ( z . object ( { rawName : z.string ( ) , quantity : z.number ( ) . nullable ( ) , unit : z.string ( ) . nullable ( ) , note : z.string ( ) . nullable ( ) } ) ) ,
steps : z.array ( z . object ( { instruction : z.string ( ) , timerSeconds : z.number ( ) . nullable ( ) } ) ) ,
} ) ) ;
const PaginatedRecipes = z . object ( {
data : z.array ( RecipeRef ) ,
pagination : z.object ( { page : z.number ( ) , limit : z.number ( ) , total : z.number ( ) , pages : z.number ( ) } ) ,
} ) ;
const idParam = z . object ( { id : z.string ( ) } ) ;
registry . registerPath ( { method : "get" , path : "/api/v1/recipes" , summary : "List recipes" , security , request : { query : z.object ( { page : z.coerce.number ( ) . default ( 1 ) , limit : z.coerce.number ( ) . default ( 20 ) , visibility : z.enum ( [ "private" , "unlisted" , "public" ] ) . optional ( ) , q : z.string ( ) . optional ( ) , difficulty : z.enum ( [ "easy" , "medium" , "hard" ] ) . 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" , summary : "Create recipe" , security , request : { body : { content : { "application/json" : { schema : CreateRecipeRef } } , required : true } } , responses : { 201 : { description : "Created" , content : { "application/json" : { schema : RecipeRef } } } , 400 : { description : "Bad request" , content : { "application/json" : { schema : ApiErrorRef } } } , 401 : { description : "Unauthorized" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "get" , path : "/api/v1/recipes/{id}" , summary : "Get recipe" , security , request : { params : idParam } , responses : { 200 : { description : "Recipe" , content : { "application/json" : { schema : RecipeRef } } } , 404 : { description : "Not found" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "put" , path : "/api/v1/recipes/{id}" , summary : "Update recipe" , security , request : { params : idParam , body : { content : { "application/json" : { schema : CreateRecipeRef } } , required : true } } , responses : { 200 : { description : "Updated" , content : { "application/json" : { schema : RecipeRef } } } , 401 : { description : "Unauthorized" , content : { "application/json" : { schema : ApiErrorRef } } } , 404 : { description : "Not found" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "delete" , path : "/api/v1/recipes/{id}" , summary : "Delete recipe" , security , request : { params : idParam } , responses : { 204 : { description : "Deleted" } , 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}/favorite" , summary : "Toggle favorite" , security , request : { params : idParam } , responses : { 200 : { description : "Favorited" , content : { "application/json" : { schema : z.object ( { favorited : z.boolean ( ) } ) } } } , 401 : { description : "Unauthorized" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "post" , path : "/api/v1/recipes/{id}/rate" , summary : "Rate recipe (1– 5)" , security , request : { params : idParam , body : { content : { "application/json" : { schema : z.object ( { score : z.number ( ) . int ( ) . min ( 1 ) . max ( 5 ) } ) } } , required : true } } , responses : { 200 : { description : "Saved" , content : { "application/json" : { schema : z.object ( { score : z.number ( ) } ) } } } , 401 : { description : "Unauthorized" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "get" , path : "/api/v1/recipes/{id}/comments" , summary : "List comments" , security , request : { params : idParam , query : Pagination } , responses : { 200 : { description : "Comments" , content : { "application/json" : { schema : z.array ( CommentRef ) } } } } } ) ;
registry . registerPath ( { method : "post" , path : "/api/v1/recipes/{id}/comments" , summary : "Post comment" , security , request : { params : idParam , body : { content : { "application/json" : { schema : z.object ( { body : z.string ( ) . min ( 1 ) , parentId : z.string ( ) . optional ( ) } ) } } , required : true } } , responses : { 201 : { description : "Created" , content : { "application/json" : { schema : CommentRef } } } , 401 : { description : "Unauthorized" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "post" , path : "/api/v1/ai/generate" , summary : "Generate recipe from prompt" , description : "Rate-limited: 10 req/min." , security , request : { body : { content : { "application/json" : { schema : z.object ( { prompt : z.string ( ) . min ( 1 ) } ) } } , required : true } } , responses : { 200 : { description : "Generated" , content : { "application/json" : { schema : AiGeneratedRef } } } , 429 : { description : "Rate limited" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "post" , path : "/api/v1/ai/import-url" , summary : "Import recipe from URL" , description : "Rate-limited: 10 req/min." , security , request : { body : { content : { "application/json" : { schema : z.object ( { url : z.string ( ) . url ( ) } ) } } , required : true } } , responses : { 200 : { description : "Imported" , content : { "application/json" : { schema : AiGeneratedRef } } } , 429 : { description : "Rate limited" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "get" , path : "/api/v1/feed" , summary : "Activity feed (pull-based)" , security , request : { query : Pagination } , responses : { 200 : { description : "Feed" , content : { "application/json" : { schema : z.array ( RecipeRef ) } } } , 401 : { description : "Unauthorized" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "get" , path : "/api/v1/collections" , summary : "List collections" , security , request : { query : Pagination } , responses : { 200 : { description : "Collections" , content : { "application/json" : { schema : z.array ( CollectionRef ) } } } , 401 : { description : "Unauthorized" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "get" , path : "/api/v1/meal-plans/{weekStart}" , summary : "Get meal plan for week" , security , request : { params : z.object ( { weekStart : z.string ( ) . describe ( "ISO date YYYY-MM-DD (Monday)" ) } ) } , responses : { 200 : { description : "Meal plan" , content : { "application/json" : { schema : MealPlanRef } } } , 401 : { description : "Unauthorized" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "get" , path : "/api/v1/pantry" , summary : "List pantry items" , security , request : { query : Pagination } , responses : { 200 : { description : "Items" , content : { "application/json" : { schema : z.array ( PantryItemRef ) } } } , 401 : { description : "Unauthorized" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "get" , path : "/api/v1/shopping-lists" , summary : "List shopping lists" , security , request : { query : Pagination } , responses : { 200 : { description : "Lists" , content : { "application/json" : { schema : z.array ( ShoppingListRef ) } } } , 401 : { description : "Unauthorized" , content : { "application/json" : { schema : ApiErrorRef } } } } } ) ;
registry . registerPath ( { method : "post" , path : "/api/v1/shopping-lists" , summary : "Create shopping list" , security , request : { body : { content : { "application/json" : { schema : z.object ( { name : z.string ( ) . min ( 1 ) , fromMealPlanWeek : z.string ( ) . optional ( ) } ) } } , required : true } } , responses : { 201 : { description : "Created" , content : { "application/json" : { schema : ShoppingListRef } } } , 401 : { description : "Unauthorized" , 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 ) } ) } } , 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 : "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 } } } } } ) ;
const generator = new OpenApiGeneratorV31 ( registry . definitions ) ;
cachedSpec = generator . generateDocument ( {
openapi : "3.1.0" ,
info : { title : "Epicure API" , version : "1.0.0" , description : "Auth: session cookie or Bearer API key (`ek_...`)." } ,
servers : [ { url : process.env [ "BETTER_AUTH_URL" ] ? ? "http://localhost:3000" } ] ,
} ) ;
return cachedSpec ;
}