123456789101112131415161718192021222324252627const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";28const IP_BYTES = 4;29const PORT_BYTES = 2;30const MIN_TOKEN_BYTES = 4;3132export interface PairingCode {33 ip: string;34 port: number;35 36 tokenHex: string;37}38394041424344export function parsePairingCode(input: string): PairingCode | null {45 const bytes = decodeCrockford(input);46 if (!bytes || bytes.length < IP_BYTES + PORT_BYTES + MIN_TOKEN_BYTES) return null;47 const port = (bytes[4] << 8) | bytes[5];48 if (port === 0) return null;49 return {50 ip: `${bytes[0]}.${bytes[1]}.${bytes[2]}.${bytes[3]}`,51 port,52 tokenHex: toHex(bytes.subarray(IP_BYTES + PORT_BYTES)),53 };54}555657export function encodePairingCode(ip: string, port: number, token: Uint8Array): string {58 const octets = ip.split(".");59 if (octets.length !== IP_BYTES) throw new Error("ip must be dotted-quad IPv4");60 const bytes = new Uint8Array(IP_BYTES + PORT_BYTES + token.length);61 for (let i = 0; i < IP_BYTES; i++) {62 const n = Number(octets[i]);63 if (!Number.isInteger(n) || n < 0 || n > 255) throw new Error(`invalid IPv4 octet: ${octets[i]}`);64 bytes[i] = n;65 }66 if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`invalid port: ${port}`);67 bytes[4] = (port >> 8) & 0xff;68 bytes[5] = port & 0xff;69 if (token.length < MIN_TOKEN_BYTES) throw new Error(`token must be at least ${MIN_TOKEN_BYTES} bytes`);70 bytes.set(token, IP_BYTES + PORT_BYTES);71 return group(encodeCrockford(bytes));72}73747576777879export function formatPairingCode(input: string): string {80 return group(input.toUpperCase().replace(/[^0-9A-Z]/g, ""));81}8283function group(chars: string): string {84 return chars.replace(/(.{4})(?=.)/g, "$1-");85}8687function decodeCrockford(input: string): Uint8Array | null {88 const clean = input89 .toUpperCase()90 .replace(/[\s-]/g, "")91 .replace(/[ILO]/g, (c) => (c === "O" ? "0" : "1"));92 if (clean.length === 0) return null;9394 95 const out = new Uint8Array(Math.floor((clean.length * 5) / 8));96 let acc = 0;97 let bits = 0;98 let o = 0;99 for (const ch of clean) {100 const v = ALPHABET.indexOf(ch);101 if (v < 0) return null;102 acc = (acc << 5) | v;103 bits += 5;104 if (bits >= 8) {105 bits -= 8;106 out[o++] = (acc >> bits) & 0xff;107 }108 }109 return out;110}111112function encodeCrockford(bytes: Uint8Array): string {113 let s = "";114 let acc = 0;115 let bits = 0;116 for (const b of bytes) {117 acc = (acc << 8) | b;118 bits += 8;119 while (bits >= 5) {120 bits -= 5;121 s += ALPHABET[(acc >> bits) & 0x1f];122 }123 }124 if (bits > 0) s += ALPHABET[(acc << (5 - bits)) & 0x1f];125 return s;126}127128function toHex(bytes: Uint8Array): string {129 let s = "";130 for (const b of bytes) s += b.toString(16).padStart(2, "0");131 return s;132}