Fix NIP conformance in domain kinds; add domain docs/skill
tests / tests (push) Failing after 5m8s

NIP fixes:
- RelayMembers (13534): use NIP-43 `member` tags (not `p`) and set the required
  NIP-70 `-` protected tag.
- Profile (kind 0): remove display-name support entirely (getter, setter, display()
  fallback, and the search weight).
- Comment (1111): A/a tags now carry a real address, not the event id.
- BlossomServerList (10063): normalize server URLs with normalizeUrl (HTTP), not
  normalizeRelayUrl (which forced wss://).
- HandlerRecommendation (31989): fix inverted removeRecommendation filter; add
  setSupportedKind()/supportedKind() for the NIP-89 d-tag.
- Report (1984): place the report-type string on the e tag (note reports) or p tag
  (profile reports); always emit the p tag.

Docs/skills:
- Add @welshman/domain docs (docs/domain/) and the welshman-domain skill.
- Prune @welshman/util docs/skill of the moved Profile/List/Handler/Encryptable
  helpers; register domain in the sidebar, index, and skills README.
- Apply accuracy fixes to the @welshman/app docs/skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsMjvv7krpZeHK1Njeneru
This commit is contained in:
2026-06-20 14:55:21 +00:00
parent e2a6ef21cd
commit 5b8fef5b23
29 changed files with 1382 additions and 606 deletions
@@ -1,5 +1,5 @@
import {uniq, nthEq} from "@welshman/lib"
import {BLOSSOM_SERVERS, getTagValues, normalizeRelayUrl} from "@welshman/util"
import {uniq, nthEq, normalizeUrl} from "@welshman/lib"
import {BLOSSOM_SERVERS, getTagValues} from "@welshman/util"
import {ListReader} from "../ListReader.js"
import {ListBuilder} from "../ListBuilder.js"
@@ -8,11 +8,11 @@ export class BlossomServerList extends ListReader {
readonly kind = BLOSSOM_SERVERS
urls() {
return uniq(getTagValues("server", this.tags()).map(normalizeRelayUrl))
return uniq(getTagValues("server", this.tags()).map(normalizeUrl))
}
includes(url: string) {
return this.urls().includes(normalizeRelayUrl(url))
return this.urls().includes(normalizeUrl(url))
}
builder() {
@@ -24,16 +24,16 @@ export class BlossomServerListBuilder extends ListBuilder<BlossomServerList> {
readonly kind = BLOSSOM_SERVERS
addUrl(url: string) {
return this.addPublic(["server", normalizeRelayUrl(url)])
return this.addPublic(["server", normalizeUrl(url)])
}
removeUrl(url: string) {
return this.drop(nthEq(1, normalizeRelayUrl(url)))
return this.drop(nthEq(1, normalizeUrl(url)))
}
setUrls(urls: string[]) {
this.clear()
return this.addPublic(...urls.map(url => ["server", normalizeRelayUrl(url)]))
return this.addPublic(...urls.map(url => ["server", normalizeUrl(url)]))
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
import {first} from "@welshman/lib"
import {COMMENT, Address, getAddress, getTagValue, isReplaceableKind} from "@welshman/util"
import {COMMENT, Address, getTagValue} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import type {ISigner} from "@welshman/signer"
import {EventReader} from "../EventReader.js"
@@ -68,7 +68,7 @@ export class CommentBuilder extends EventBuilder<Comment> {
this.rootTags = [["K", String(kind)], ["E", id], ["P", pubkey]]
if (identifier) {
this.rootTags.push(["A", id])
this.rootTags.push(["A", new Address(kind, pubkey, identifier).toString()])
}
return this
@@ -78,7 +78,7 @@ export class CommentBuilder extends EventBuilder<Comment> {
this.parentTags = [["k", String(kind)], ["e", id], ["p", pubkey]]
if (identifier) {
this.parentTags.push(["a", id])
this.parentTags.push(["a", new Address(kind, pubkey, identifier).toString()])
}
return this
@@ -1,4 +1,4 @@
import {nthEq, last} from "@welshman/lib"
import {nthNe, last} from "@welshman/lib"
import {HANDLER_RECOMMENDATION, getAddressTags, getAddressTagValues} from "@welshman/util"
import {EventReader} from "../EventReader.js"
import {EventBuilder} from "../EventBuilder.js"
@@ -22,6 +22,11 @@ export class HandlerRecommendation extends EventReader {
return tag?.[1]
}
// NIP-89: the `d` tag is the event-kind this recommendation applies to.
supportedKind() {
return this.identifier()
}
builder() {
return new HandlerRecommendationBuilder(this)
}
@@ -38,6 +43,14 @@ export class HandlerRecommendationBuilder extends EventBuilder<HandlerRecommenda
this.addressTags = this.consumeTags("a")
}
// NIP-89: set the `d` tag to the event-kind being recommended for, so the
// standard {kinds:[31989], "#d":[kind]} query resolves.
setSupportedKind(kind: number) {
this.setIdentifier(String(kind))
return this
}
addRecommendation(address: string, relay?: string, platform?: string) {
if (!this.addressTags.some(t => t[1] === address)) {
this.addressTags = [...this.addressTags, ["a", address, relay || "", platform || ""]]
@@ -47,7 +60,7 @@ export class HandlerRecommendationBuilder extends EventBuilder<HandlerRecommenda
}
removeRecommendation(address: string) {
this.addressTags = this.addressTags.filter(nthEq(1, address))
this.addressTags = this.addressTags.filter(nthNe(1, address))
return this
}
-14
View File
@@ -41,10 +41,6 @@ export class Profile extends EventReader {
return this.values.name
}
displayName(): Maybe<string> {
return this.values.display_name
}
nip05(): Maybe<string> {
return this.values.nip05
}
@@ -74,10 +70,6 @@ export class Profile extends EventReader {
if (name) return ellipsize(name, 60).trim()
const displayName= this.displayName()
if (displayName) return ellipsize(displayName, 60).trim()
return displayPubkey(this.event.pubkey).trim() || fallback.trim()
}
@@ -107,12 +99,6 @@ export class ProfileBuilder extends EventBuilder<Profile> {
return this
}
setDisplayName(displayName: string) {
this.values.displayName = displayName
return this
}
setNip05(nip05: string) {
this.values.nip05 = nip05
+9 -5
View File
@@ -1,14 +1,15 @@
import {uniq, nth, nthNe, uniqBy} from "@welshman/lib"
import {RELAY_MEMBERS, getPubkeyTagValues} from "@welshman/util"
import {RELAY_MEMBERS, getTagValues} from "@welshman/util"
import {EventReader} from "../EventReader.js"
import {EventBuilder} from "../EventBuilder.js"
// Flotilla kind-13534 relay/space member-list snapshot.
// Flotilla kind-13534 relay/space member-list snapshot. Members are carried in
// NIP-43 `member` tags, and the event is NIP-70 protected (`-`).
export class RelayMembers extends EventReader {
readonly kind = RELAY_MEMBERS
pubkeys() {
return uniq(getPubkeyTagValues(this.event.tags))
return uniq(getTagValues("member", this.event.tags))
}
isMember(pubkey: string) {
@@ -28,11 +29,14 @@ export class RelayMembersBuilder extends EventBuilder<RelayMembers> {
constructor(readonly reader?: RelayMembers) {
super(reader)
this.pubkeyTags = uniqBy(nth(1), this.consumeTags("p"))
this.pubkeyTags = uniqBy(nth(1), this.consumeTags("member"))
// NIP-43 requires kind-13534 member lists to be NIP-70 protected.
this.setProtected(true)
}
addPubkey(pubkey: string) {
this.pubkeyTags = uniqBy(nth(1), [...this.pubkeyTags, ["p", pubkey]])
this.pubkeyTags = uniqBy(nth(1), [...this.pubkeyTags, ["member", pubkey]])
return this
}
+12 -3
View File
@@ -16,7 +16,9 @@ export class Report extends EventReader {
}
reason() {
return getTag("e", this.event.tags)?.[2]
// NIP-56: the report-type string is the 3rd entry of the reported tag — the
// `e` tag for note reports, or the `p` tag for profile-only reports.
return getTag("e", this.event.tags)?.[2] ?? getTag("p", this.event.tags)?.[2]
}
builder() {
@@ -39,7 +41,7 @@ export class ReportBuilder extends EventBuilder<Report> {
this.reportedPubkey = p?.[1]
this.eventId = e?.[1]
this.reason = e?.[2]
this.reason = e?.[2] ?? p?.[2]
}
setReportedPubkey(reportedPubkey: string) {
@@ -63,8 +65,15 @@ export class ReportBuilder extends EventBuilder<Report> {
protected buildTags() {
const tags: string[][] = []
// NIP-56: a `p` tag referencing the reported pubkey is mandatory. The
// report-type string goes on the reported object — the `e` tag for note
// reports, otherwise the `p` tag for profile-only reports.
if (this.reportedPubkey) {
tags.push(["p", this.reportedPubkey])
tags.push(
this.eventId
? ["p", this.reportedPubkey]
: ["p", this.reportedPubkey, ...(this.reason ? [this.reason] : [])],
)
}
if (this.eventId) {