(req: any, res: any)
| 14 | } from './lib/security.js'; |
| 15 | |
| 16 | export default async function handler(req: any, res: any) { |
| 17 | // Only allow POST requests |
| 18 | if (req.method !== 'POST') { |
| 19 | return res.status(405).json({ error: 'Method not allowed' }); |
| 20 | } |
| 21 | |
| 22 | // Basic request hardening |
| 23 | const origin = req.headers?.origin || req.headers?.referer; |
| 24 | if (!isAllowedOrigin(origin)) { |
| 25 | return res.status(403).json({ |
| 26 | error: 'Forbidden origin' |
| 27 | }); |
| 28 | } |
| 29 | |
| 30 | if (!isAuthorizedRequest(req)) { |
| 31 | return res.status(401).json({ |
| 32 | error: 'Unauthorized request' |
| 33 | }); |
| 34 | } |
| 35 | |
| 36 | const contentType = req.headers?.['content-type'] || ''; |
| 37 | if (typeof contentType === 'string' && !contentType.includes('application/json')) { |
| 38 | return res.status(415).json({ error: 'Content-Type must be application/json' }); |
| 39 | } |
| 40 | |
| 41 | const clientIp = getClientIp(req); |
| 42 | const rateLimitResult = checkRateLimit(clientIp); |
| 43 | if (!rateLimitResult.allowed) { |
| 44 | res.setHeader('Retry-After', String(rateLimitResult.retryAfterSeconds)); |
| 45 | return res.status(429).json({ |
| 46 | error: 'Rate limit exceeded. Please retry later.', |
| 47 | retry_after_seconds: rateLimitResult.retryAfterSeconds |
| 48 | }); |
| 49 | } |
| 50 | |
| 51 | if (!process.env.GITHUB_TOKEN) { |
| 52 | return res.status(503).json({ |
| 53 | error: 'Bundle generation service is not configured' |
| 54 | }); |
| 55 | } |
| 56 | |
| 57 | const rawRepoUrl = req.body.repoUrl; |
| 58 | const repoUrl = rawRepoUrl ? rawRepoUrl.trim() : rawRepoUrl; |
| 59 | |
| 60 | // Validate input |
| 61 | if (!repoUrl) { |
| 62 | return res.status(400).json({ error: 'Repository URL is required' }); |
| 63 | } |
| 64 | |
| 65 | // Validate GitHub URL format |
| 66 | // Allow optional trailing slash and .git extension |
| 67 | const githubUrlPattern = /^https?:\/\/(www\.)?github\.com\/([^\/]+)\/([^\/]+)(\.git)?\/?$/; |
| 68 | const match = repoUrl.match(githubUrlPattern); |
| 69 | |
| 70 | if (!match) { |
| 71 | return res.status(400).json({ |
| 72 | error: 'Invalid GitHub URL format. Expected: https://github.com/owner/repo' |
| 73 | }); |
nothing calls this directly
no test coverage detected