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}$")1011121314151617181920enum class VaultStatus { EMPTY, POPULATED }2122data class PairingInvitation(23 val relayUrl: String,24 val token: String,25 val vaultStatus: VaultStatus = VaultStatus.POPULATED,26)2728@Serializable29private data class PairingInvitationWire(30 val relay: String,31 val token: String,32 val vault: String? = null,33)3435363738394041fun createPairingInvitation(42 relayUrl: String,43 token: String = Rendezvous.newPairingToken(),44 vaultStatus: VaultStatus = VaultStatus.POPULATED,45): String {46 require(PairingTokenPattern.matches(token)) {47 "pairing invitation token must be 128-bit base64url"48 }49 val wire = PairingInvitationWire(50 relay = Rendezvous.normalizeRelayUrl(relayUrl),51 token = token,52 vault = wireVaultStatus(vaultStatus),53 )54 val encoded = Base64.getUrlEncoder().withoutPadding()55 .encodeToString(Json.encodeToString(wire).toByteArray(Charsets.UTF_8))56 return PAIRING_INVITATION_PREFIX + encoded57}5859fun parsePairingInvitation(input: String): PairingInvitation? {60 val value = input.trim()61 if (!value.startsWith(PAIRING_INVITATION_PREFIX)) return null62 return runCatching {63 val decoded = Base64.getUrlDecoder().decode(64 value.removePrefix(PAIRING_INVITATION_PREFIX),65 )66 val wire = Json.decodeFromString<PairingInvitationWire>(67 String(decoded, Charsets.UTF_8),68 )69 require(PairingTokenPattern.matches(wire.token))70 PairingInvitation(71 relayUrl = Rendezvous.normalizeRelayUrl(wire.relay),72 token = wire.token,73 vaultStatus = parseVaultStatus(wire.vault),74 )75 }.getOrNull()76}7778private fun wireVaultStatus(status: VaultStatus): String = when (status) {79 VaultStatus.EMPTY -> "empty"80 VaultStatus.POPULATED -> "populated"81}8283private fun parseVaultStatus(raw: String?): VaultStatus = when (raw) {84 "empty" -> VaultStatus.EMPTY85 "populated" -> VaultStatus.POPULATED86 87 88 else -> VaultStatus.POPULATED89}9091fun PairingInvitation.roomUrl(): String = Rendezvous.buildUrl(relayUrl, token)