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
6.1 KiB
Data Plugins
These plugins expose reactive collections of nostr data. They all follow the plugin patterns: read synchronously with get(key), reactively with one(key) (which lazily loads), and use the convenience accessors that return a Projection. Resolve each with app.use(...).
Most event-backed plugins load via the outbox model: they first resolve the author's NIP-65 write relays (from RelayLists), then query those relays. This is why nearly every data plugin depends on relay lists.
Profiles
Kind-0 profiles keyed by pubkey.
const profiles = app.use(Profiles)
profiles.one(pubkey) // Readable<Maybe<Profile>> — lazily loads
profiles.get(pubkey) // Maybe<Profile> — sync snapshot, no load
await profiles.load(pubkey) // explicit load (cached)
profiles.display(pubkey) // Projection<string> — display name (falls back to npub)
await profiles.publish(values) // merge a partial values record over the current profile and publish (kind 0)
profiles.display(pubkey).$ is the right thing to bind in a component for a user's name.
Follows
Kind-3 follow lists keyed by pubkey.
const follows = app.use(FollowLists)
follows.one(pubkey) // Readable<Maybe<FollowList>>
await follows.follow(["p", otherPubkey]) // add a tag and publish to outbox
await follows.unfollow(otherPubkey) // remove and publish
Mutes
Kind-10000 mute lists keyed by pubkey. Private entries are NIP-44 encrypted, so decoding is asynchronous.
const mutes = app.use(MuteLists)
mutes.one(pubkey) // Readable<Maybe<MuteList>>
await mutes.mutePublicly(["p", pubkey]) // public mute
await mutes.mutePrivately(["p", pubkey]) // encrypted mute
await mutes.unmute(pubkey)
await mutes.setMutes({publicTags, privateTags})
Pins
Kind-10001 pin lists keyed by pubkey.
const pins = app.use(PinLists)
pins.one(pubkey)
await pins.pin(["e", eventId])
await pins.unpin(eventId)
Relay lists
The NIP-65 relay list (kind 10002) is the routing substrate the whole outbox model depends on.
const relayLists = app.use(RelayLists)
relayLists.urls(pubkey) // Projection<string[]> — all relays
relayLists.readUrls(pubkey) // Projection<string[]> — read relays
relayLists.writeUrls(pubkey) // Projection<string[]> — write relays
// Mutations for the current user
await relayLists.addRelay(url, RelayMode.Write)
await relayLists.removeRelay(url, RelayMode.Read) // also notifies the removed relay
await relayLists.setReadRelays(urls)
await relayLists.setWriteRelays(urls)
await relayLists.setRelays(tags)
Specialized relay lists
Each of these is a separate kind with the same shape (urls(pubkey), addUrl, removeUrl, setUrls):
| Plugin | Kind | Purpose |
|---|---|---|
BlockedRelayLists |
10006 | Relays the user refuses to connect to (also gates auth and relay quality) |
MessagingRelayLists |
10050 | NIP-17 DM inbox relays (used by gift-wrapped publishing) |
SearchRelayLists |
10007 | NIP-50 search relays |
app.use(BlockedRelayLists).urls(pubkey) // Projection<string[]>
app.use(MessagingRelayLists).urls(pubkey)
app.use(SearchRelayLists).urls(pubkey)
Relays (NIP-11)
Relay metadata fetched over HTTP, keyed by relay URL.
const relays = app.use(Relays)
relays.one(url) // Readable<Maybe<RelayProfile>> — lazily fetches NIP-11
relays.display(url) // Projection<string>
await relays.hasNip(url, 50) // boolean — does the relay support a NIP?
await relays.hasNegentropy(url) // boolean — NIP-77 / negentropy support
Relay management (NIP-86)
await app.use(RelayManagement).post(url, managementRequest)
Builds a NIP-98 HTTP-auth event signed by the current user and sends a NIP-86 management request to the relay.
Handles (NIP-05)
NIP-05 identifiers verified over HTTP, keyed by name@domain. Lookups are batched (and use dufflepudUrl if configured).
const handles = app.use(Handles)
handles.forPubkey(pubkey) // Projection<Maybe<Handle>> — resolves via the profile's nip05
handles.display(nip05) // string — displayable nip05
await handles.loadForPubkey(pubkey)
Zappers (Lightning)
LNURL zapper info keyed by lnurl, fetched over HTTP.
const zappers = app.use(Zappers)
zappers.forPubkey(pubkey) // Projection<Maybe<Zapper>>
await zappers.validateZapReceipt(zapReceipt, parentEvent) // Promise<Maybe<Zap>>
zappers.validZapReceipts(zapReceipts, parentEvent) // Projection<Zap[]>
Blossom servers
Blossom media-server lists (kind 10063) keyed by pubkey.
const list = await app.use(BlossomServerLists).load(pubkey)
app.use(BlossomServerLists).one(pubkey) // Readable<Maybe<List>>
Topics
Hashtags with usage counts, derived from the repository's tag index.
const topics = app.use(Topics)
topics.all // Readable<Topic[]> ({name, count})
topics.byName // Readable<Map<string, Topic>>
Rooms (NIP-29)
Relay-based group management. Each method builds the relevant room event and publishes it to a single relay as the current user.
const rooms = app.use(Rooms)
rooms.create(relayUrl, roomMeta)
rooms.edit(relayUrl, roomMeta)
rooms.delete(relayUrl, roomMeta)
rooms.join(relayUrl, roomMeta)
rooms.leave(relayUrl, roomMeta)
rooms.addMember(relayUrl, roomMeta, pubkey)
rooms.removeMember(relayUrl, roomMeta, pubkey)
Plaintext
A cache of decrypted content, keyed by event id. Only decrypts events authored by the current user (e.g. your own private list entries or DMs).
const text = await app.use(Plaintext).ensure(event) // decrypts & caches
const cached = app.use(Plaintext).get(event.id) // sync read of the cache