/**
 * Trusted client-IP extraction.
 *
 * The `X-Forwarded-For` header is fully attacker-controlled when the server is
 * directly reachable from the Internet — anyone can send `X-Forwarded-For: 1.2.3.4`
 * to bypass per-IP rate limits and poison audit logs.
 *
 * This helper resolves the real client IP using the most trustworthy header
 * available, in this order:
 *
 *   1. Vercel — `x-vercel-forwarded-for` is set by the Vercel edge and cannot
 *      be spoofed by clients (Vercel strips inbound copies).
 *   2. Common reverse proxies — `x-real-ip` is typically set by nginx/Caddy
 *      to the directly-connected client IP.
 *   3. `x-forwarded-for` — only honoured when the integrator opts in via
 *      `trustProxy: true`. In that mode we use the **last** entry (closest to
 *      the proxy) rather than the first (closest to the client / spoofable).
 *
 * Returns the literal string `'unknown'` when no trusted source is available;
 * callers should treat that as a hard failure for security-sensitive decisions.
 */
import { createHash } from 'node:crypto';
const VERCEL_HEADER = 'x-vercel-forwarded-for';
const REAL_IP_HEADER = 'x-real-ip';
const FORWARDED_HEADER = 'x-forwarded-for';
export function getClientIp(request, options = {}) {
    const trustProxy = options.trustProxy ?? process.env.ACTUATE_TRUST_PROXY === '1';
    const vercel = request.headers.get(VERCEL_HEADER);
    if (vercel) {
        const first = vercel.split(',')[0]?.trim();
        if (first)
            return first;
    }
    const realIp = request.headers.get(REAL_IP_HEADER);
    if (realIp) {
        const trimmed = realIp.trim();
        if (trimmed)
            return trimmed;
    }
    if (trustProxy) {
        const fwd = request.headers.get(FORWARDED_HEADER);
        if (fwd) {
            // Use the last entry — that's the IP your trusted proxy itself saw,
            // not whatever the original client claimed in the chain.
            const parts = fwd
                .split(',')
                .map((p) => p.trim())
                .filter(Boolean);
            const last = parts[parts.length - 1];
            if (last)
                return last;
        }
    }
    return 'unknown';
}
/**
 * Returns true when the resolved client IP is a real value we can use for
 * security-sensitive keys (rate limits, IP allowlists). Returns false for the
 * `'unknown'` sentinel.
 */
export function isResolvedIp(ip) {
    return ip !== 'unknown' && ip.length > 0;
}
/**
 * One-way hash of a client IP for storage alongside form submissions.
 *
 * We never persist raw IPs — they are PII and a liability. Hashing with the
 * instance secret as salt lets operators correlate repeat submitters / abuse
 * within a single deployment while making the stored value useless to anyone
 * who exfiltrates the table without also having `CMS_SECRET`. Returns `null`
 * for the `'unknown'` sentinel so callers store SQL NULL rather than a hash of
 * the literal string `'unknown'`.
 */
export function hashClientIp(ip) {
    if (!isResolvedIp(ip))
        return null;
    const salt = process.env.CMS_SECRET ?? 'actuate-fallback-salt';
    return createHash('sha256').update(`${salt}:${ip}`).digest('hex').slice(0, 32);
}
//# sourceMappingURL=client-ip.js.map