From cb319736a2e160b882cddcb061512843fb654d73 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 1 May 2026 18:29:10 +0700 Subject: [PATCH] Fix native-safe connection offer decoding --- .../src/shared/connection-offer.test.ts | 46 +++++++++++++++++++ .../server/src/shared/connection-offer.ts | 5 +- 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 packages/server/src/shared/connection-offer.test.ts diff --git a/packages/server/src/shared/connection-offer.test.ts b/packages/server/src/shared/connection-offer.test.ts new file mode 100644 index 000000000..0c9c7db83 --- /dev/null +++ b/packages/server/src/shared/connection-offer.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; + +import { + ConnectionOfferSchema, + decodeOfferFragmentPayload, + parseConnectionOfferFromUrl, +} from "./connection-offer.js"; + +function encodeBase64UrlNoPadUtf8(input: string): string { + return Buffer.from(input, "utf8") + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); +} + +describe("connection offer", () => { + it("decodes base64url JSON payloads", () => { + const payload = { + v: 2, + serverId: "server-123", + daemonPublicKeyB64: "pubkey", + relay: { endpoint: "relay.paseo.sh:443" }, + }; + + expect(decodeOfferFragmentPayload(encodeBase64UrlNoPadUtf8(JSON.stringify(payload)))).toEqual( + payload, + ); + }); + + it("parses connection offers from QR-style URLs", () => { + const offer = ConnectionOfferSchema.parse({ + v: 2, + serverId: "server-123", + daemonPublicKeyB64: "pubkey", + relay: { endpoint: "relay.paseo.sh:443" }, + }); + const encoded = encodeBase64UrlNoPadUtf8(JSON.stringify(offer)); + + expect(parseConnectionOfferFromUrl(`https://app.paseo.sh/#offer=${encoded}`)).toEqual(offer); + }); + + it("returns null when the URL has no offer fragment", () => { + expect(parseConnectionOfferFromUrl("https://app.paseo.sh/pair")).toBeNull(); + }); +}); diff --git a/packages/server/src/shared/connection-offer.ts b/packages/server/src/shared/connection-offer.ts index 7cd9372d7..61c1aaf7e 100644 --- a/packages/server/src/shared/connection-offer.ts +++ b/packages/server/src/shared/connection-offer.ts @@ -1,4 +1,3 @@ -import { Buffer } from "node:buffer"; import { z } from "zod"; /** @@ -24,7 +23,9 @@ export type ConnectionOffer = ConnectionOfferV2; function decodeBase64UrlToUtf8(input: string): string { const base64 = input.replace(/-/g, "+").replace(/_/g, "/"); const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "="); - return Buffer.from(padded, "base64").toString("utf8"); + const binary = globalThis.atob(padded); + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); + return new TextDecoder("utf-8", { fatal: true }).decode(bytes); } export function decodeOfferFragmentPayload(encoded: string): unknown {