UNPKG

@centinel/nextjs

Version:

Package designed to add Centinel Analytica functionality to Next.js applications

116 lines (115 loc) 4.5 kB
import { NextResponse } from 'next/server'; export class CentinelVercelIntegration { constructor(config) { this.config = config; } async runParallel(event, originRequest) { const request = event.request; try { // Extract client information const clientInfo = this.extractClientInfo(request); // Build validation payload const payload = this.buildValidationPayload(request, clientInfo); // Start validation in parallel with origin request const [validationResult, originResponse] = await Promise.all([ this.validateRequest(payload), originRequest(request) ]); // If validation says to block or redirect, override the origin response if (validationResult.decision === 'block') { return { response: NextResponse.redirect(new URL('/block', request.url)), decision: 'block' }; } if (validationResult.decision === 'redirect') { const redirectUrl = validationResult.redirect_url || '/interstitial'; const redirectResponse = NextResponse.redirect(new URL(redirectUrl, request.url)); // Set cookies from validation result if (validationResult.cookies && validationResult.cookies.length > 0) { this.setCookies(redirectResponse, validationResult.cookies, request); } return { response: redirectResponse, decision: 'redirect' }; } // For allow or not_matched, return the origin response return { response: originResponse, decision: validationResult.decision }; } catch (error) { // On error, allow the request to proceed (fail open for parallel mode) return { response: await originRequest(request), decision: 'allow' }; } } extractClientInfo(request) { const forwardedFor = request.headers.get('x-forwarded-for'); const realIp = request.headers.get('x-real-ip'); const cfConnectingIp = request.headers.get('cf-connecting-ip'); return { ip: cfConnectingIp || forwardedFor?.split(',')[0]?.trim() || realIp || 'unknown', referer: request.headers.get('referer') || request.headers.get('referrer') || '' }; } buildValidationPayload(request, clientInfo) { const headers = {}; request.headers.forEach((value, key) => { headers[key] = value; }); return { cookie: request.cookies.get('_centinel')?.value, url: request.url, ip: clientInfo.ip, referrer: clientInfo.referer, method: request.method, headers }; } async validateRequest(payload) { try { const response = await fetch('https://validator.centinelanalytica.com/validate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': this.config.secretKey }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error(`Validator service responded with ${response.status}`); } const result = await response.json(); return { success: result.success !== false, decision: result.decision || 'block', redirect_url: result.redirect_url, cookies: result.cookies || [] }; } catch (error) { return { success: false, decision: 'allow' // Fail open for parallel mode }; } } setCookies(response, cookies, request) { const realHost = this.getRealHost(request); cookies.forEach(cookie => { response.cookies.set(cookie.name, cookie.value, { path: cookie.path || '/', domain: cookie.domain || realHost }); }); } getRealHost(request) { const host = request.headers.get('host'); return host?.includes('localhost') ? undefined : host || undefined; } }