Centinel AnalyticaCentinel Analytica

Validation

Wire the /validate API into your backend when no platform integration covers your stack.

Overview

If a platform integration covers your stack, use it. This page is for custom backends and platforms not yet supported. You will call POST /validate for every protected request and act on the response.

When to call /validate

  • On the server, after you have parsed the request.
  • Once per request, before your application handles it.
  • Skip locally for paths you do not want to protect — the validator returns not_matched for those, but skipping saves a round trip.

Handle each decision

Call POST /validate with the request URL, method, client IP, all headers, and the _centinel cookie.

Apply every entry from response.headers to your outgoing response, and every entry from response.cookies as a Set-Cookie.

Branch on response.decision.

For block or redirect, base64-decode response.response_html and return it as the response body.

For allow or not_matched, pass the request through to your application.

type ValidateDecision = 'allow' | 'block' | 'redirect' | 'not_matched';

async function enforce(req, res, next) {
  const result = await callValidate({
    url: req.url,
    method: req.method,
    ip: clientIpFrom(req),
    referrer: req.headers.referer,
    cookie: req.cookies['_centinel'],
    headers: req.headers,
  });

  for (const [name, value] of Object.entries(result.headers)) res.setHeader(name, value);
  for (const c of result.cookies ?? []) res.cookie(c.name, c.value, { path: c.path, domain: c.domain });

  switch (result.decision) {
    case 'allow':
    case 'not_matched':
      return next();
    case 'block':
      return res.status(403).send(Buffer.from(result.response_html, 'base64').toString('utf8'));
    case 'redirect':
      return res.status(200).send(Buffer.from(result.response_html, 'base64').toString('utf8'));
  }
}

Decoding response_html

Both block and redirect carry response_html as base64-encoded UTF-8. Decode and return as the body. Use HTTP 403 for block, 200 for redirect — or honour response.status_code if you prefer the validator's recommendation.

// Node.js
const html = Buffer.from(response.response_html, 'base64').toString('utf8');
// Web platforms (Cloudflare Workers, Deno, browsers)
const html = new TextDecoder().decode(
  Uint8Array.from(atob(response.response_html), c => c.charCodeAt(0))
);

Applying headers

Every response carries a headers object Centinel wants set on your outgoing response — Content-Type, security headers, observability identifiers. Copy them through; do not hardcode your own.

// Express
for (const [name, value] of Object.entries(response.headers)) {
  res.setHeader(name, value);
}
// Cloudflare Worker
const headers = new Headers();
for (const [name, value] of Object.entries(response.headers)) {
  headers.set(name, value);
}
return new Response(body, { status, headers });

Common pitfalls

  • Calling /validate from the browser. The x-api-key is server-only. Anyone with the key has full tenant access.
  • Hardcoding Content-Type. Always copy response.headers through. They may include observability or session headers your app needs.
  • Ignoring cookies on allow. The cookies array can be non-empty on any decision (session cookies on a happy-path response, challenge cookies on redirect). Apply them all.
  • Passing your CDN's IP as ip. Pass the real client IP from X-Forwarded-For or your platform's equivalent. Wrong IP breaks detection.
  • Dropping the _centinel cookie. Without it, session tracking across interstitial challenges fails.

See also

On this page