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