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";12const STATIC_KEY = "01".repeat(32);131415const KAT_POPULATED =16 "peach-pair-v2:eyJyZWxheSI6IndzczovL3JlbGF5LnBlYWNocGFzc3dvcmRzLmNvbSIsInRva2VuIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EdyIsInZhdWx0IjoicG9wdWxhdGVkIn0";171819const KAT_LEGACY =20 "peach-pair-v2:eyJyZWxheSI6IndzczovL3JlbGF5LnBlYWNocGFzc3dvcmRzLmNvbSIsInRva2VuIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EdyJ9";2122describe("relay pairing invitation", () => {23 it("pins the cross-client compact JSON/base64url vector", () => {24 expect(createPairingInvitation(`${RELAY}/`, TOKEN, "populated")).toBe(KAT_POPULATED);25 expect(parsePairingInvitation(KAT_POPULATED)).toEqual({26 relayUrl: RELAY,27 token: TOKEN,28 vaultStatus: "populated",29 });30 });3132 it("round-trips the vaultStatus field in both directions", () => {33 for (const status of ["empty", "populated"] as const) {34 const encoded = createPairingInvitation(RELAY, TOKEN, status);35 const parsed = parsePairingInvitation(encoded);36 expect(parsed).toEqual({ relayUrl: RELAY, token: TOKEN, vaultStatus: status });37 }38 });3940 it("binds a validated Noise static key into new invitations", () => {41 const encoded = createPairingInvitation(RELAY, TOKEN, "populated", STATIC_KEY);42 expect(parsePairingInvitation(encoded)).toEqual({43 relayUrl: RELAY,44 token: TOKEN,45 vaultStatus: "populated",46 noiseStaticPublicKeyHex: STATIC_KEY,47 });48 expect(parsePairingInvitation(createRaw({49 relay: RELAY,50 token: TOKEN,51 vault: "populated",52 static: "not-a-key",53 }))).toBeNull();54 });5556 it("defaults to 'populated' for legacy invitations without the field", () => {57 58 59 expect(parsePairingInvitation(KAT_LEGACY)).toEqual({60 relayUrl: RELAY,61 token: TOKEN,62 vaultStatus: "populated",63 });64 });6566 it("defaults vaultStatus to 'populated' when omitted from createPairingInvitation", () => {67 const encoded = createPairingInvitation(RELAY, TOKEN);68 expect(parsePairingInvitation(encoded)).toEqual({69 relayUrl: RELAY,70 token: TOKEN,71 vaultStatus: "populated",72 });73 });7475 it("ignores an unknown vault value rather than failing the parse", () => {76 const bogus = createRaw({ relay: RELAY, token: TOKEN, vault: "full" });77 const parsed = parsePairingInvitation(bogus);78 expect(parsed).toEqual({ relayUrl: RELAY, token: TOKEN, vaultStatus: "populated" });79 });8081 it("builds a room URL without identity or vault state", () => {82 const parsed = parsePairingInvitation(KAT_POPULATED);83 expect(parsed).not.toBeNull();84 expect(pairingInvitationUrl(parsed!)).toBe(`${RELAY}/r/${TOKEN}`);85 expect(KAT_POPULATED).not.toContain("device");86 expect(KAT_POPULATED).not.toContain("identity");87 expect(KAT_POPULATED).not.toContain("encrypted");88 });8990 it("fails closed on malformed, insecure, or short-token payloads", () => {91 expect(parsePairingInvitation("not-an-invitation")).toBeNull();92 expect(parsePairingInvitation(`${PAIRING_INVITATION_PREFIX}%%%`)).toBeNull();93 const insecure = createRaw({ relay: "ws://relay.example.test", token: TOKEN });94 expect(parsePairingInvitation(insecure)).toBeNull();95 const shortToken = createRaw({ relay: RELAY, token: "too-short" });96 expect(parsePairingInvitation(shortToken)).toBeNull();97 });98});99100function createRaw(value: unknown): string {101 const bytes = new TextEncoder().encode(JSON.stringify(value));102 let binary = "";103 for (const byte of bytes) binary += String.fromCharCode(byte);104 return `${PAIRING_INVITATION_PREFIX}${btoa(binary)105 .replace(/\+/g, "-")106 .replace(/\//g, "_")107 .replace(/=+$/g, "")}`;108}