1package com.peachpasswords.sync23import java.util.Base644import kotlinx.serialization.Serializable5import kotlinx.serialization.encodeToString6import kotlinx.serialization.json.Json78private const val PAIRING_INVITATION_PREFIX = "peach-pair-v1:"9private val PairingTokenPattern = Regex("^[A-Za-z0-9_-]{22}$")1011data class PairingInvitation(12 val relayUrl: String,13 val token: String,14)1516@Serializable17private data class PairingInvitationWire(18 val relay: String,19 val token: String,20)21222324252627fun createPairingInvitation(28 relayUrl: String,29 token: String = Rendezvous.newPairingToken(),30): String {31 require(PairingTokenPattern.matches(token)) {32 "pairing invitation token must be 128-bit base64url"33 }34 val wire = PairingInvitationWire(Rendezvous.normalizeRelayUrl(relayUrl), token)35 val encoded = Base64.getUrlEncoder().withoutPadding()36 .encodeToString(Json.encodeToString(wire).toByteArray(Charsets.UTF_8))37 return PAIRING_INVITATION_PREFIX + encoded38}3940fun parsePairingInvitation(input: String): PairingInvitation? {41 val value = input.trim()42 if (!value.startsWith(PAIRING_INVITATION_PREFIX)) return null43 return runCatching {44 val decoded = Base64.getUrlDecoder().decode(45 value.removePrefix(PAIRING_INVITATION_PREFIX),46 )47 val wire = Json.decodeFromString<PairingInvitationWire>(48 String(decoded, Charsets.UTF_8),49 )50 require(PairingTokenPattern.matches(wire.token))51 PairingInvitation(52 relayUrl = Rendezvous.normalizeRelayUrl(wire.relay),53 token = wire.token,54 )55 }.getOrNull()56}5758fun PairingInvitation.roomUrl(): String = Rendezvous.buildUrl(relayUrl, token)