@centinel/nextjs
Version:
Package designed to add Centinel Analytica functionality to Next.js applications
156 lines (155 loc) • 5.41 kB
JavaScript
class CentinelApiValidator {
constructor(config) {
this.config = config;
}
async isBot(request) {
try {
// Extract client information
const clientInfo = this.extractClientInfo(request);
// Build validation payload
const payload = this.buildValidationPayload(request, clientInfo);
// Perform validation
const validationResult = await this.validateRequest(payload);
// Return true if bot/should be blocked, false if legitimate user
// redirect should never be the case for an isBot check
return !validationResult.success || ["block", "redirect"].includes(validationResult.decision);
}
catch (error) {
console.error('Validation error:', error);
// Fail open - assume not a bot on error
return false;
}
}
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;
});
let realHost = request.headers.get("x-forwarded-host") || request.nextUrl.hostname;
if (realHost === "localhost") {
realHost = `localhost:${process.env.PORT}`;
}
const requestUrl = `${request.nextUrl.protocol}//${realHost}${request.nextUrl.pathname}${request.nextUrl.search}`;
return {
cookie: request.cookies.get("_centinel")?.value,
url: requestUrl,
ip: clientInfo.ip,
referrer: clientInfo.referer,
method: request.method,
headers: 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: "block",
};
}
}
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") || undefined;
return host?.includes("localhost") ? undefined : host;
}
}
/**
* Creates a request validation function for manual validation in API routes.
*
* @param config - Centinel configuration object
* @returns Object with verify function
*
* @example
* ```typescript
* // app/api/login/route.ts
* import { createRequestValidator } from '@centinel/nextjs';
* import { NextRequest, NextResponse } from 'next/server';
*
* const { isBot } = createRequestValidator({
* siteKey: process.env.CENTINEL_SITE_KEY!,
* secretKey: process.env.CENTINEL_SECRET_KEY!
* });
*
* export async function POST(request: NextRequest) {
* if (await isBot(request)) {
* return NextResponse.json({ error: 'You have been blocked' }, { status: 403 });
* }
* return handleLogin(request);
* }
* ```
*/
export function createRequestValidator(config) {
const validator = new CentinelApiValidator(config);
return {
isBot: (request) => validator.isBot(request),
};
}
/**
* Creates request validation function using environment variables for configuration.
*
* @returns Object with verify function
*
* @example
* ```typescript
* // app/api/login/route.ts
* import { createRequestValidatorFromEnv } from '@centinel/nextjs';
* import { NextRequest, NextResponse } from 'next/server';
*
* const { isBot } = createRequestValidatorFromEnv();
*
* export async function POST(request: NextRequest) {
* if (await isBot(request)) {
* return NextResponse.json({ error: 'You have been blocked' }, { status: 403 });
* }
* return handleLogin(request);
* }
* ```
*/
export function createRequestValidatorFromEnv() {
const config = {
siteKey: process.env.CENTINEL_SITE_KEY,
secretKey: process.env.CENTINEL_SECRET_KEY,
};
return createRequestValidator(config);
}
export { CentinelApiValidator };