25 lines · 1018 B
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<string>(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`;
}