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:
@@ -6,12 +6,20 @@ export { getOrCreateServerId } from "./server-id.js";
|
||||
export { createRootLogger, type LogLevel, type LogFormat } from "./logger.js";
|
||||
export { loadPersistedConfig, type PersistedConfig } from "./persisted-config.js";
|
||||
export { generateLocalPairingOffer, type LocalPairingOffer } from "./pairing-offer.js";
|
||||
export {
|
||||
ConnectionOfferSchema,
|
||||
decodeOfferFragmentPayload,
|
||||
parseConnectionOfferFromUrl,
|
||||
type ConnectionOffer,
|
||||
} from "../shared/connection-offer.js";
|
||||
export { buildRelayWebSocketUrl } from "../shared/daemon-endpoints.js";
|
||||
export {
|
||||
DaemonClient,
|
||||
type DaemonClientConfig,
|
||||
type ConnectionState,
|
||||
type DaemonEvent,
|
||||
} from "../client/daemon-client.js";
|
||||
export type { WebSocketFactory, WebSocketLike } from "../client/daemon-client-transport-types.js";
|
||||
export {
|
||||
ensureLocalSpeechModels,
|
||||
listLocalSpeechModels,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user