pairing-invitation.ts

2.4 KB · raw

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}1314/**15 * QR/manual first-pair payload. It contains reachability only: a credential-free16 * relay URL and a fresh 128-bit bearer token. No identity or vault material is17 * present before Noise and the two-person SAS gate.18 */19export 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  // A first-pair token is exactly 16 bytes encoded without padding.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}