Merge pull request #2911 from davotoula/fix/nip05-verified-on-error-clean

Stop reporting verification errors as Verified
This commit is contained in:
Vitor Pamplona
2026-05-15 13:03:28 -04:00
committed by GitHub
3 changed files with 79 additions and 7 deletions
@@ -45,7 +45,7 @@ sealed interface Nip05State {
fun markAsVerifying() = verificationState.tryEmit(Nip05VerifState.Verifying(TimeUtils.fiveMinutesAhead())) fun markAsVerifying() = verificationState.tryEmit(Nip05VerifState.Verifying(TimeUtils.fiveMinutesAhead()))
fun markAsError() = verificationState.tryEmit(Nip05VerifState.Verified(TimeUtils.fiveMinutesAhead())) fun markAsError() = verificationState.tryEmit(Nip05VerifState.Error(TimeUtils.fiveMinutesAhead()))
fun reset() = verificationState.tryEmit(Nip05VerifState.NotStarted) fun reset() = verificationState.tryEmit(Nip05VerifState.NotStarted)
@@ -47,14 +47,26 @@ data class Nip05Id(
fun toDomainUrl(): String = domainUrl(domain) fun toDomainUrl(): String = domainUrl(domain)
companion object { companion object {
// NIP-05 localpart: dot-separated atoms of [a-z0-9_-]. Forbids leading,
// trailing, and consecutive dots (NIP-05 + RFC 5321 local-part rules).
private val LOCAL_PART_REGEX = Regex("^[a-z0-9_-]+(\\.[a-z0-9_-]+)*$")
// Hostname: dot-separated labels of [a-z0-9-], no leading/trailing hyphen,
// require at least one dot so single-label garbage (e.g. "s!ayer") is rejected.
private val DOMAIN_REGEX =
Regex("^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$")
fun parse(nip05address: String): Nip05Id? { fun parse(nip05address: String): Nip05Id? {
val parts = nip05address.trim().lowercase().split("@") val parts = nip05address.trim().lowercase().split("@")
if (parts.size != 2) return null
return when (parts.size) { val name = parts[0]
2 -> Nip05Id(parts[0], parts[1]) val domain = parts[1]
1 -> Nip05Id(parts[0], "_") if (!LOCAL_PART_REGEX.matches(name)) return null
else -> null if (!DOMAIN_REGEX.matches(domain)) return null
} // Reject IP literals — NIP-05 expects a hostname, and a digits-only
// TLD is the cheapest way to tell them apart from real domains.
if (domain.substringAfterLast('.').all { it.isDigit() }) return null
return Nip05Id(name, domain)
} }
fun assemble( fun assemble(
@@ -94,6 +94,66 @@ class Nip05Test {
assertNull(parsedNip05) assertNull(parsedNip05)
} }
@Test
fun `parse rejects empty localpart`() {
assertNull(Nip05Id.parse("@example.com"))
}
@Test
fun `parse rejects empty domain`() {
assertNull(Nip05Id.parse("alice@"))
}
@Test
fun `parse rejects domain without a dot`() {
assertNull(Nip05Id.parse("alice@localhost"))
}
@Test
fun `parse rejects domain with illegal character`() {
assertNull(Nip05Id.parse("_@s!ayer"))
assertNull(Nip05Id.parse("@s!ayer"))
}
@Test
fun `parse rejects localpart with illegal character`() {
assertNull(Nip05Id.parse("al!ce@example.com"))
assertNull(Nip05Id.parse("alice space@example.com"))
}
@Test
fun `parse rejects bare string without at-sign`() {
assertNull(Nip05Id.parse("alice"))
assertNull(Nip05Id.parse("example.com"))
}
@Test
fun `parse rejects domain label with leading or trailing hyphen`() {
assertNull(Nip05Id.parse("alice@-example.com"))
assertNull(Nip05Id.parse("alice@example-.com"))
}
@Test
fun `parse rejects localpart with leading trailing or consecutive dots`() {
assertNull(Nip05Id.parse(".alice@example.com"))
assertNull(Nip05Id.parse("alice.@example.com"))
assertNull(Nip05Id.parse("alice..bob@example.com"))
}
@Test
fun `parse rejects IPv4 literal as domain`() {
assertNull(Nip05Id.parse("alice@192.168.1.1"))
assertNull(Nip05Id.parse("alice@8.8.8.8"))
}
@Test
fun `parse accepts wildcard underscore localpart`() {
val nip05 = Nip05Id.parse("_@example.com")
assertNotNull(nip05)
assertEquals("_", nip05.name)
assertEquals("example.com", nip05.domain)
}
@Test @Test
fun `execute assemble url with valid value returns nip05 url`() { fun `execute assemble url with valid value returns nip05 url`() {
// given // given