import type { CliPlugin, PluginConfigValue, PluginContext } from '../types.js' /** * CORS Plugin * Provides configurable Cross-Origin Resource Sharing (CORS) middleware */ export const corsPlugin: CliPlugin = { id: 'cors', category: 'middleware', description: 'Configurable Cross-Origin Resource Sharing (CORS) middleware', config: { origin: { type: 'select', description: 'Allowed origins', options: [ { name: 'All origins (*)', value: '*' }, { name: 'Same origin only', value: 'same-origin' }, { name: 'Custom origins', value: 'custom' }, ], default: 'same-origin', }, customOrigins: { type: 'string', description: 'Comma-separated list of allowed origins (when origin is "custom")', default: 'http://localhost:3000,http://localhost:5173', }, methods: { type: 'string', description: 'Allowed HTTP methods (comma-separated)', default: 'GET,POST,PUT,DELETE,OPTIONS,PATCH', }, allowedHeaders: { type: 'string', description: 'Allowed request headers (comma-separated)', default: 'Content-Type,Authorization,X-Requested-With,Accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers', }, exposedHeaders: { type: 'string', description: 'Headers exposed to the client (comma-separated)', default: 'X-Total-Count,X-Page-Count,X-Per-Page,X-Current-Page', }, credentials: { type: 'boolean', description: 'Allow credentials (cookies, authorization headers)', default: true, }, maxAge: { type: 'number', description: 'Preflight cache duration in seconds', default: 86400, }, preflightContinue: { type: 'boolean', description: 'Pass control to next handler after preflight', default: false, }, optionsSuccessStatus: { type: 'number', description: 'Status code for successful OPTIONS requests', default: 204, }, enableLogging: { type: 'boolean', description: 'Log CORS requests and violations', default: true, }, }, prepare(_ctx: PluginContext, _config: Record) { // Add hono/cors dependency (built-in with Hono) }, async apply(ctx: PluginContext, config: Record): Promise { const origin = config.origin || 'same-origin' const customOrigins = config.customOrigins ? String(config.customOrigins) .split(',') .map(o => o.trim()) : [] const methods = config.methods ? String(config.methods) .split(',') .map(m => m.trim()) : ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'] const allowedHeaders = config.allowedHeaders ? String(config.allowedHeaders) .split(',') .map(h => h.trim()) : [ 'Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers', ] const exposedHeaders = config.exposedHeaders ? String(config.exposedHeaders) .split(',') .map(h => h.trim()) : ['X-Total-Count', 'X-Page-Count', 'X-Per-Page', 'X-Current-Page'] const credentials = config.credentials !== false const maxAge = config.maxAge || 86400 const preflightContinue = config.preflightContinue || false const optionsSuccessStatus = config.optionsSuccessStatus || 204 const enableLogging = config.enableLogging !== false // Generate CORS middleware file const corsMiddlewareContent = `import type { Context, MiddlewareHandler } from 'hono' import { createMiddleware } from 'hono/factory' import { cors } from 'hono/cors' import type { AppBindings } from './types.js' /** * CORS middleware generated by CORS Plugin */ /** * Custom CORS middleware with enhanced logging and validation */ export function corsMiddleware(): MiddlewareHandler { // Configure origin function const originFunction = (origin: string, c: Context) => { ${ origin === '*' ? ` return '*' ` : origin === 'same-origin' ? ` const requestOrigin = c.req.header('origin') const host = c.req.header('host') // Allow same-origin requests if (!requestOrigin) return true // Same-origin requests don't have Origin header // Check if origin matches host try { const originUrl = new URL(requestOrigin) const hostUrl = new URL(\`\${c.req.url.startsWith('https') ? 'https' : 'http'}://\${host}\`) return originUrl.origin === hostUrl.origin } catch { return false } ` : ` const allowedOrigins = [${customOrigins.map(o => `'${o}'`).join(', ')}] return allowedOrigins.includes(origin) || allowedOrigins.includes('*') ` } } return createMiddleware(async (c, next) => { const requestOrigin = c.req.header('origin') const method = c.req.method const requestedMethod = c.req.header('access-control-request-method') const requestedHeaders = c.req.header('access-control-request-headers') ${ enableLogging ? ` // Log CORS requests if (requestOrigin) { console.log(JSON.stringify({ type: 'cors_request', origin: requestOrigin, method, requestedMethod, requestedHeaders, timestamp: new Date().toISOString() })) } ` : '' } // Handle preflight requests if (method === 'OPTIONS') { const allowOrigin = originFunction(requestOrigin || '', c) if (!allowOrigin) { ${ enableLogging ? ` console.warn(JSON.stringify({ type: 'cors_violation', reason: 'origin_not_allowed', origin: requestOrigin, timestamp: new Date().toISOString() })) ` : '' } return c.text('CORS: Origin not allowed', 403) } // Set CORS headers for preflight if (typeof allowOrigin === 'string') { c.header('Access-Control-Allow-Origin', allowOrigin) } else if (allowOrigin === true && requestOrigin) { c.header('Access-Control-Allow-Origin', requestOrigin) } ${ credentials ? ` c.header('Access-Control-Allow-Credentials', 'true') ` : '' } c.header('Access-Control-Allow-Methods', '${methods.join(', ')}') c.header('Access-Control-Allow-Headers', '${allowedHeaders.join(', ')}') c.header('Access-Control-Max-Age', '${maxAge}') ${ exposedHeaders.length > 0 ? ` c.header('Access-Control-Expose-Headers', '${exposedHeaders.join(', ')}') ` : '' } // Validate requested method if (requestedMethod && !${JSON.stringify(methods)}.includes(requestedMethod)) { ${ enableLogging ? ` console.warn(JSON.stringify({ type: 'cors_violation', reason: 'method_not_allowed', origin: requestOrigin, requestedMethod, allowedMethods: ${JSON.stringify(methods)}, timestamp: new Date().toISOString() })) ` : '' } return c.text('CORS: Method not allowed', 405) } // Validate requested headers if (requestedHeaders) { const requestedHeadersList = requestedHeaders.split(',').map(h => h.trim().toLowerCase()) const allowedHeadersList = ${JSON.stringify(allowedHeaders.map(h => h.toLowerCase()))} const invalidHeaders = requestedHeadersList.filter(h => !allowedHeadersList.includes(h)) if (invalidHeaders.length > 0) { ${ enableLogging ? ` console.warn(JSON.stringify({ type: 'cors_violation', reason: 'headers_not_allowed', origin: requestOrigin, invalidHeaders, allowedHeaders: ${JSON.stringify(allowedHeaders)}, timestamp: new Date().toISOString() })) ` : '' } return c.text('CORS: Headers not allowed', 400) } } ${ preflightContinue ? ` await next() ` : ` return c.text('', ${optionsSuccessStatus}) ` } } else { // Handle actual requests const allowOrigin = originFunction(requestOrigin || '', c) if (requestOrigin && !allowOrigin) { ${ enableLogging ? ` console.warn(JSON.stringify({ type: 'cors_violation', reason: 'origin_not_allowed', origin: requestOrigin, method, timestamp: new Date().toISOString() })) ` : '' } return c.text('CORS: Origin not allowed', 403) } // Set CORS headers for actual requests if (typeof allowOrigin === 'string') { c.header('Access-Control-Allow-Origin', allowOrigin) } else if (allowOrigin === true && requestOrigin) { c.header('Access-Control-Allow-Origin', requestOrigin) } ${ credentials ? ` c.header('Access-Control-Allow-Credentials', 'true') ` : '' } ${ exposedHeaders.length > 0 ? ` c.header('Access-Control-Expose-Headers', '${exposedHeaders.join(', ')}') ` : '' } await next() } }) } /** * Simple CORS middleware using Hono's built-in cors */ export function simpleCors(): MiddlewareHandler { return cors({ origin: ${ origin === '*' ? `'*'` : origin === 'same-origin' ? `(origin, c) => { const host = c.req.header('host') if (!origin) return true try { const originUrl = new URL(origin) const hostUrl = new URL(\`\${c.req.url.startsWith('https') ? 'https' : 'http'}://\${host}\`) return originUrl.origin === hostUrl.origin } catch { return false } }` : `[${customOrigins.map(o => `'${o}'`).join(', ')}]` }, allowMethods: [${methods.map(m => `'${m}'`).join(', ')}], allowHeaders: [${allowedHeaders.map(h => `'${h}'`).join(', ')}], exposeHeaders: [${exposedHeaders.map(h => `'${h}'`).join(', ')}], credentials: ${credentials}, maxAge: ${maxAge} }) } // Export the configured CORS function (use enhanced version by default) export const corsHandler = corsMiddleware ` await ctx.addFile('src/lib/cors.middleware.ts', corsMiddlewareContent) // Update main app file to include CORS middleware if (await ctx.fileExists('src/index.ts')) { await ctx.modifyFile('src/index.ts', (content: string) => { // Add import if not already present if (!content.includes('cors.middleware')) { const importLine = `import { corsHandler } from './lib/cors.middleware.js'` // Find the last import statement const lines = content.split('\\n') let lastImportIndex = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].trim().startsWith('import ')) { lastImportIndex = i } } if (lastImportIndex >= 0) { lines.splice(lastImportIndex + 1, 0, importLine) } else { lines.unshift(importLine) } content = lines.join('\\n') } // Add middleware usage if not already present if (!content.includes('corsHandler()')) { // Find where to insert middleware calls (should be early, after logger) if (content.includes('const app = ') || content.includes('export const app = ')) { const lines = content.split('\\n') let insertIndex = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].includes('const app = ') || lines[i].includes('export const app = ')) { insertIndex = i + 1 // Look for existing middleware to insert after logger while ( insertIndex < lines.length && (lines[insertIndex].includes('pinoLogger') || lines[insertIndex].trim() === '' || lines[insertIndex].startsWith('//')) ) { insertIndex++ } break } } if (insertIndex >= 0) { lines.splice(insertIndex, 0, '', '// CORS middleware', "app.use('*', corsHandler())") content = lines.join('\\n') } } } return content }) } }, }