import { buildRendezvousUrl, generatePairingRendezvousToken, normalizeRelayUrl, } from "./rendezvous"; export const PAIRING_INVITATION_PREFIX = "peach-pair-v1:"; export interface PairingInvitation { relayUrl: string; token: string; } /** * QR/manual first-pair payload. It contains reachability only: a credential-free * relay URL and a fresh 128-bit bearer token. No identity or vault material is * present before Noise and the two-person SAS gate. */ export function createPairingInvitation( relayUrl: string, token = generatePairingRendezvousToken(), ): string { const normalized = normalizeRelayUrl(relayUrl); assertToken(token); const json = JSON.stringify({ relay: normalized, token }); return `${PAIRING_INVITATION_PREFIX}${base64urlEncode(new TextEncoder().encode(json))}`; } export function parsePairingInvitation(input: string): PairingInvitation | null { const value = input.trim(); if (!value.startsWith(PAIRING_INVITATION_PREFIX)) return null; try { const json = new TextDecoder("utf-8", { fatal: true }).decode( base64urlDecode(value.slice(PAIRING_INVITATION_PREFIX.length)), ); const parsed = JSON.parse(json) as { relay?: unknown; token?: unknown }; if (typeof parsed.relay !== "string" || typeof parsed.token !== "string") return null; assertToken(parsed.token); return { relayUrl: normalizeRelayUrl(parsed.relay), token: parsed.token, }; } catch { return null; } } export function pairingInvitationUrl(invitation: PairingInvitation): string { return buildRendezvousUrl(invitation.relayUrl, invitation.token); } function assertToken(token: string): void { // A first-pair token is exactly 16 bytes encoded without padding. if (!/^[A-Za-z0-9_-]{22}$/.test(token)) { throw new Error("pairing invitation token must be 128-bit base64url"); } } function base64urlEncode(bytes: Uint8Array): string { let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } function base64urlDecode(value: string): Uint8Array { if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("invalid base64url"); const padded = value.replace(/-/g, "+").replace(/_/g, "/") .padEnd(Math.ceil(value.length / 4) * 4, "="); const binary = atob(padded); return Uint8Array.from(binary, (char) => char.charCodeAt(0)); }