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:
+2
-1
@@ -19,7 +19,8 @@ npx skills add coracle-social/welshman
|
||||
| Skill | Description |
|
||||
|-------|-------------|
|
||||
| welshman | General overview, package map, getting started |
|
||||
| welshman-util | Core nostr types, events, filters, NIPs |
|
||||
| welshman-util | Core nostr types, events, filters, tags, NIPs (profiles/lists/handlers/rooms moved to domain) |
|
||||
| welshman-domain | Nostr event kinds as Reader/Builder classes: profiles, lists, rooms, handlers, zaps |
|
||||
| welshman-lib | Utilities: LRU, emitter, deferred, task queue |
|
||||
| welshman-net | Relay connections, request/publish, auth |
|
||||
| welshman-router | Relay selection strategies |
|
||||
|
||||
@@ -89,14 +89,14 @@ All follow the same shape — `get(key)` (sync), `one(key)` (reactive, lazy-load
|
||||
|
||||
| Plugin | Data | Notable accessors |
|
||||
|---|---|---|
|
||||
| `Profiles` | kind-0 profiles | `one(pk)`, `display(pk)`, `publish(profile)` |
|
||||
| `Profiles` | kind-0 profiles | `one(pk)`, `display(pk)`, `publish(values)` |
|
||||
| `FollowLists` | kind-3 follows | `one(pk)`, `follow(tag)`, `unfollow(value)` |
|
||||
| `MuteLists` | kind-10000 mutes (private = encrypted) | `mutePublicly(tag)`, `mutePrivately(tag)`, `unmute(v)`, `setMutes(...)` |
|
||||
| `PinLists` | kind-10001 pins | `pin(tag)`, `unpin(value)` |
|
||||
| `RelayLists` | NIP-65 (kind 10002) | `urls(pk)`, `readUrls(pk)`, `writeUrls(pk)`, `addRelay(url, mode)`, `setWriteRelays(urls)` |
|
||||
| `BlockedRelayLists` | kind-10006 | `urls(pk)`, `addRelay`, `removeRelay`, `setRelays` |
|
||||
| `MessagingRelayLists` | kind-10050 (NIP-17 DM relays) | `urls(pk)`, `addRelay`, ... |
|
||||
| `SearchRelayLists` | kind-10007 | `urls(pk)`, `addRelay`, ... |
|
||||
| `BlockedRelayLists` | kind-10006 | `urls(pk)`, `addUrl`, `removeUrl`, `setUrls` |
|
||||
| `MessagingRelayLists` | kind-10050 (NIP-17 DM relays) | `urls(pk)`, `addUrl`, ... |
|
||||
| `SearchRelayLists` | kind-10007 | `urls(pk)`, `addUrl`, ... |
|
||||
| `Relays` | NIP-11 relay info (HTTP) | `one(url)`, `display(url)`, `hasNip(url, n)`, `hasNegentropy(url)` |
|
||||
| `RelayManagement` | NIP-86 | `post(url, request)` |
|
||||
| `Handles` | NIP-05 (HTTP, batched) | `forPubkey(pk)`, `display(nip05)`, `loadForPubkey(pk)` |
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
---
|
||||
name: welshman-domain
|
||||
description: "Use this skill when working with @welshman/domain: reading and building typed nostr events by kind — the Reader/Builder split (EventReader/EventBuilder, ListReader/ListBuilder) that sits between @welshman/util's raw event types and @welshman/app's data plugins. Covers Profile, NIP-51 lists (follows, mutes, pins, relays, bookmarks, topics, emoji, feeds, blossom), NIP-29 rooms, Flotilla relay/space membership, NIP-89 handlers, NIP-57/75 zaps, and content kinds (comment, thread, classified, poll, calendar, report). Use it to parse an event into domain getters, build/edit an event template, handle NIP-44 private-list encryption, or migrate from the old makeProfile/readProfile/makeList/readHandlers/Encryptable/makeRoom*Event helpers."
|
||||
---
|
||||
|
||||
# welshman/domain — Typed Readers & Builders for Nostr Kinds
|
||||
|
||||
## Overview
|
||||
|
||||
`@welshman/domain` translates nostr events to and from typed domain objects. For each event kind it ships a matched pair:
|
||||
|
||||
- a **Reader** — a read-only view over a `TrustedEvent` with synchronous getters (`profile.name()`, `followList.pubkeys()`), and
|
||||
- a **Builder** — a mutable, chainable producer of an `EventTemplate` (`new ProfileBuilder().setName("alice")`).
|
||||
|
||||
It sits one layer above `@welshman/util` (raw `TrustedEvent`/`EventTemplate` types, tag getters, kind constants) and one layer below `@welshman/app` (whose data plugins like `Profiles`/`FollowLists` use these readers as their `eventToItem`). The package is stateless: no stores, no network, no globals — just `event → object` and `object → template`.
|
||||
|
||||
This replaces the old free-function helpers that used to live in `@welshman/util` (`makeProfile`/`readProfile`, `makeList`/`readList`, `readHandlers`, `Encryptable`, `makeRoom*Event`). See the migration table below.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @welshman/domain
|
||||
# or
|
||||
pnpm add @welshman/domain
|
||||
```
|
||||
|
||||
Peer deps: `@welshman/lib`, `@welshman/util`, `@welshman/signer`, `@welshman/feeds`, and `nostr-tools`.
|
||||
|
||||
## Core mental model
|
||||
|
||||
1. **Readers wrap an event; Builders produce a template.** A Reader is `new Profile(event)` (validated + parsed via the static factories); a Builder is `new ProfileBuilder()` (or seeded from a reader). Each Reader knows its `builder()`, each Builder optionally takes a `reader`.
|
||||
2. **Reading is async; getters are sync.** You enter through `await X.fromEvent(event, signer?)` (which runs `parse`), then call plain synchronous getters.
|
||||
3. **Building is chainable; output is async.** Setters return `this`; you finish with `await b.toTemplate(signer?)` / `toRumor(signer)` / `toEvent(signer)`.
|
||||
4. **The signer is optional and lazy.** Most kinds ignore it. Only NIP-51 lists need it — to *decrypt* private tags on read, and to *encrypt* them (NIP-44, self-encrypted) on build. Callers can always pass a signer; the kind decides whether it matters.
|
||||
5. **Round-trips preserve unknown tags.** Build a Builder from a Reader and any tags the class doesn't model are carried through verbatim into the rebuilt template.
|
||||
|
||||
## Reading an event
|
||||
|
||||
`EventReader` exposes two static entry points. Both validate `event.kind === reader.kind` (throwing `Expected a kind X event, got kind Y`) and run the kind's `parse`.
|
||||
|
||||
```typescript
|
||||
import {Profile, FollowList} from "@welshman/domain"
|
||||
|
||||
// One-shot read (the usual entry point):
|
||||
const profile = await Profile.fromEvent(event) // no signer needed
|
||||
const follows = await FollowList.fromEvent(event, signer) // signer decrypts private tags
|
||||
|
||||
// Reusable, class-bound factory over a fixed signer — point-free friendly:
|
||||
const toProfile = Profile.factory(signer) // (event) => Promise<Profile>
|
||||
const profile2 = await toProfile(someEvent)
|
||||
```
|
||||
|
||||
`factory(signer?)` returns an `async (event) => Promise<Reader>`. It's the shape `@welshman/app` plugins want for their `eventToItem`.
|
||||
|
||||
Common base getters available on every reader (all synchronous): `id()`, `author()`, `content()`, `tags()`, `createdAt()`, `identifier()` (d-tag), `address()` (`kind:pubkey:d`), `group()` (NIP-29 h-tag), `protect()` (has `["-"]`), `expires()`. Each kind adds its own — e.g. `profile.name()`, `profile.display()`, `followList.pubkeys()`, `followList.includes(pk)`.
|
||||
|
||||
## Building / editing an event
|
||||
|
||||
```typescript
|
||||
import {ProfileBuilder, FollowListBuilder} from "@welshman/domain"
|
||||
|
||||
// Build from scratch:
|
||||
const template = await new ProfileBuilder()
|
||||
.setName("alice")
|
||||
.setAbout("hi")
|
||||
.toTemplate() // EventTemplate {kind, content, tags}
|
||||
|
||||
// Edit an existing event (seed the builder from a reader):
|
||||
const reader = await FollowList.fromEvent(event, signer)
|
||||
const signed = await reader.builder() // === new FollowListBuilder(reader)
|
||||
.addFollow(["p", pubkey])
|
||||
.toEvent(signer) // SignedEvent
|
||||
```
|
||||
|
||||
- Construct empty (`new XBuilder()`) or from a reader (`new XBuilder(reader)`, or `reader.builder()`).
|
||||
- Base behavior setters (chainable): `setContent`, `setGroup`/`clearGroup` (h-tag), `setProtected(bool)`, `setExpiration`/`clearExpiration`, `setIdentifier`/`clearIdentifier` (d-tag, defaults to a random id). Each kind adds its own setters.
|
||||
- Output methods (all async): `toTemplate(signer?)` → `EventTemplate`; `toRumor(signer)` → `HashedEvent` (needs signer); `toEvent(signer)` → `SignedEvent` (signs + stamps, needs signer).
|
||||
|
||||
**Round-trip / extra-tag passthrough.** When a builder is seeded from a reader, every tag in `event.tags` starts in `extraTags`. The base constructor lifts out `h`/`-`/`expiration`/`d`; each subclass lifts the tags it models (via `consumeTags`). Whatever is left is re-emitted unchanged by `toTemplate` (`[...implTags, ...behaviorTags, ...extraTags]`), so unmodeled tags survive an edit.
|
||||
|
||||
## Async & signer notes
|
||||
|
||||
- **Async surface:** `fromEvent`, the function returned by `factory`, and all of `toTemplate`/`toRumor`/`toEvent` are async. Every getter and every setter is synchronous.
|
||||
- **Reading private list tags:** a `ListReader` only surfaces `privateTags` when you pass the **author's own** signer to `fromEvent`/`factory` (it decrypts NIP-44 content only when `signer.getPubkey() === event.pubkey`). Decryption failures are swallowed — `decrypted` stays `false` and private tags stay empty.
|
||||
- **Writing private list tags:** `ListBuilder.buildContent` is where encryption happens. If there are private tags it requires a signer and NIP-44-encrypts them to the author's own pubkey (`A signer is required to encrypt private tags`). If the source was never decrypted, the original ciphertext is preserved untouched (so you don't clobber tags you couldn't see).
|
||||
- **`toTemplate(signer?)`** needs a signer only for list kinds with non-empty private tags. `toRumor`/`toEvent` always need one. All other kinds ignore the signer entirely.
|
||||
- **`d`-tag required** (base `validate` throws otherwise) for parameterized-replaceable kinds: `RelaySet` (30002), `Classified` (30402), `Feed` (31890), `TimeEvent` (31923), `HandlerRecommendation` (31989), `Handler` (31990), `RoomMeta` (39000), `RoomAdmins` (39001), `RoomMembers` (39002). Call `setIdentifier()`.
|
||||
- **`h`-group required** (subclass `validate` throws): `RoomDelete` (9008), `RoomJoin` (9021), `RoomLeave` (9022). Call `setGroup(groupId)`.
|
||||
|
||||
## Kind classes
|
||||
|
||||
Each row: kind# — NIP — Reader / Builder.
|
||||
|
||||
### Profile
|
||||
|
||||
| Kind | NIP | Reader / Builder |
|
||||
|---|---|---|
|
||||
| 0 | NIP-01 | `Profile` / `ProfileBuilder` |
|
||||
|
||||
`Profile`: `name`, `displayName`, `nip05`, `lnurl`, `about`, `banner`, `picture`, `website`, `display(fallback?)`. Builder: `update`, `setName`, `setDisplayName`, `setNip05`, `setAbout`, `setBanner`, `setPicture`, `setWebsite`. (Also exports `parseLnUrl`, `displayPubkey`.) Note: `setDisplayName` writes `values.displayName` (camelCase) but `displayName()` reads `values.display_name` — they're not symmetric.
|
||||
|
||||
### Lists (ListReader/ListBuilder — public/private split, NIP-44 encryption)
|
||||
|
||||
| Kind | NIP | Reader / Builder |
|
||||
|---|---|---|
|
||||
| 3 | NIP-02 | `FollowList` / `FollowListBuilder` |
|
||||
| 10000 | NIP-51 | `MuteList` / `MuteListBuilder` |
|
||||
| 10001 | NIP-51 | `PinList` / `PinListBuilder` |
|
||||
| 10002 | NIP-65 | `RelayList` / `RelayListBuilder` |
|
||||
| 10003 | NIP-51 | `BookmarkList` / `BookmarkListBuilder` |
|
||||
| 10004 | NIP-51 | `GroupList` / `GroupListBuilder` |
|
||||
| 10006 | NIP-51 | `BlockedRelayList` / `BlockedRelayListBuilder` |
|
||||
| 10007 | NIP-51 | `SearchRelayList` / `SearchRelayListBuilder` |
|
||||
| 10009 | NIP-51 | `RoomList` / `RoomListBuilder` |
|
||||
| 10014 | NIP-51 | `FeedList` / `FeedListBuilder` |
|
||||
| 10015 | NIP-51 | `TopicList` / `TopicListBuilder` |
|
||||
| 10030 | NIP-51 | `EmojiList` / `EmojiListBuilder` |
|
||||
| 10050 | NIP-17 | `MessagingRelayList` / `MessagingRelayListBuilder` |
|
||||
| 10063 | Blossom BUD-03 | `BlossomServerList` / `BlossomServerListBuilder` |
|
||||
| 30002 | NIP-51 | `RelaySet` / `RelaySetBuilder` |
|
||||
|
||||
List builders share the `ListBuilder` mutators: `addPublic`/`addPrivate`, `keepPublic`/`keepPrivate`/`keep`, `dropPublic`/`dropPrivate`/`drop`, `clearPublic`/`clearPrivate`/`clear`. Each kind also exposes intent-named helpers, e.g. `FollowListBuilder.addFollow`/`removeFollow`, `MuteListBuilder.mutePublicly`/`mutePrivately`/`unmute`, `RelayListBuilder.addUrl(url, mode)`/`setReadUrls`/`setWriteUrls`.
|
||||
|
||||
### Rooms (NIP-29)
|
||||
|
||||
Room ops are scoped by the `h` group tag (base `setGroup`); metadata kinds 39000–39002 are addressable per room.
|
||||
|
||||
| Kind | NIP | Reader / Builder |
|
||||
|---|---|---|
|
||||
| 9000 | NIP-29 | `RoomAddMember` / `RoomAddMemberBuilder` |
|
||||
| 9001 | NIP-29 | `RoomRemoveMember` / `RoomRemoveMemberBuilder` |
|
||||
| 9002 | NIP-29 | `RoomEdit` / `RoomEditBuilder` |
|
||||
| 9007 | NIP-29 | `RoomCreate` / `RoomCreateBuilder` |
|
||||
| 9008 | NIP-29 | `RoomDelete` / `RoomDeleteBuilder` |
|
||||
| 9021 | NIP-29 | `RoomJoin` / `RoomJoinBuilder` |
|
||||
| 9022 | NIP-29 | `RoomLeave` / `RoomLeaveBuilder` |
|
||||
| 19004 | NIP-29 / Flotilla | `RoomCreatePermission` / `RoomCreatePermissionBuilder` |
|
||||
| 39000 | NIP-29 | `RoomMeta` / `RoomMetaBuilder` |
|
||||
| 39001 | NIP-29 | `RoomAdmins` / `RoomAdminsBuilder` |
|
||||
| 39002 | NIP-29 | `RoomMembers` / `RoomMembersBuilder` |
|
||||
|
||||
`RoomMeta`: `name`, `about`, `picture`, `pictureMeta`, `isClosed`/`isHidden`/`isPrivate`/`isRestricted`/`hasLivekit`. `RoomEdit` mirrors it but its livekit getter is named `livekit()`. `RoomJoin`: `code()` (reads the `claim` tag), `reason()`; builder `setCode`/`setReason`.
|
||||
|
||||
### Relay membership (Flotilla "spaces" — relay-level, NIP-29-adjacent)
|
||||
|
||||
| Kind | NIP | Reader / Builder |
|
||||
|---|---|---|
|
||||
| 8000 | Flotilla | `RelayAddMember` / `RelayAddMemberBuilder` |
|
||||
| 8001 | Flotilla | `RelayRemoveMember` / `RelayRemoveMemberBuilder` |
|
||||
| 13534 | Flotilla | `RelayMembers` / `RelayMembersBuilder` |
|
||||
| 28934 | Flotilla | `RelayJoin` / `RelayJoinBuilder` |
|
||||
| 28935 | NIP-29 | `RelayInvite` / `RelayInviteBuilder` |
|
||||
| 28936 | Flotilla | `RelayLeave` / `RelayLeaveBuilder` |
|
||||
|
||||
### Handlers (NIP-89)
|
||||
|
||||
| Kind | NIP | Reader / Builder |
|
||||
|---|---|---|
|
||||
| 31989 | NIP-89 | `HandlerRecommendation` / `HandlerRecommendationBuilder` |
|
||||
| 31990 | NIP-89 | `Handler` / `HandlerBuilder` |
|
||||
|
||||
`Handler`: JSON content → `values: HandlerMeta`; getters `name`, `about`, `picture`, `website`, `lud16`, `nip05`, `kinds()`; builder `setName`/…/`setKinds(number[])`. Exports type `HandlerMeta`.
|
||||
|
||||
### Zaps (NIP-57 / NIP-75)
|
||||
|
||||
| Kind | NIP | Reader / Builder |
|
||||
|---|---|---|
|
||||
| 9041 | NIP-75 | `ZapGoal` / `ZapGoalBuilder` |
|
||||
| 9734 | NIP-57 | `ZapRequest` / `ZapRequestBuilder` |
|
||||
| 9735 | NIP-57 | `ZapReceipt` / `ZapReceiptBuilder` |
|
||||
|
||||
`ZapRequest`: `amount`, `lnurl`, `recipient`, `eventId`, `urls`, `comment`. `ZapReceipt`: `bolt11`, `invoiceAmount`, `request`, `sender`, `recipient`, `eventId`, `comment`, `preimage`, plus `verify(zapper)`.
|
||||
|
||||
### Content
|
||||
|
||||
| Kind | NIP | Reader / Builder |
|
||||
|---|---|---|
|
||||
| 11 | NIP-7D | `Thread` / `ThreadBuilder` |
|
||||
| 1018 | NIP-88 | `PollResponse` / `PollResponseBuilder` |
|
||||
| 1068 | NIP-88 | `Poll` / `PollBuilder` |
|
||||
| 1111 | NIP-22 | `Comment` / `CommentBuilder` |
|
||||
| 1984 | NIP-56 | `Report` / `ReportBuilder` |
|
||||
| 30402 | NIP-99 | `Classified` / `ClassifiedBuilder` |
|
||||
| 31890 | NIP-51 | `Feed` / `FeedBuilder` |
|
||||
| 31923 | NIP-52 | `TimeEvent` / `TimeEventBuilder` |
|
||||
|
||||
`Comment`: `root()`/`parent()`; builder `setRoot`/`setParent`/`setRootFromEvent`/`setParentFromEvent`. `Poll`: `title`, `options`, `pollType`, `endsAt`, `isClosed`, `urls`, plus `results(responses)`; builder `addOption`, `setPollType`, `setEndsAt`. Exported types: `CommentRef`, `ClassifiedPrice`, `PollType`, `PollOption`, `PollResult`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Private list tags need the author's signer.** `await FollowList.fromEvent(event)` with no signer (or someone else's) yields only public tags; `decrypted` stays `false`. Pass the author's own signer to see private entries.
|
||||
- **Don't clobber undecryptable lists.** If you edit a list you couldn't decrypt and try to write private tags, `validate()` throws `Unable to modify list when decryption was not performed`. Editing only public tags is fine — the original ciphertext is preserved.
|
||||
- **`toRumor`/`toEvent` always need a signer**, even for kinds whose `toTemplate` doesn't (they call `getPubkey()`/`sign`).
|
||||
- **Parameterized-replaceable kinds throw without a `d` tag** — call `setIdentifier()` (or let it default to a random id). Room ops scoped by `h` throw without `setGroup`.
|
||||
- **`ProfileBuilder.setDisplayName` is asymmetric** with `Profile.displayName()` (camelCase write vs `display_name` read). Use `update({display_name})` if you need the getter to round-trip.
|
||||
- **`RoomJoin.code()` reads the `claim` tag** (accessor name ≠ tag key); `RelayJoin`/`RelayInvite` use `claim()`.
|
||||
- **No top-level free functions beyond `parseLnUrl` and `displayPubkey`.** The h/group tag is handled by `setGroup`/`group()`, not a helper. Don't invent `makeX`/`readX` helpers — those were removed (see below).
|
||||
|
||||
## Moved from @welshman/util
|
||||
|
||||
These helpers used to live in `@welshman/util` and have been replaced by Reader/Builder classes here:
|
||||
|
||||
| Old (`@welshman/util`) | New (`@welshman/domain`) |
|
||||
|---|---|
|
||||
| `readProfile(event)` | `await Profile.fromEvent(event)` |
|
||||
| `makeProfile({...})` / editing | `new ProfileBuilder().setName(...)….toTemplate()` |
|
||||
| `readList(event, signer)` | `await FollowList.fromEvent(event, signer)` (or the kind-specific list reader) |
|
||||
| `makeList({...})` | `new FollowListBuilder().addFollow(...)….toTemplate(signer)` |
|
||||
| `readHandlers(event)` | `await Handler.fromEvent(event)` |
|
||||
| `Encryptable` (manual private-tag encrypt) | `ListBuilder.buildContent` — `addPrivate(...)` then `toTemplate(signer)` (NIP-44, self) |
|
||||
| `makeRoomMetaEvent` / `makeRoomEditEvent` / `makeRoomJoinEvent` / … | `RoomMetaBuilder` / `RoomEditBuilder` / `RoomJoinBuilder` / … `.toTemplate()` |
|
||||
|
||||
The shape of the change: free functions that returned/consumed raw events became classes with sync getters and chainable setters, and private-list encryption moved from an explicit `Encryptable` wrapper into the builder's async `buildContent`.
|
||||
|
||||
## Related skills
|
||||
|
||||
- `welshman-util` — the raw `TrustedEvent`/`EventTemplate` types, kind constants, and tag getters these classes are built on.
|
||||
- `welshman-app` — the instance-based app layer whose data plugins (`Profiles`, `FollowLists`, `MuteLists`, …) use these readers as `eventToItem`.
|
||||
- `welshman-signer` — the `ISigner` interface and NIP-44 `decrypt`/`encrypt` used for private list tags and for `toRumor`/`toEvent`.
|
||||
@@ -1,11 +1,13 @@
|
||||
---
|
||||
name: welshman-util
|
||||
description: "Use this skill when working with @welshman/util: nostr event types, kinds, tags, filters, addresses, NIPs (42/86/98), profiles, relays, zaps, wallets, or any core nostr data structures."
|
||||
description: "Use this skill when working with @welshman/util: nostr event types, kinds, tags, filters, addresses, NIPs (42/86/98), relays, zaps, wallets, or any core nostr data structures. (Profiles, lists, handlers, rooms, and Encryptable now live in @welshman/domain.)"
|
||||
---
|
||||
|
||||
# welshman/util — Core Nostr Utilities
|
||||
|
||||
`@welshman/util` is the foundational layer of the welshman nostr stack, providing types, constants, and helpers for every nostr primitive: events, kinds, tags, filters, addresses, profiles, lists, zaps, relays, and Lightning wallet integration. Higher level welshman packages (`@welshman/net`, `@welshman/app`, `@welshman/store`, etc.) depend on the types and utilities defined here.
|
||||
`@welshman/util` is the foundational layer of the welshman nostr stack, providing types, constants, and helpers for every nostr primitive: events, kinds, tags, filters, addresses, zaps, relays, and Lightning wallet integration. Higher level welshman packages (`@welshman/net`, `@welshman/app`, `@welshman/store`, etc.) depend on the types and utilities defined here.
|
||||
|
||||
> **Moved to `@welshman/domain`:** Profiles, lists, handlers, rooms, and Encryptable (`makeProfile`/`readProfile`/`displayProfile`, `makeList`/`addToList*`, `readHandlers`/`displayHandler`, room helpers, `Encryptable`/`asDecryptedEvent`/`DecryptedEvent`, etc.) now live in `@welshman/domain` as Reader/Builder classes — see the welshman-domain skill.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -32,7 +34,6 @@ yarn add @welshman/util
|
||||
| `HashedEvent` | `OwnedEvent + id` |
|
||||
| `SignedEvent` | `HashedEvent + sig` |
|
||||
| `TrustedEvent` | `HashedEvent + optional sig` — most common in-app type |
|
||||
| `DecryptedEvent` | `TrustedEvent + plaintext` (for encrypted lists/events) |
|
||||
|
||||
### Event Utilities
|
||||
|
||||
@@ -323,40 +324,6 @@ isDVMKind(kind) // 5000–7000
|
||||
| `address.toNaddr()` | Serialize to NIP-19 naddr |
|
||||
| `getAddress(event)` | Convenience: get address string from event |
|
||||
|
||||
### Profile
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `makeProfile(partial)` | Create a profile object |
|
||||
| `readProfile(event)` | Parse `PublishedProfile` from kind 0 event |
|
||||
| `createProfile(profile)` | Create kind 0 `EventTemplate` |
|
||||
| `editProfile(published)` | Update existing profile event |
|
||||
| `displayProfile(profile?, fallback?)` | Get best display name string |
|
||||
| `displayPubkey(pubkey)` | Shorten pubkey to `npub1abc...xyz` |
|
||||
| `profileHasName(profile?)` | Check if profile has a name field |
|
||||
|
||||
Profile fields: `name`, `display_name`, `about`, `picture`, `banner`, `website`, `nip05`, `lud06`, `lud16`, `lnurl`
|
||||
|
||||
### Lists (kind 10000+)
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `makeList(params)` | Create a new list |
|
||||
| `readList(event)` | Parse `PublishedList` from `DecryptedEvent` |
|
||||
| `getListTags(list)` | Combined public + private tags |
|
||||
| `addToListPublicly(list, ...tags)` | Returns `Encryptable` with tag added publicly |
|
||||
| `addToListPrivately(list, ...tags)` | Returns `Encryptable` with tag added privately |
|
||||
| `removeFromList(list, value)` | Returns `Encryptable` with tag removed |
|
||||
| `removeFromListByPredicate(list, pred)` | Returns `Encryptable` with matching tags removed |
|
||||
| `updateList(list, { publicTags?, privateTags? })` | Bulk update tags |
|
||||
|
||||
### Encryptable
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `Encryptable<T>` | Wraps a partial event with plaintext updates; call `.reconcile(encrypt)` to produce encrypted event |
|
||||
| `asDecryptedEvent(event, plaintext?)` | Attach plaintext data to a `TrustedEvent` |
|
||||
|
||||
### Relay
|
||||
|
||||
| Export | Description |
|
||||
@@ -414,15 +381,6 @@ sendManagementRequest(url: string, request: ManagementRequest, authEvent: Signed
|
||||
// ManagementMethod enum covers: BanPubkey, AllowPubkey, BanEvent, AllowEvent, etc.
|
||||
```
|
||||
|
||||
### Handlers (NIP-89)
|
||||
|
||||
```typescript
|
||||
readHandlers(event: TrustedEvent): Handler[]
|
||||
getHandlerKey(handler: Handler): string // "kind:address" format
|
||||
getHandlerAddress(event: TrustedEvent): string | undefined
|
||||
displayHandler(handler?: Handler, fallback?: string): string
|
||||
```
|
||||
|
||||
### Links
|
||||
|
||||
```typescript
|
||||
@@ -566,20 +524,6 @@ const parsed = Address.fromNaddr('naddr1...')
|
||||
const addressStr = getAddress(event) // '30023:deadbeef:my-slug'
|
||||
```
|
||||
|
||||
### Profiles
|
||||
|
||||
```typescript
|
||||
import { readProfile, displayProfile, displayPubkey, editProfile } from '@welshman/util'
|
||||
|
||||
const profile = readProfile(kind0Event)
|
||||
console.log(displayProfile(profile, 'Anonymous')) // name or fallback
|
||||
console.log(displayPubkey(pubkey)) // 'npub1abc...xyz'
|
||||
|
||||
// Update profile
|
||||
const updatedEvent = editProfile({ ...profile, name: 'New Name', about: 'Updated bio' })
|
||||
// sign and publish updatedEvent
|
||||
```
|
||||
|
||||
### Zap flow
|
||||
|
||||
```typescript
|
||||
@@ -646,9 +590,9 @@ await fetch('https://api.example.com/upload', {
|
||||
|
||||
- **`@welshman/net`** — uses `TrustedEvent`, `Filter`, `SignedEvent` from this package as the wire types for relay connections and subscriptions.
|
||||
- **`@welshman/store`** — provides Svelte stores over repositories built on `TrustedEvent`; relies on `isReplaceable`, `getAddress`, etc. for deduplication.
|
||||
- **`@welshman/app`** — high-level application layer; wraps net/store/router and uses profile, list, zap, and handler helpers from this package.
|
||||
- **`@welshman/app`** — high-level application layer; wraps net/store/router and uses zap helpers from this package (profile/list/handler/room helpers now live in `@welshman/domain`).
|
||||
- **`@welshman/router`** — uses `RelayMode` and relay URL helpers when computing relay selections.
|
||||
- **`@welshman/signer`** — produces `SignedEvent` objects that satisfy types defined here; the `Encrypt` function type used by `Encryptable` is typically provided by a signer.
|
||||
- **`@welshman/signer`** — produces `SignedEvent` objects that satisfy types defined here; signers also provide the `Encrypt` function used by `@welshman/domain` list/Encryptable builders.
|
||||
|
||||
---
|
||||
|
||||
@@ -660,8 +604,6 @@ await fetch('https://api.example.com/upload', {
|
||||
|
||||
- **`getAncestors` handles two protocols**: Kind 1111 (comment/NIP-22) uses uppercase `E`/`A` for roots and lowercase for replies, returning `{ roots, replies }`. All other kinds use NIP-10 positional rules, returning `{ roots, replies, mentions }` where `mentions` is always present but may be an empty array. You do not need to branch on this; `getAncestors`, `getParentIdOrAddr`, and `isChildOf` handle it automatically.
|
||||
|
||||
- **List mutations return `Encryptable`**: `addToListPrivately`, `removeFromList`, etc. do not return an event directly. Call `.reconcile(encryptFn)` on the result to get the final `EventTemplate` ready to sign.
|
||||
|
||||
- **`zapFromEvent` returns `undefined` on any validation failure** including amount mismatch, wrong zapper pubkey, malformed invoice, or self-zap. Always check the result.
|
||||
|
||||
- **`getLnUrl` handles three input forms**: bare lightning address (`user@domain`), full HTTPS URL, or already-encoded `lnurl1...`. Returns `undefined` for anything else.
|
||||
@@ -673,3 +615,9 @@ await fetch('https://api.example.com/upload', {
|
||||
- **`getTagValue` / `getTagValues` argument order**: the type(s) come **first**, the tags array comes **second** — `getTagValue('title', event.tags)`. This is the opposite of the specialized helpers like `getEventTags(tags)` which take only the tags array. Mixing up the order produces no TypeScript error but silently returns `undefined` or `[]`.
|
||||
|
||||
- **`verifiedSymbol` is a Symbol key**: you must import `verifiedSymbol` from `@welshman/util` and use it as a computed property key — `event[verifiedSymbol] = true`. You cannot use a string key. The symbol is re-exported from `nostr-tools/pure`, so it is the same identity as the one used internally by `verifyEvent`.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- **`@welshman/domain`** (welshman-domain skill) — Profiles, lists, handlers, rooms, and Encryptable (`makeProfile`/`readProfile`/`displayProfile`/`PublishedProfile`, `makeList`/`readList`/`addToList*`/`removeFromList*`/`getListTags`/`getRelaysFromList`, `readHandlers`/`displayHandler`/`getHandlerKey`/`getHandlerAddress`, room helpers, `Encryptable`/`asDecryptedEvent`/`DecryptedEvent`) moved out of `@welshman/util` and now live here as Reader/Builder classes.
|
||||
|
||||
Reference in New Issue
Block a user