import { describe, expect, it } from "vitest"; import { PAIRING_INVITATION_PREFIX, createPairingInvitation, pairingInvitationUrl, parsePairingInvitation, } from "./pairing-invitation"; const TOKEN = "AAECAwQFBgcICQoLDA0ODw"; const RELAY = "wss://relay.peachpasswords.com"; // Cross-client compact vector (pinned in the changelog). Adding a top-level key // would change the byte length, so this is a separate vector from the v1 one. const KAT_POPULATED = "peach-pair-v1:eyJyZWxheSI6IndzczovL3JlbGF5LnBlYWNocGFzc3dvcmRzLmNvbSIsInRva2VuIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EdyIsInZhdWx0IjoicG9wdWxhdGVkIn0"; // A pre-vaultStatus invitation as it would have circulated before the field // existed. Older phones parse this and fall back to "populated". const KAT_LEGACY = "peach-pair-v1:eyJyZWxheSI6IndzczovL3JlbGF5LnBlYWNocGFzc3dvcmRzLmNvbSIsInRva2VuIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EdyJ9"; describe("relay pairing invitation", () => { it("pins the cross-client compact JSON/base64url vector", () => { expect(createPairingInvitation(`${RELAY}/`, TOKEN, "populated")).toBe(KAT_POPULATED); expect(parsePairingInvitation(KAT_POPULATED)).toEqual({ relayUrl: RELAY, token: TOKEN, vaultStatus: "populated", }); }); it("round-trips the vaultStatus field in both directions", () => { for (const status of ["empty", "populated"] as const) { const encoded = createPairingInvitation(RELAY, TOKEN, status); const parsed = parsePairingInvitation(encoded); expect(parsed).toEqual({ relayUrl: RELAY, token: TOKEN, vaultStatus: status }); } }); it("defaults to 'populated' for legacy invitations without the field", () => { // Older invitations from before vaultStatus existed still parse cleanly — // they default to "populated" so the phone does the right thing. expect(parsePairingInvitation(KAT_LEGACY)).toEqual({ relayUrl: RELAY, token: TOKEN, vaultStatus: "populated", }); }); it("defaults vaultStatus to 'populated' when omitted from createPairingInvitation", () => { const encoded = createPairingInvitation(RELAY, TOKEN); expect(parsePairingInvitation(encoded)).toEqual({ relayUrl: RELAY, token: TOKEN, vaultStatus: "populated", }); }); it("ignores an unknown vault value rather than failing the parse", () => { const bogus = createRaw({ relay: RELAY, token: TOKEN, vault: "full" }); const parsed = parsePairingInvitation(bogus); expect(parsed).toEqual({ relayUrl: RELAY, token: TOKEN, vaultStatus: "populated" }); }); it("builds a room URL without identity or vault state", () => { const parsed = parsePairingInvitation(KAT_POPULATED); expect(parsed).not.toBeNull(); expect(pairingInvitationUrl(parsed!)).toBe(`${RELAY}/r/${TOKEN}`); expect(KAT_POPULATED).not.toContain("device"); expect(KAT_POPULATED).not.toContain("identity"); expect(KAT_POPULATED).not.toContain("encrypted"); }); it("fails closed on malformed, insecure, or short-token payloads", () => { expect(parsePairingInvitation("not-an-invitation")).toBeNull(); expect(parsePairingInvitation(`${PAIRING_INVITATION_PREFIX}%%%`)).toBeNull(); const insecure = createRaw({ relay: "ws://relay.example.test", token: TOKEN }); expect(parsePairingInvitation(insecure)).toBeNull(); const shortToken = createRaw({ relay: RELAY, token: "too-short" }); expect(parsePairingInvitation(shortToken)).toBeNull(); }); }); function createRaw(value: unknown): string { const bytes = new TextEncoder().encode(JSON.stringify(value)); let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return `${PAIRING_INVITATION_PREFIX}${btoa(binary) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/g, "")}`; }