1package com.peachpasswords.passkeys23import com.peachpasswords.vault.VaultEntry4import kotlinx.serialization.json.Json5import kotlinx.serialization.json.contentOrNull6import kotlinx.serialization.json.jsonArray7import kotlinx.serialization.json.jsonObject8import kotlinx.serialization.json.jsonPrimitive910data class PasskeyCredentialCandidate(11 val entryId: String,12 val title: String,13 val rpId: String,14 val credentialId: String,15 val username: String,16 val displayName: String,17 val lastUsedAt: Long?,18)1920object PasskeyCredentialPlanner {21 private val json = Json { ignoreUnknownKeys = true }2223 fun candidates(entries: List<VaultEntry>, requestJson: String): List<PasskeyCredentialCandidate> {24 val request = runCatching { json.parseToJsonElement(requestJson).jsonObject }.getOrNull() ?: return emptyList()25 val requestedRpId = request["rpId"]?.jsonPrimitive?.contentOrNull?.trim()?.lowercase() ?: return emptyList()26 val allowedIds = runCatching { request["allowCredentials"]?.jsonArray }.getOrNull()27 ?.mapNotNull { item ->28 runCatching { item.jsonObject["id"]?.jsonPrimitive?.contentOrNull }.getOrNull()29 }30 ?.filter { it.isNotBlank() }31 ?.toSet()3233 return entries.asSequence()34 .filter { it.deletedAt == null && it.passkey != null }35 .mapNotNull { entry ->36 val passkey = entry.passkey ?: return@mapNotNull null37 val rpId = passkey.rpId.trim().lowercase()38 val credentialId = passkey.credentialId39 if (rpId != requestedRpId) return@mapNotNull null40 if (!allowedIds.isNullOrEmpty() && credentialId !in allowedIds) return@mapNotNull null41 val username = passkey.userName.ifBlank { entry.username ?: entry.email ?: entry.title }42 PasskeyCredentialCandidate(43 entryId = entry.id,44 title = entry.title,45 rpId = passkey.rpId,46 credentialId = credentialId,47 username = username,48 displayName = passkey.userDisplayName?.takeIf { it.isNotBlank() } ?: entry.title,49 lastUsedAt = entry.lastUsedAt,50 )51 }52 .sortedWith(53 compareByDescending<PasskeyCredentialCandidate> { it.lastUsedAt ?: Long.MIN_VALUE }54 .thenBy { it.displayName.lowercase() }55 .thenBy { it.username.lowercase() },56 )57 .toList()58 }59}