{ "name": "ip-cluster", "type": "registry:ui", "title": "IP Cluster", "description": "Accounts grouped by a shared address or device. The shape is the finding: one node fanning out to eight accounts is multi-accounting, eight accounts each on their own address is a shared ISP.", "category": "Security", "dependencies": [ "lucide-react@^1.39.0" ], "registryDependencies": [ "badge", "lib-motion", "lib-styles", "lib-utils" ], "files": [ { "path": "components/ui/ip-cluster.tsx", "type": "registry:ui", "content": "'use client'\n\nimport { useMemo, type ComponentProps, type ReactNode } from 'react'\nimport { Globe, Monitor, User } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { enterFade } from '@/lib/motion'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Accounts grouped by a shared IP address or device.\n *\n * The shape it draws is the finding: one node fanning out to eight accounts is\n * multi-accounting; eight accounts each on their own address is a coincidence of\n * a shared ISP. A flat list of \"accounts that share an IP\" cannot tell those\n * apart, which is why this is a grouped view rather than a table.\n *\n * Group sizes are printed and the list sorts largest-first, because an analyst\n * opening this has a queue and needs the worst cluster at the top without\n * scanning.\n *\n * Residential and mobile addresses are marked. Carrier-grade NAT puts thousands\n * of unrelated subscribers behind one address, so an unmarked shared-IP finding\n * on a mobile network is close to meaningless — the label is what stops someone\n * banning a whole apartment block.\n */\nexport type ClusterMember = {\n id: string\n label: ReactNode\n detail?: ReactNode\n /** Marks the account under review, so it stands out in its own cluster. */\n focus?: boolean\n status?: 'active' | 'banned' | 'flagged'\n}\n\nexport type Cluster = {\n id: string\n /** The shared value — an IP, a device hash, a payout wallet. */\n value: ReactNode\n kind?: 'ip' | 'device' | 'account'\n /** Shared residential or mobile addresses are far weaker evidence. */\n network?: 'residential' | 'mobile' | 'datacenter' | 'vpn'\n location?: ReactNode\n members: ClusterMember[]\n}\n\nconst KIND_ICON = { ip: Globe, device: Monitor, account: User } as const\n\nconst NETWORK_TONE = {\n datacenter: { color: 'destructive', note: 'datacenter' },\n vpn: { color: 'destructive', note: 'VPN / proxy' },\n residential: { color: 'neutral', note: 'residential' },\n // Worth spelling out: CGNAT puts thousands of strangers on one address.\n mobile: { color: 'amber', note: 'mobile — shared by carrier NAT' },\n} as const\n\nconst STATUS_COLOR = { active: 'neutral', banned: 'destructive', flagged: 'amber' } as const\n\nfunction IpCluster({\n clusters,\n onSelect,\n className,\n ...props\n}: Omit, 'children' | 'onSelect'> & {\n clusters: Cluster[]\n onSelect?: (member: ClusterMember, cluster: Cluster) => void\n}) {\n // Worst cluster first — this is a queue, not a reference.\n const sorted = useMemo(\n () => [...clusters].sort((a, b) => b.members.length - a.members.length),\n [clusters],\n )\n\n return (\n
\n {sorted.map((cluster) => {\n const Icon = KIND_ICON[cluster.kind ?? 'ip']\n const network = cluster.network ? NETWORK_TONE[cluster.network] : undefined\n return (\n \n
\n \n {cluster.value}\n {network && (\n \n {network.note}\n \n )}\n {cluster.location && (\n {cluster.location}\n )}\n \n {cluster.members.length} account{cluster.members.length === 1 ? '' : 's'}\n \n
\n\n
    \n {cluster.members.map((member) => {\n const content = (\n <>\n {member.label}\n {member.detail && (\n {member.detail}\n )}\n {member.status && member.status !== 'active' && (\n \n {member.status}\n \n )}\n \n )\n const classes = cn(\n 'flex max-w-full items-center gap-1.5 px-2 py-1 text-xs',\n radius.control,\n member.focus\n ? 'bg-primary text-primary-foreground'\n : 'bg-secondary text-secondary-foreground',\n onSelect && !member.focus && 'hover:bg-accent',\n )\n\n return (\n
  • \n {onSelect ? (\n onSelect(member, cluster)}\n >\n {content}\n \n ) : (\n {content}\n )}\n
  • \n )\n })}\n
\n \n )\n })}\n
\n )\n}\n\nexport { IpCluster }\n" } ] }