/**
 * IP allowlist matcher supporting both IPv4 and IPv6, including CIDR ranges.
 *
 * - Empty allowlist allows everything (the integrator opted out).
 * - When the allowlist is non-empty, the literal `'unknown'` (the sentinel
 *   returned by the trusted-IP resolver when no proxy header was present)
 *   never matches.
 * - IPv4 entries match IPv4 addresses (with optional `/n` CIDR mask, n=0..32).
 * - IPv6 entries match IPv6 addresses (with optional `/n` CIDR mask, n=0..128).
 *   Two IPv6 strings are equal when their canonical 16-byte form matches —
 *   so `2001:db8::1` matches `2001:db8:0:0:0:0:0:1`.
 * - Mapped IPv4-in-IPv6 addresses (`::ffff:1.2.3.4`) are normalised to IPv4.
 */
export function isIpAllowed(ip, allowlist) {
    if (allowlist.length === 0)
        return true;
    if (!ip || ip === 'unknown')
        return false;
    const normalised = normaliseIp(ip);
    for (const entry of allowlist) {
        if (entry.includes('/')) {
            if (isInCidr(normalised, entry))
                return true;
        }
        else if (ipsEqual(normalised, normaliseIp(entry))) {
            return true;
        }
    }
    return false;
}
function ipsEqual(a, b) {
    if (a === b)
        return true;
    // Compare IPv6 by canonical bytes so `2001:db8::1` === `2001:db8:0:0:0:0:0:1`.
    if (a.includes(':') && b.includes(':')) {
        const ca = canonicalIpv6(a);
        const cb = canonicalIpv6(b);
        return ca !== null && cb !== null && ca === cb;
    }
    return false;
}
function canonicalIpv6(ip) {
    const bytes = ipv6ToBytes(ip);
    if (!bytes)
        return null;
    let out = '';
    for (let i = 0; i < 16; i++) {
        out += (bytes[i] ?? 0).toString(16).padStart(2, '0');
    }
    return out;
}
function normaliseIp(ip) {
    const trimmed = ip.trim();
    // ::ffff:1.2.3.4 → 1.2.3.4
    if (trimmed.toLowerCase().startsWith('::ffff:')) {
        const v4 = trimmed.slice(7);
        if (isValidIPv4(v4))
            return v4;
    }
    return trimmed;
}
function isValidIPv4(s) {
    const parts = s.split('.').map(Number);
    return parts.length === 4 && parts.every((p) => Number.isInteger(p) && p >= 0 && p <= 255);
}
function isInCidr(ip, cidr) {
    const [range, bitsStr] = cidr.split('/');
    if (!range || !bitsStr)
        return false;
    const bits = parseInt(bitsStr, 10);
    if (Number.isNaN(bits))
        return false;
    if (ip.includes(':') || range.includes(':')) {
        return matchIpv6Cidr(ip, range, bits);
    }
    return matchIpv4Cidr(ip, range, bits);
}
function matchIpv4Cidr(ip, range, bits) {
    if (bits < 0 || bits > 32)
        return false;
    const ipNum = ipv4ToNumber(ip);
    const rangeNum = ipv4ToNumber(range);
    if (ipNum === null || rangeNum === null)
        return false;
    if (bits === 0)
        return true;
    const mask = ~((1 << (32 - bits)) - 1) >>> 0;
    return (ipNum & mask) === (rangeNum & mask);
}
function ipv4ToNumber(ip) {
    if (!isValidIPv4(ip))
        return null;
    const parts = ip.split('.').map(Number);
    return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
}
function matchIpv6Cidr(ip, range, bits) {
    if (bits < 0 || bits > 128)
        return false;
    const ipBytes = ipv6ToBytes(ip);
    const rangeBytes = ipv6ToBytes(range);
    if (!ipBytes || !rangeBytes)
        return false;
    let remaining = bits;
    for (let i = 0; i < 16 && remaining > 0; i++) {
        const take = Math.min(8, remaining);
        const mask = (0xff << (8 - take)) & 0xff;
        if ((ipBytes[i] & mask) !== (rangeBytes[i] & mask))
            return false;
        remaining -= take;
    }
    return true;
}
/** Expand the `::` shorthand and return 16 bytes; null when the input isn't a valid IPv6. */
function ipv6ToBytes(ip) {
    // Reject IPv4-mapped form for the IPv6 path; caller normalises ::ffff:x.x.x.x first.
    if (ip.includes('.'))
        return null;
    const halves = ip.split('::');
    if (halves.length > 2)
        return null;
    const left = halves[0] ? halves[0].split(':') : [];
    const groups = halves.length === 2 ? fillGroups(left, halves[1] ? halves[1].split(':') : []) : left;
    if (groups.length !== 8)
        return null;
    const bytes = new Uint8Array(16);
    for (let i = 0; i < 8; i++) {
        const value = parseInt(groups[i] || '0', 16);
        if (Number.isNaN(value) || value < 0 || value > 0xffff)
            return null;
        bytes[i * 2] = (value >> 8) & 0xff;
        bytes[i * 2 + 1] = value & 0xff;
    }
    return bytes;
}
function fillGroups(left, right) {
    const fill = 8 - left.length - right.length;
    if (fill < 0)
        return [];
    return [...left, ...new Array(fill).fill('0'), ...right];
}
//# sourceMappingURL=ip-allowlist.js.map