feat(cli): connect via relay using pairing offer URL (#639)

Pass `--host https://app.paseo.sh/#offer=...` (or `PASEO_HOST=...`) to
any CLI command and the connection routes through the relay with E2EE,
the same way the mobile and desktop apps connect. Same `paseo daemon
pair` link the app already accepts — no new pairing flow.

Closes #181
This commit is contained in:
Mohamed Boudra
2026-04-30 21:37:51 +08:00
committed by GitHub
parent c2ad4a02ef
commit 30f7842e8f
7 changed files with 373 additions and 27 deletions
@@ -1,3 +1,4 @@
import { Buffer } from "node:buffer";
import { z } from "zod";
/**
@@ -19,3 +20,38 @@ export type ConnectionOfferV2 = z.infer<typeof ConnectionOfferV2Schema>;
export const ConnectionOfferSchema = ConnectionOfferV2Schema;
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");
}
export function decodeOfferFragmentPayload(encoded: string): unknown {
const json = decodeBase64UrlToUtf8(encoded);
return JSON.parse(json) as unknown;
}
const OFFER_FRAGMENT_PREFIX = "#offer=";
function extractOfferFragmentEncoded(input: string): string | null {
const trimmed = input.trim();
if (!trimmed) return null;
const fragmentIndex = trimmed.indexOf(OFFER_FRAGMENT_PREFIX);
if (fragmentIndex === -1) return null;
const encoded = trimmed.slice(fragmentIndex + OFFER_FRAGMENT_PREFIX.length).trim();
return encoded.length > 0 ? encoded : null;
}
/**
* Parse a pairing-offer URL of the form `https://app.paseo.sh/#offer=<base64url>`.
*
* Returns `null` if the input has no `#offer=` fragment. Throws if the fragment
* exists but the payload is malformed or fails schema validation.
*/
export function parseConnectionOfferFromUrl(input: string): ConnectionOffer | null {
const encoded = extractOfferFragmentEncoded(input);
if (!encoded) return null;
const payload = decodeOfferFragmentPayload(encoded);
return ConnectionOfferSchema.parse(payload);
}