1import {2 buildRendezvousUrl,3 generatePairingRendezvousToken,4 normalizeRelayUrl,5} from "./rendezvous";67export const PAIRING_INVITATION_PREFIX = "peach-pair-v1:";89export interface PairingInvitation {10 relayUrl: string;11 token: string;12}13141516171819export function createPairingInvitation(20 relayUrl: string,21 token = generatePairingRendezvousToken(),22): string {23 const normalized = normalizeRelayUrl(relayUrl);24 assertToken(token);25 const json = JSON.stringify({ relay: normalized, token });26 return `${PAIRING_INVITATION_PREFIX}${base64urlEncode(new TextEncoder().encode(json))}`;27}2829export function parsePairingInvitation(input: string): PairingInvitation | null {30 const value = input.trim();31 if (!value.startsWith(PAIRING_INVITATION_PREFIX)) return null;32 try {33 const json = new TextDecoder("utf-8", { fatal: true }).decode(34 base64urlDecode(value.slice(PAIRING_INVITATION_PREFIX.length)),35 );36 const parsed = JSON.parse(json) as { relay?: unknown; token?: unknown };37 if (typeof parsed.relay !== "string" || typeof parsed.token !== "string") return null;38 assertToken(parsed.token);39 return {40 relayUrl: normalizeRelayUrl(parsed.relay),41 token: parsed.token,42 };43 } catch {44 return null;45 }46}4748export function pairingInvitationUrl(invitation: PairingInvitation): string {49 return buildRendezvousUrl(invitation.relayUrl, invitation.token);50}5152function assertToken(token: string): void {53 54 if (!/^[A-Za-z0-9_-]{22}$/.test(token)) {55 throw new Error("pairing invitation token must be 128-bit base64url");56 }57}5859function base64urlEncode(bytes: Uint8Array): string {60 let binary = "";61 for (const byte of bytes) binary += String.fromCharCode(byte);62 return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");63}6465function base64urlDecode(value: string): Uint8Array {66 if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("invalid base64url");67 const padded = value.replace(/-/g, "+").replace(/_/g, "/")68 .padEnd(Math.ceil(value.length / 4) * 4, "=");69 const binary = atob(padded);70 return Uint8Array.from(binary, (char) => char.charCodeAt(0));71}