fix(namecoin): accept single-identity {nostr:{pubkey,relays}} on d/ names
ifa-0001 doesn't mandate that domain records use the nostr.names
sub-dictionary. Operators who own a name outright commonly publish:
{"nostr": {"pubkey": "<hex>", "relays": ["wss://..."]}}
(the same shape id/ records use). Before this fix d/-namespace branch
only accepted {"nostr":"<hex>"} or {"nostr":{"names":{...}}},
so a record like d/mstrofnone with the single-identity object form
silently failed with NoNostrField even though id/mstrofnone resolved
fine.
Resolution rules:
1. nostr.names wins for any sub-identity.
2. Root lookups fall back to bare pubkey when names["_"] is absent.
3. Non-root lookups against names-only or single-identity records
do NOT silently use the bare pubkey.
This commit is contained in:
+68
-35
@@ -281,8 +281,16 @@ class NamecoinNameResolver(
|
|||||||
* Extract Nostr data from a `d/` domain value.
|
* Extract Nostr data from a `d/` domain value.
|
||||||
*
|
*
|
||||||
* Supports:
|
* Supports:
|
||||||
* { "nostr": "hex-pubkey" } → simple form
|
* { "nostr": "hex-pubkey" } → simple-string form
|
||||||
* { "nostr": { "names": { "alice": "hex" }, ... } } → extended NIP-05-like form
|
* { "nostr": { "names": { "alice": "hex" }, ... } } → extended NIP-05-like form
|
||||||
|
* { "nostr": { "pubkey": "hex", "relays": [...] } } → single-identity form
|
||||||
|
*
|
||||||
|
* The single-identity form is the same shape used by `id/` records and is
|
||||||
|
* the natural way to express "this one name = this one pubkey". It only
|
||||||
|
* resolves the root local-part (`_`) — there are no sub-identities in
|
||||||
|
* this shape. If `nostr.names` is also present, it wins for any
|
||||||
|
* sub-identity; root-lookups fall back to the bare `pubkey` when
|
||||||
|
* `names["_"]` is missing.
|
||||||
*/
|
*/
|
||||||
private fun extractFromDomainValue(
|
private fun extractFromDomainValue(
|
||||||
value: JsonObject,
|
value: JsonObject,
|
||||||
@@ -290,7 +298,7 @@ class NamecoinNameResolver(
|
|||||||
): NamecoinNostrResult? {
|
): NamecoinNostrResult? {
|
||||||
val nostrField = value["nostr"] ?: return null
|
val nostrField = value["nostr"] ?: return null
|
||||||
|
|
||||||
// Simple form: "nostr": "hex-pubkey"
|
// Simple-string form: "nostr": "hex-pubkey"
|
||||||
if (nostrField is JsonPrimitive && nostrField.isString) {
|
if (nostrField is JsonPrimitive && nostrField.isString) {
|
||||||
val pubkey = nostrField.content
|
val pubkey = nostrField.content
|
||||||
if (parsed.localPart == "_" && isValidPubkey(pubkey)) {
|
if (parsed.localPart == "_" && isValidPubkey(pubkey)) {
|
||||||
@@ -305,48 +313,73 @@ class NamecoinNameResolver(
|
|||||||
if (parsed.localPart != "_") return null
|
if (parsed.localPart != "_") return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extended form: "nostr": { "names": {...}, "relays": {...} }
|
|
||||||
if (nostrField is JsonObject) {
|
if (nostrField is JsonObject) {
|
||||||
val names = nostrField["names"]?.jsonObject ?: return null
|
// Extended NIP-05-like form first: "nostr": { "names": {...}, "relays": {...} }
|
||||||
|
val names = nostrField["names"]?.jsonObject
|
||||||
|
|
||||||
// Resolve: exact match → "_" root → first entry (root lookups only)
|
if (names != null) {
|
||||||
val resolvedLocalPart: String
|
val exactMatch = names[parsed.localPart]
|
||||||
val pubkey: String
|
val rootMatch = names["_"]
|
||||||
|
val firstEntry = if (parsed.localPart == "_") names.entries.firstOrNull() else null
|
||||||
|
|
||||||
val exactMatch = names[parsed.localPart]
|
if (exactMatch is JsonPrimitive && isValidPubkey(exactMatch.content)) {
|
||||||
val rootMatch = names["_"]
|
return NamecoinNostrResult(
|
||||||
val firstEntry = if (parsed.localPart == "_") names.entries.firstOrNull() else null
|
pubkey = exactMatch.content.lowercase(),
|
||||||
|
relays = extractRelays(nostrField, exactMatch.content),
|
||||||
when {
|
namecoinName = parsed.namecoinName,
|
||||||
exactMatch is JsonPrimitive && isValidPubkey(exactMatch.content) -> {
|
localPart = parsed.localPart,
|
||||||
resolvedLocalPart = parsed.localPart
|
)
|
||||||
pubkey = exactMatch.content
|
|
||||||
}
|
}
|
||||||
|
if (rootMatch is JsonPrimitive && isValidPubkey(rootMatch.content)) {
|
||||||
rootMatch is JsonPrimitive && isValidPubkey(rootMatch.content) -> {
|
return NamecoinNostrResult(
|
||||||
resolvedLocalPart = "_"
|
pubkey = rootMatch.content.lowercase(),
|
||||||
pubkey = rootMatch.content
|
relays = extractRelays(nostrField, rootMatch.content),
|
||||||
|
namecoinName = parsed.namecoinName,
|
||||||
|
localPart = "_",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
if (firstEntry != null &&
|
||||||
firstEntry != null &&
|
|
||||||
firstEntry.value is JsonPrimitive &&
|
firstEntry.value is JsonPrimitive &&
|
||||||
isValidPubkey((firstEntry.value as JsonPrimitive).content) -> {
|
isValidPubkey((firstEntry.value as JsonPrimitive).content)
|
||||||
resolvedLocalPart = firstEntry.key
|
) {
|
||||||
pubkey = (firstEntry.value as JsonPrimitive).content
|
val pk = (firstEntry.value as JsonPrimitive).content
|
||||||
}
|
return NamecoinNostrResult(
|
||||||
|
pubkey = pk.lowercase(),
|
||||||
else -> {
|
relays = extractRelays(nostrField, pk),
|
||||||
return null
|
namecoinName = parsed.namecoinName,
|
||||||
|
localPart = firstEntry.key,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
// names was present but didn't yield a match. Fall through to
|
||||||
|
// the single-identity check below — only meaningful for root
|
||||||
|
// lookups (non-root requests against a names-only record
|
||||||
|
// correctly stop here).
|
||||||
|
if (parsed.localPart != "_") return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val relays = extractRelays(nostrField, pubkey)
|
// Single-identity form: "nostr": { "pubkey": "hex", "relays": [...] }
|
||||||
return NamecoinNostrResult(
|
// Only resolves the root local-part; non-root requests against a
|
||||||
pubkey = pubkey.lowercase(),
|
// single-identity record fall through to null so we don't hand
|
||||||
relays = relays,
|
// alice@example.bit the root operator's identity.
|
||||||
namecoinName = parsed.namecoinName,
|
if (parsed.localPart == "_") {
|
||||||
localPart = resolvedLocalPart,
|
val pubkey = (nostrField["pubkey"] as? JsonPrimitive)?.content
|
||||||
)
|
if (pubkey != null && isValidPubkey(pubkey)) {
|
||||||
|
val relays =
|
||||||
|
try {
|
||||||
|
nostrField["relays"]?.jsonArray?.mapNotNull {
|
||||||
|
(it as? JsonPrimitive)?.content
|
||||||
|
} ?: emptyList()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
return NamecoinNostrResult(
|
||||||
|
pubkey = pubkey.lowercase(),
|
||||||
|
relays = relays,
|
||||||
|
namecoinName = parsed.namecoinName,
|
||||||
|
localPart = "_",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
Reference in New Issue
Block a user