@centinel/nextjs
Version:
Package designed to add Centinel Analytica functionality to Next.js applications
116 lines (115 loc) • 4.5 kB
JavaScript
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;
}
}