export interface ServerIdentityInput extends Record { id?: unknown; label?: unknown; tag?: unknown; host?: unknown; server?: unknown; port?: unknown; server_port?: unknown; protocol?: unknown; type?: unknown; country?: unknown; city?: unknown; provider?: unknown; } export interface NormalizedServer extends Record { id: string; label: string; host: string; port: number; protocol: string; tag: string; server: string; server_port: number; type: string; } const text = (value: unknown) => String(value || '').trim(); function record(value: unknown): ServerIdentityInput { return value && typeof value === 'object' && !Array.isArray(value) ? value as ServerIdentityInput : {}; } function hash64(value: string) { let hash = 0xcbf29ce484222325n; for (let index = 0; index < value.length; index += 1) { hash ^= BigInt(value.charCodeAt(index)); hash = BigInt.asUintN(64, hash * 0x100000001b3n); } return hash.toString(16).padStart(16, '0'); } export function serverIdentityKey(value: unknown) { const server = record(value); const protocol = text(server.protocol || server.type).toLowerCase(); const host = text(server.host || server.server).toLowerCase(); const port = Number(server.port || server.server_port) || 0; return `${protocol}\u0000${host}\u0000${port}`; } export function createServerId(server: unknown) { return `srv_${hash64(`endpoint\u0000${serverIdentityKey(server)}`)}`; } export function normalizeServer(server: unknown): NormalizedServer { const source = record(server); const protocol = text(source.protocol || source.type).toLowerCase(); const host = text(source.host || source.server); const port = Number(source.port || source.server_port) || 0; const id = text(source.id) || createServerId(source); const label = text(source.label || source.tag) || host; const metadata = Object.fromEntries([ ['country', text(source.country)], ['city', text(source.city)], ['provider', text(source.provider)], ].filter(([, value]) => value)); return { id, label, host, port, protocol, ...metadata, // v0 aliases remain until old clients no longer consume this API. tag: label, server: host, server_port: port, type: protocol, }; } export function normalizeServers(servers: unknown): NormalizedServer[] { const seen = new Set(); return (Array.isArray(servers) ? servers : []).flatMap((server) => { const normalized = normalizeServer(server); if (!normalized.id || seen.has(normalized.id)) return []; seen.add(normalized.id); return [normalized]; }); } export function resolveServerId( servers: readonly Pick[], serverId: unknown, legacyTag: unknown = '', ) { const id = text(serverId); if (id) return servers.some((server) => server.id === id) ? id : ''; const tag = text(legacyTag); const matches = tag ? servers.filter((server) => server.label === tag) : []; return matches.length === 1 ? matches[0].id : ''; }