106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
export interface ServerIdentityInput extends Record<string, unknown> {
|
|
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<string, unknown> {
|
|
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<string>();
|
|
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<NormalizedServer, 'id' | 'label'>[],
|
|
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 : '';
|
|
}
|