package com.peachpasswords.sync import java.util.Base64 import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json private const val PAIRING_INVITATION_PREFIX = "peach-pair-v1:" private val PairingTokenPattern = Regex("^[A-Za-z0-9_-]{22}$") data class PairingInvitation( val relayUrl: String, val token: String, ) @Serializable private data class PairingInvitationWire( val relay: String, val token: String, ) /** * QR/manual first-pair payload. Only reachability is present before Noise: * a credential-free wss relay and a fresh 128-bit token. Never add identity or * vault fields here. */ fun createPairingInvitation( relayUrl: String, token: String = Rendezvous.newPairingToken(), ): String { require(PairingTokenPattern.matches(token)) { "pairing invitation token must be 128-bit base64url" } val wire = PairingInvitationWire(Rendezvous.normalizeRelayUrl(relayUrl), token) val encoded = Base64.getUrlEncoder().withoutPadding() .encodeToString(Json.encodeToString(wire).toByteArray(Charsets.UTF_8)) return PAIRING_INVITATION_PREFIX + encoded } fun parsePairingInvitation(input: String): PairingInvitation? { val value = input.trim() if (!value.startsWith(PAIRING_INVITATION_PREFIX)) return null return runCatching { val decoded = Base64.getUrlDecoder().decode( value.removePrefix(PAIRING_INVITATION_PREFIX), ) val wire = Json.decodeFromString( String(decoded, Charsets.UTF_8), ) require(PairingTokenPattern.matches(wire.token)) PairingInvitation( relayUrl = Rendezvous.normalizeRelayUrl(wire.relay), token = wire.token, ) }.getOrNull() } fun PairingInvitation.roomUrl(): String = Rendezvous.buildUrl(relayUrl, token)