const IPV6_GROUPS = 8; const IPV6_PREFIX_GROUPS = 4; /** * An IPv6 client typically holds a whole /64, so keying on the full address lets it rotate past every * per-IP limit. IPv6 collapses to its /64 prefix; IPv4 stays exact. */ export function ipBucket(ip: string): string { if (!ip.includes(":") || ip.includes(".")) return ip; const halves = ip.toLowerCase().split("::"); if (halves.length > 2) return ip; const [head, tail = ""] = halves; const left = head ? head.split(":") : []; const right = tail ? tail.split(":") : []; if ([...left, ...right].some((group) => !/^[0-9a-f]{1,4}$/.test(group))) return ip; if (!ip.includes("::") && left.length !== IPV6_GROUPS) return ip; const fill = Math.max(0, IPV6_GROUPS - left.length - right.length); const groups = [...left, ...Array(fill).fill("0"), ...right]; if (groups.length !== IPV6_GROUPS) return ip; return `${groups .slice(0, IPV6_PREFIX_GROUPS) .map((group) => group.replace(/^0+(?=.)/, "")) .join(":")}::/64`; }