| 168 | // rotate the lower 64 host bits via SLAAC privacy extensions (RFC 8981), so |
| 169 | // grouping by /64 collapses those rotations into one key. |
| 170 | function ipPrefix(ip: string | undefined) { |
| 171 | if (!ip) return undefined; |
| 172 | if (ip.includes(".") && !ip.includes(":")) return `${ip}/32`; |
| 173 | if (!ip.includes(":")) return undefined; |
| 174 | |
| 175 | // Expand "::" to its full form, then keep the first 4 hextets. |
| 176 | const [head, tail] = ip.split("::") as [string, string | undefined]; |
| 177 | const headParts = head ? head.split(":") : []; |
| 178 | const tailParts = tail !== undefined ? tail.split(":") : []; |
| 179 | const missing = 8 - headParts.length - tailParts.length; |
| 180 | if (missing < 0) return undefined; |
| 181 | const full = [...headParts, ...new Array(missing).fill("0"), ...tailParts]; |
| 182 | if (full.length !== 8) return undefined; |
| 183 | |
| 184 | const prefix = full |
| 185 | .slice(0, 4) |
| 186 | .map((part) => part.toLowerCase().replace(/^0+(?=.)/, "")) |
| 187 | .join(":"); |
| 188 | return `${prefix}::/64`; |
| 189 | } |
| 190 | |
| 191 | function string(value: string | undefined) { |
| 192 | if (typeof value === "string") return value; |