package com.peachpasswords.credentials import com.peachpasswords.shared.extractDomain import com.peachpasswords.vault.VaultEntry data class PasswordCredentialCandidate( val entryId: String, val title: String, val username: String, val lastUsedAt: Long?, ) /** * Content-based matching for Credential Manager password rows — the same * stance as autofill (AutofillResponseBuilder): registrable-domain matching * for web callers, app-link-domain / app-title matching for native callers. * No caller-trust gate (removed queue #3); the caller package itself is the * framework-authenticated identity. */ object PasswordCredentialPlanner { fun candidates( entries: List, domains: Set, appTitle: String?, allowedUserIds: Set? = null, ): List { val eligible = entries.filter { it.fillable() } val byDomain = if (domains.isEmpty()) { emptyList() } else { eligible.filter { entry -> domains.any { entry.matchesDomain(it) } } } val byTitle = if (appTitle.isNullOrBlank()) { emptyList() } else { eligible.filter { it.title.trim().equals(appTitle, ignoreCase = true) } } return (byDomain + byTitle) .distinctBy { it.id } .mapNotNull { entry -> val username = entry.username ?: entry.email ?: return@mapNotNull null if (!allowedUserIds.isNullOrEmpty() && username !in allowedUserIds) { return@mapNotNull null } PasswordCredentialCandidate( entryId = entry.id, title = entry.title, username = username, lastUsedAt = entry.lastUsedAt, ) } .sortedWith( compareByDescending { it.lastUsedAt ?: Long.MIN_VALUE } .thenBy { it.username.lowercase() }, ) } /** Entries that can actually back a password credential row. */ private fun VaultEntry.fillable(): Boolean = deletedAt == null && !password.isNullOrBlank() private fun VaultEntry.matchesDomain(domain: String): Boolean = sequenceOf(url) .plus(additionalUrls.orEmpty().asSequence()) .filterNotNull() .mapNotNull { extractDomain(it) } .any { it == domain } }