import { OpenAPIRegistry, OpenApiGeneratorV3, extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; import { z } from 'zod'; extendZodWithOpenApi(z); const registry = new OpenAPIRegistry(); const ErrorResponseSchema = registry.register( 'ErrorResponse', z.object({ message: z.string(), errors: z.unknown().optional(), }), ); const VideoRecordSchema = registry.register( 'VideoRecord', z.object({ id: z.string().uuid(), objectKey: z.string(), bucket: z.string(), status: z.string(), createdAt: z.string().datetime(), expiresAt: z.string().datetime().nullable(), }), ); const VideoUploadUrlRequestSchema = registry.register( 'VideoUploadUrlRequest', z.object({ fileName: z.string().min(1).max(255), deviceId: z.string().uuid(), prefix: z.string().optional(), }), ); const AdminUploadUrlRequestSchema = registry.register( 'AdminUploadUrlRequest', z.object({ fileName: z.string().min(1).max(255), prefix: z.string().optional(), }), ); const UploadUrlResponseSchema = registry.register( 'UploadUrlResponse', z.object({ message: z.string(), bucket: z.string(), objectKey: z.string(), uploadUrl: z.string().url(), expiresInSeconds: z.number().int().positive(), video: VideoRecordSchema, }), ); const DownloadUrlQuerySchema = registry.register( 'DownloadUrlQuery', z.object({ objectKey: z.string().min(1), }), ); const DownloadUrlResponseSchema = registry.register( 'DownloadUrlResponse', z.object({ message: z.string(), bucket: z.string(), objectKey: z.string(), downloadUrl: z.string().url(), expiresInSeconds: z.number().int().positive(), }), ); const ListVideosQuerySchema = registry.register( 'ListVideosQuery', z.object({ prefix: z.string().optional(), limit: z.coerce.number().int().min(1).max(100).default(20), }), ); const ListedObjectSchema = registry.register( 'ListedObject', z.object({ objectKey: z.string().nullable(), size: z.number(), etag: z.string().nullable(), lastModified: z.string().datetime().nullable(), }), ); const ListVideosResponseSchema = registry.register( 'ListVideosResponse', z.object({ bucket: z.string(), count: z.number().int().nonnegative(), objects: z.array(ListedObjectSchema), }), ); const DeleteVideoResponseSchema = registry.register( 'DeleteVideoResponse', z.object({ message: z.string(), bucket: z.string(), objectKey: z.string(), }), ); registry.registerPath({ method: 'get', path: '/', summary: 'Health check', tags: ['System'], responses: { 200: { description: 'Service health text', content: { 'text/plain': { schema: z.string(), }, }, }, }, }); registry.registerPath({ method: 'post', path: '/videos/upload-url', summary: 'Create a presigned upload URL', tags: ['Videos'], security: [{ cookieAuth: [] }], request: { body: { content: { 'application/json': { schema: VideoUploadUrlRequestSchema, }, }, }, }, responses: { 201: { description: 'Presigned upload URL generated', content: { 'application/json': { schema: UploadUrlResponseSchema, }, }, }, 400: { description: 'Invalid request body', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, 401: { description: 'Unauthorized', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, }, }); registry.registerPath({ method: 'get', path: '/videos/download-url', summary: 'Create a signed download URL', tags: ['Videos'], security: [{ cookieAuth: [] }], request: { query: DownloadUrlQuerySchema, }, responses: { 200: { description: 'Signed download URL generated', content: { 'application/json': { schema: DownloadUrlResponseSchema, }, }, }, 400: { description: 'Invalid query params', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, 401: { description: 'Unauthorized', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, }, }); registry.registerPath({ method: 'get', path: '/videos', summary: 'List objects in the configured bucket', tags: ['Videos'], security: [{ cookieAuth: [] }], request: { query: ListVideosQuerySchema, }, responses: { 200: { description: 'Object listing', content: { 'application/json': { schema: ListVideosResponseSchema, }, }, }, 400: { description: 'Invalid query params', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, 401: { description: 'Unauthorized', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, }, }); registry.registerPath({ method: 'delete', path: '/videos', summary: 'Delete object by object key', tags: ['Videos'], security: [{ cookieAuth: [] }], request: { query: DownloadUrlQuerySchema, }, responses: { 200: { description: 'Object deleted', content: { 'application/json': { schema: DeleteVideoResponseSchema, }, }, }, 400: { description: 'Invalid query params', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, 401: { description: 'Unauthorized', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, }, }); registry.registerPath({ method: 'post', path: '/admin/upload-url', summary: 'Admin: create presigned upload URL', tags: ['Admin'], security: [{ basicAuth: [] }], request: { body: { content: { 'application/json': { schema: AdminUploadUrlRequestSchema, }, }, }, }, responses: { 201: { description: 'Admin upload URL generated', content: { 'application/json': { schema: z.object({ message: z.string(), bucket: z.string(), objectKey: z.string(), uploadUrl: z.string().url(), expiresAt: z.string().datetime(), expiresInSeconds: z.number().int().positive(), }), }, }, }, 400: { description: 'Invalid request body', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, 401: { description: 'Unauthorized', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, }, }); registry.registerPath({ method: 'get', path: '/admin/objects', summary: 'Admin: list objects in bucket', tags: ['Admin'], security: [{ basicAuth: [] }], request: { query: ListVideosQuerySchema, }, responses: { 200: { description: 'Admin object listing', content: { 'application/json': { schema: ListVideosResponseSchema, }, }, }, 400: { description: 'Invalid query params', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, 401: { description: 'Unauthorized', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, }, }); registry.registerPath({ method: 'delete', path: '/admin/object', summary: 'Admin: delete object by key', tags: ['Admin'], security: [{ basicAuth: [] }], request: { query: DownloadUrlQuerySchema, }, responses: { 200: { description: 'Object deleted', content: { 'application/json': { schema: DeleteVideoResponseSchema, }, }, }, 400: { description: 'Invalid query params', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, 401: { description: 'Unauthorized', content: { 'application/json': { schema: ErrorResponseSchema, }, }, }, }, }); export function buildOpenApiDocument() { const generator = new OpenApiGeneratorV3(registry.definitions); return generator.generateDocument({ openapi: '3.0.3', info: { title: 'Backend API', version: '1.0.0', description: 'Auto-generated API documentation from Zod schemas.', }, servers: [{ url: process.env.BETTER_AUTH_URL ?? 'http://localhost:3000' }], tags: [ { name: 'System', description: 'Service endpoints' }, { name: 'Videos', description: 'Authenticated video object operations' }, { name: 'Admin', description: 'Basic-auth protected admin operations' }, ], components: { securitySchemes: { cookieAuth: { type: 'apiKey', in: 'cookie', name: 'better-auth.session_token', description: 'Better Auth session cookie', }, basicAuth: { type: 'http', scheme: 'basic', }, }, }, }); }