import ipaddr from 'ipaddr.js';
/**
 * Normalize a raw IP into a canonical string. IPv4-mapped IPv6 addresses
 * (e.g. "::ffff:1.2.3.4") collapse to their IPv4 form so the same client
 * compares equal regardless of socket family.
 */
export function normalizeIp(raw) {
    if (!raw) {
        return null;
    }
    const trimmed = raw.trim();
    if (!ipaddr.isValid(trimmed)) {
        return null;
    }
    let addr = ipaddr.parse(trimmed);
    if (addr.kind() === 'ipv6' && addr.isIPv4MappedAddress()) {
        addr = addr.toIPv4Address();
    }
    return addr.toNormalizedString();
}
/** Validate that a string is a bare IP or CIDR (e.g. "1.2.3.4" or "10.0.0.0/8"). */
export function isValidIpRule(value) {
    const trimmed = value.trim();
    if (trimmed.includes('/')) {
        try {
            ipaddr.parseCIDR(trimmed);
            return true;
        }
        catch {
            return false;
        }
    }
    return ipaddr.isValid(trimmed);
}
/** True if `ip` matches the rule, which may be a bare IP or a CIDR range. */
function ipMatchesRule(ip, rule) {
    const trimmed = rule.trim();
    let candidate;
    try {
        candidate = ipaddr.parse(ip);
    }
    catch {
        return false;
    }
    if (trimmed.includes('/')) {
        let range;
        try {
            range = ipaddr.parseCIDR(trimmed);
        }
        catch {
            return false;
        }
        if (candidate.kind() !== range[0].kind()) {
            return false;
        }
        return candidate.match(range);
    }
    const normalizedRule = normalizeIp(trimmed);
    return normalizedRule !== null && normalizedRule === normalizeIp(ip);
}
export function ipMatchesAny(ip, rules) {
    if (!ip) {
        return false;
    }
    return rules.some((rule) => ipMatchesRule(ip, rule.value));
}
// Default prefix lengths used to decide whether a new IP "looks like" the
// same network as the one a session was bound to. /24 for IPv4 covers the
// common residential / CGNAT rotation case (ISP keeps reassigning addresses
// inside the same /24 every few minutes) without letting a totally unrelated
// network reuse the session. /48 for IPv6 matches the canonical site prefix.
export const DEFAULT_SESSION_IP_MASK_V4 = 24;
export const DEFAULT_SESSION_IP_MASK_V6 = 48;
// True when `candidate` falls inside `reference`'s /prefix subnet of the
// matching address family. Both inputs must already be normalized strings
// (use `normalizeIp` first). Returns false when families differ, either
// input fails to parse, or the prefix is out of range — callers treat that
// as "no match" rather than throwing, so a malformed value can't accidentally
// let a session through.
export function isWithinSessionNetwork(candidate, reference, ipv4Bits = DEFAULT_SESSION_IP_MASK_V4, ipv6Bits = DEFAULT_SESSION_IP_MASK_V6) {
    if (candidate === reference) {
        return true;
    }
    let candAddr;
    let refAddr;
    try {
        candAddr = ipaddr.parse(candidate);
        refAddr = ipaddr.parse(reference);
    }
    catch {
        return false;
    }
    if (candAddr.kind() !== refAddr.kind()) {
        return false;
    }
    const bits = candAddr.kind() === 'ipv4' ? ipv4Bits : ipv6Bits;
    const maxBits = candAddr.kind() === 'ipv4' ? 32 : 128;
    if (!Number.isInteger(bits) || bits < 0 || bits > maxBits) {
        return false;
    }
    return candAddr.match([refAddr, bits]);
}
//# sourceMappingURL=ipMatch.js.map