1import { describe, expect, it } from "vitest";23import {4 PAIRING_INVITATION_PREFIX,5 createPairingInvitation,6 pairingInvitationUrl,7 parsePairingInvitation,8} from "./pairing-invitation";910const TOKEN = "AAECAwQFBgcICQoLDA0ODw";11const RELAY = "wss://relay.peachpasswords.com";121314const KAT_POPULATED =15 "peach-pair-v1:eyJyZWxheSI6IndzczovL3JlbGF5LnBlYWNocGFzc3dvcmRzLmNvbSIsInRva2VuIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EdyIsInZhdWx0IjoicG9wdWxhdGVkIn0";161718const KAT_LEGACY =19 "peach-pair-v1:eyJyZWxheSI6IndzczovL3JlbGF5LnBlYWNocGFzc3dvcmRzLmNvbSIsInRva2VuIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EdyJ9";2021describe("relay pairing invitation", () => {22 it("pins the cross-client compact JSON/base64url vector", () => {23 expect(createPairingInvitation(`${RELAY}/`, TOKEN, "populated")).toBe(KAT_POPULATED);24 expect(parsePairingInvitation(KAT_POPULATED)).toEqual({25 relayUrl: RELAY,26 token: TOKEN,27 vaultStatus: "populated",28 });29 });3031 it("round-trips the vaultStatus field in both directions", () => {32 for (const status of ["empty", "populated"] as const) {33 const encoded = createPairingInvitation(RELAY, TOKEN, status);34 const parsed = parsePairingInvitation(encoded);35 expect(parsed).toEqual({ relayUrl: RELAY, token: TOKEN, vaultStatus: status });36 }37 });3839 it("defaults to 'populated' for legacy invitations without the field", () => {40 41 42 expect(parsePairingInvitation(KAT_LEGACY)).toEqual({43 relayUrl: RELAY,44 token: TOKEN,45 vaultStatus: "populated",46 });47 });4849 it("defaults vaultStatus to 'populated' when omitted from createPairingInvitation", () => {50 const encoded = createPairingInvitation(RELAY, TOKEN);51 expect(parsePairingInvitation(encoded)).toEqual({52 relayUrl: RELAY,53 token: TOKEN,54 vaultStatus: "populated",55 });56 });5758 it("ignores an unknown vault value rather than failing the parse", () => {59 const bogus = createRaw({ relay: RELAY, token: TOKEN, vault: "full" });60 const parsed = parsePairingInvitation(bogus);61 expect(parsed).toEqual({ relayUrl: RELAY, token: TOKEN, vaultStatus: "populated" });62 });6364 it("builds a room URL without identity or vault state", () => {65 const parsed = parsePairingInvitation(KAT_POPULATED);66 expect(parsed).not.toBeNull();67 expect(pairingInvitationUrl(parsed!)).toBe(`${RELAY}/r/${TOKEN}`);68 expect(KAT_POPULATED).not.toContain("device");69 expect(KAT_POPULATED).not.toContain("identity");70 expect(KAT_POPULATED).not.toContain("encrypted");71 });7273 it("fails closed on malformed, insecure, or short-token payloads", () => {74 expect(parsePairingInvitation("not-an-invitation")).toBeNull();75 expect(parsePairingInvitation(`${PAIRING_INVITATION_PREFIX}%%%`)).toBeNull();76 const insecure = createRaw({ relay: "ws://relay.example.test", token: TOKEN });77 expect(parsePairingInvitation(insecure)).toBeNull();78 const shortToken = createRaw({ relay: RELAY, token: "too-short" });79 expect(parsePairingInvitation(shortToken)).toBeNull();80 });81});8283function createRaw(value: unknown): string {84 const bytes = new TextEncoder().encode(JSON.stringify(value));85 let binary = "";86 for (const byte of bytes) binary += String.fromCharCode(byte);87 return `${PAIRING_INVITATION_PREFIX}${btoa(binary)88 .replace(/\+/g, "-")89 .replace(/\//g, "_")90 .replace(/=+$/g, "")}`;91}