package com.peachpasswords.passkeys import com.peachpasswords.vault.VaultEntry import kotlinx.serialization.json.Json import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive data class PasskeyCredentialCandidate( val entryId: String, val title: String, val rpId: String, val credentialId: String, val username: String, val displayName: String, val lastUsedAt: Long?, ) object PasskeyCredentialPlanner { private val json = Json { ignoreUnknownKeys = true } fun candidates(entries: List, requestJson: String): List { val request = runCatching { json.parseToJsonElement(requestJson).jsonObject }.getOrNull() ?: return emptyList() val requestedRpId = request["rpId"]?.jsonPrimitive?.contentOrNull?.trim()?.lowercase() ?: return emptyList() val allowedIds = runCatching { request["allowCredentials"]?.jsonArray }.getOrNull() ?.mapNotNull { item -> runCatching { item.jsonObject["id"]?.jsonPrimitive?.contentOrNull }.getOrNull() } ?.filter { it.isNotBlank() } ?.toSet() return entries.asSequence() .filter { it.deletedAt == null && it.passkey != null } .mapNotNull { entry -> val passkey = entry.passkey ?: return@mapNotNull null val rpId = passkey.rpId.trim().lowercase() val credentialId = passkey.credentialId if (rpId != requestedRpId) return@mapNotNull null if (!allowedIds.isNullOrEmpty() && credentialId !in allowedIds) return@mapNotNull null val username = passkey.userName.ifBlank { entry.username ?: entry.email ?: entry.title } PasskeyCredentialCandidate( entryId = entry.id, title = entry.title, rpId = passkey.rpId, credentialId = credentialId, username = username, displayName = passkey.userDisplayName?.takeIf { it.isNotBlank() } ?: entry.title, lastUsedAt = entry.lastUsedAt, ) } .sortedWith( compareByDescending { it.lastUsedAt ?: Long.MIN_VALUE } .thenBy { it.displayName.lowercase() } .thenBy { it.username.lowercase() }, ) .toList() } }