Add vitepress docs

This commit is contained in:
Ticruz
2025-02-04 14:43:40 +01:00
committed by Jon Staab
parent 43255bcb74
commit 94375a56ec
84 changed files with 10821 additions and 139 deletions
-1
View File
@@ -1,3 +1,2 @@
node_modules
build
docs
+5 -1
View File
@@ -1,6 +1,10 @@
node_modules
docs
dist
build
.vscode
.svelte-kit
docs/.vitepress/dist
docs/.vitepress/cached
docs/.vitepress/cache
docs/reference
docs/**/*.html
+1
View File
@@ -0,0 +1 @@
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
+144
View File
@@ -0,0 +1,144 @@
import {defineConfig} from "vitepress"
import typeDocSidebar from "../reference/typedoc-sidebar.json"
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Welshman",
description: "The official Welshman documentation",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{text: "Guide", link: "/what-is-welshman"},
{text: "Reference", link: "/reference/"},
],
sidebar: {
"/reference/": [...typeDocSidebar],
"/": [
{
text: "Introduction",
items: [
{text: "What is Welshman", link: "/what-is-welshman"},
{text: "Getting started", link: "/getting-started"},
],
},
{
text: "@welshman/lib",
link: "/lib/",
items: [
{text: "Utilities", link: "/lib/tools"},
{text: "LRU cache", link: "/lib/lru"},
{text: "Worker", link: "/lib/worker"},
{text: "Deferred", link: "/lib/deferred"},
],
},
{
text: "@welshman/util",
link: "/util/",
items: [
{text: "Address", link: "/util/address"},
{text: "Kinds", link: "/util/kinds"},
{text: "Encryptable", link: "/util/encryptable"},
{text: "Events", link: "/util/events"},
{text: "Filters", link: "/util/filters"},
{text: "Handlers", link: "/util/handlers"},
{text: "Links", link: "/util/links"},
{text: "Profile", link: "/util/profile"},
{text: "Relay", link: "/util/relay"},
{text: "Repository", link: "/util/repository"},
{text: "Tags", link: "/util/tags"},
{text: "Zaps", link: "/util/zaps"},
],
},
{
text: "@welshman/content",
link: "/content/",
items: [
{text: "Parser", link: "/content/parser"},
{text: "Renderer", link: "/content/renderer"},
],
},
{
text: "@welshman/feeds",
link: "/feeds/",
items: [
{text: "Core", link: "/feeds/core"},
{text: "Utilities", link: "/feeds/utils"},
{text: "Compiler", link: "/feeds/compiler"},
{text: "Controller", link: "/feeds/controller"},
],
},
{
text: "@welshman/editor",
link: "/editor/",
items: [],
},
{
text: "@welshman/store",
link: "/store/",
items: [
{text: "Basic utilities", link: "/store/basic"},
{text: "Event stores", link: "/store/events"},
],
},
{
text: "@welshman/net",
link: "/net/",
items: [
{text: "Context", link: "/net/context"},
{text: "Executor", link: "/net/executor"},
{text: "Subscribe", link: "/net/subscribe"},
{text: "Publish", link: "/net/publish"},
{text: "Sync", link: "/net/sync"},
{text: "Pool", link: "/net/pool"},
{text: "Targets", link: "/net/targets"},
{text: "Tracker", link: "/net/tracker"},
{text: "Connection", link: "/net/connection"},
{text: "Socket", link: "/net/socket"},
],
},
{
text: "@welshman/dvm",
link: "/dvm/",
items: [
{text: "Handler", link: "/dvm/handler"},
{text: "Request", link: "/dvm/request"},
],
},
{
text: "@welshman/signer",
link: "/signer/",
items: [
{text: "ISigner", link: "/signer/isigner"},
{text: "NIP-01", link: "/signer/nip-01"},
{text: "NIP-07", link: "/signer/nip-07"},
{text: "NIP-46", link: "/signer/nip-46"},
{text: "NIP-55", link: "/signer/nip-55"},
{text: "NIP-59", link: "/signer/nip-59"},
],
},
{
text: "@welshman/app",
link: "/app/",
items: [
{text: "Context", link: "/app/context"},
{text: "Storage", link: "/app/storage"},
{text: "Router", link: "/app/router"},
{text: "Session", link: "/app/session"},
{text: "Collection", link: "/app/collection"},
{text: "Commands", link: "/app/commands"},
{text: "Subscription", link: "/app/subscription"},
{text: "Publish (Thunks)", link: "/app/thunks"},
{text: "Feed", link: "/app/feed"},
{text: "Tag utilities", link: "/app/tags"},
{text: "Topics", link: "/app/topics"},
{text: "Web of Trust", link: "/app/wot"},
{text: "Stores and Loaders", link: "/app/storesandloaders"},
],
},
],
},
socialLinks: [{icon: "github", link: "https://github.com/vuejs/vitepress"}],
},
})
+103
View File
@@ -0,0 +1,103 @@
# Collection Stores
The `collection` utility creates stores that handle caching, loading, and indexing of Nostr data. It provides a consistent pattern for managing entities that need to be fetched from the network and cached locally.
```typescript
const {
indexStore, // Map of all items by key
deriveItem, // Get reactive item by key
loadItem // Trigger network load
} = collection({
name: "storeName", // For persistence
store: writable([]), // Base store
getKey: item => item.id // How to index items
load: async (key) => { // Network loader
// Load logic here
}
})
```
## Available Collections
```typescript
// Profiles
profiles profilesByPubkey deriveProfile loadProfile
// Lists
follows followsByPubkey deriveFollows loadFollows
mutes mutesByPubkey deriveMutes loadMutes
pins pinsByPubkey derivePins loadPins
// Relays
relays relaysByUrl deriveRelay loadRelay
relaySelections relaySelectionsByPubkey deriveRelaySelections loadRelaySelections
inboxRelaySelections inboxRelaySelectionsByPubkey deriveInboxRelaySelections loadInboxRelaySelections
// Identity
handles handlesByNip05 deriveHandle loadHandle
zappers zappersByLnurl deriveZapper loadZapper
```
## Real World Examples
### Loading and Displaying Profiles
```typescript
import {
deriveProfile,
loadProfile,
displayProfile
} from '@welshman/app'
// In a Svelte component
let profile
// Subscribe to profile changes
$: profile = $deriveProfile(pubkey)
// Load automatically triggers when needed
onMount(() => {
loadProfile(pubkey, {
// Optional request params
relays: ["wss://relay.example.com"]
})
})
// Display with fallback
$: name = displayProfile(profile, "unknown")
```
### Managing Relay Selections
```typescript
import {
deriveRelaySelections,
loadRelaySelections,
getReadRelayUrls,
getWriteRelayUrls
} from '@welshman/app'
// Get user's relay preferences
const selections = deriveRelaySelections(pubkey).get()
// Load from network if needed
await loadRelaySelections(pubkey)
// Get read/write URLs
const readRelays = getReadRelayUrls(selections)
const writeRelays = getWriteRelayUrls(selections)
// Use with router
const relays = ctx.app.router
.FromPubkey(pubkey)
.getUrls()
```
Each collection automatically:
- Caches to IndexedDB
- Deduplicates network requests
- Updates reactively
- Provides typed access
- Handles loading states
The pattern is consistent across all stores, making it predictable to work with different types of nostr data.
+88
View File
@@ -0,0 +1,88 @@
# Commands
High-level commands for common Nostr operations.
Each command handles signing, encryption, and relay selection automatically.
## Available Commands
```typescript
// List Management
follow(pubkey)
unfollow(pubkey)
mute(pubkey)
unmute(pubkey)
pin(tag)
unpin(tag)
```
Each command returns a [`Thunk`](app/thunk) which:
- Optimistically updates local state
- Signs and publishes the event
- Can be aborted within a delay window
- Reports publish progress
## Real World Examples
### Following/Unfollowing Users
```typescript
import {follow, unfollow, userFollows} from '@welshman/app'
// Follow with optimistic update
const followUser = async (pubkey: string) => {
// Creates and publishes event with an updated follow list
const thunk = await follow(pubkey)
// Track publish status per relay
thunk.status.subscribe(statuses => {
for (const [url, status] of Object.entries(statuses)) {
console.log(`${url}: ${status}`)
}
})
// Can abort within delay window
setTimeout(() => thunk.controller.abort(), 1000)
}
// Unfollow works the same way
const unfollowUser = async (pubkey: string) => {
const thunk = await unfollow(pubkey)
// Wait for completion
const results = await thunk.result
}
```
### Managing Pins
```typescript
import {pin, unpin, userPins} from '@welshman/app'
// Pin an event with context
const pinEvent = async (event: TrustedEvent) => {
const thunk = await pin([
'e', event.id,
ctx.app.router.Event(event).getUrl()
])
// Handle specific relay errors
thunk.status.subscribe(statuses => {
for (const [url, {status, message}] of Object.entries(statuses)) {
if (status === 'failure') {
console.error(`Failed on ${url}: ${message}`)
}
}
})
}
```
All commands:
- Handle encryption automatically
- Select appropriate relays
- Update local state immediately
- Allow soft-undo via abort
- Report per-relay status
- Return consistent Thunk interface
Commands provide a high-level way to modify the Nostr state without dealing with the complexities of event creation, encryption, and relay selection.
+139
View File
@@ -0,0 +1,139 @@
# Application Context
The `@welshman/app` package uses a global context system to configure core behaviors.
Understanding the app context is essential as it powers [session/authentication](/app/session), [relay routing](/app/relay) and [request handling](/app/request).
## Basic Setup
```typescript
import {ctx, setContext} from '@welshman/lib'
import {getDefaultNetContext, getDefaultAppContext} from '@welshman/app'
// Initialize app with default settings
setContext({
net: getDefaultNetContext(),
app: getDefaultAppContext()
})
// Access context anywhere
console.log(ctx.app.router)
console.log(ctx.net.pool)
```
## Default App Context
```typescript
export type AppContext = {
// Smart relay routing system
router: Router
// Time to wait between batched requests (ms)
requestDelay: number // default: 50
// Time to wait for NIP-42 relay auth (ms)
authTimeout: number // default: 300
// Time to wait for request completion (ms)
requestTimeout: number // default: 3000
// URL of metadata service (optional)
dufflepudUrl?: string
// Additional relays for indexed content
indexerRelays?: string[]
}
// Example with custom settings
setContext({
app: getDefaultAppContext({
requestDelay: 100,
authTimeout: 500,
requestTimeout: 5000,
dufflepudUrl: "https://api.example.com",
indexerRelays: [
"wss://relay.example.com",
"wss://indexed.example.com"
]
})
})
```
## Network Context
```typescript
export type NetContext = {
// Global connection pool
pool: Pool
// How to handle NIP-42 auth
authMode: AuthMode // default: 'implicit'
// Event validation and handling
onEvent: (url: string, event: TrustedEvent) => void
isDeleted: (url: string, event: TrustedEvent) => boolean
isValid: (url: string, event: TrustedEvent) => boolean
// Event signing (used by all packages)
signEvent: (event: StampedEvent) => Promise<SignedEvent>
// Subscription optimization
optimizeSubscriptions: (subs: Subscription[]) => RelaysAndFilters[]
}
// Example with custom validation
setContext({
net: getDefaultNetContext({
// Custom event validation
isValid: (url, event) => {
if (url === LOCAL_RELAY_URL) return true
return hasValidSignature(event)
},
// Track deleted events
isDeleted: (url, event) =>
repository.isDeleted(event),
// Custom event handling
onEvent: (url, event) => {
// Save to local repository
repository.publish(event)
// Track which relay it came from
tracker.track(event.id, url)
}
})
})
```
## Using Context Values
Once configured, context values are used throughout the app:
```typescript
import {ctx} from '@welshman/lib'
// Smart relay routing
const relays = ctx.app.router
.ForPubkey(pubkey)
.getUrls()
// Publish with timeout
const pub = publish({
event,
relays,
timeout: ctx.app.requestTimeout
})
// Subscribe with auth
const sub = subscribe({
filters,
relays,
authTimeout: ctx.app.authTimeout
})
// Check connection pool
const connected = ctx.net.pool
.get(relay)
.socket.status === 'open'
```
+96
View File
@@ -0,0 +1,96 @@
# Feed
The feed system provides a powerful way to compose and load complex `Nostr` queries. It supports user scopes, web of trust filtering, DVM integration, and thread construction.
## Controller
The `controller.load()` function is the main interface for fetching events from a feed. It handles all the complexity of relay selection, subscription management, and event filtering.
```typescript
import {createFeedController} from '@welshman/app'
import {scopeFeed, wotFeed} from '@welshman/feeds'
const controller = createFeedController({
// Define what to load
feed: scopeFeed("follows"),
// Optional configurations
closeOnEose: true, // Close after getting all events
onEvent: event => {}, // Handle events as they arrive
onEose: url => {}, // Handle EOSE from each relay
onComplete: () => {}, // Called when all relays complete
})
// Load first 20 events
const events = await controller.load(20)
// Load next 20 events
const moreEvents = await controller.load(20)
```
The controller maintains its state between loads, so subsequent calls will:
- Continue from last position
- Use appropriate time windows
- Skip already seen events
- Maintain relay connections
## Paginated Feed
```typescript
import {intersectionFeed, scopeFeed, wotFeed} from '@welshman/feeds'
const HomeFeed = {
let events = []
let loading = false
let controller
onMount(() => {
// Create feed for home timeline
controller = createFeedController({
feed: intersectionFeed(
// Content from follows
scopeFeed("follows"),
// Filtered by web of trust
wotFeed({min: 0.1})
),
// Handle events as they arrive
onEvent: event => {
events = [...events, event]
},
// Track loading state
onComplete: () => {
loading = false
}
})
// Initial load
loadMore()
})
const loadMore = async () => {
if (loading) return
loading = true
// Load next batch
await controller.load(20)
}
}
```
Key points about `controller.load()`:
- Takes a limit parameter for batch size
- Returns a promise of loaded events
- Can be called repeatedly for pagination
- Handles subscription lifecycle
- Manages relay connections
- Deduplicates events
The controller is stateful and maintains:
- Current time window
- Seen events
- Active subscriptions
- Relay connections
This makes it ideal for implementing infinite scroll feeds, thread loading, and other paginated content scenarios.
+20
View File
@@ -0,0 +1,20 @@
# @welshman/app
A comprehensive framework for building nostr clients, powering production applications like [Coracle](https://coracle.social) and [Flotilla](https://flotilla.social). It provides a complete toolkit for managing events, subscriptions, user data, and relay connections.
## What's Included
- **Repository System** - Event storage and query capabilities
- **Router** - Intelligent relay selection for optimal networking
- **Feed Controller** - Manages feed creation and updates
- **Authentication** - User identity and key management
- **Event Actions** - High-level operations like reacting, replying, etc.
- **Profile Management** - User profile handling and metadata
- **Relay Directories** - Discovery and management of relays
## Installation
```bash
npm install @welshman/app
```
+103
View File
@@ -0,0 +1,103 @@
# Router
The Router is the critical component to efficiently enable the `outbox model` in your Nostr application. It handles relay selection for reading, writing, and discovering events while considering relay quality, user preferences, and network conditions.
## Overview
The router provides scenarios for common **Nostr** operations:
- Reading user profiles
- Publishing events
- Following threads
- Handling DMs
- Searching content
Each scenario considers:
- User's relay preferences (NIP-65)
- Event hints in tags
- Relay quality scores
- Fallback policies
- Connection status
## Basic Usage
```typescript
import {ctx, setContext} from '@welshman/lib'
import {getDefaultAppContext} from '@welshman/app'
// Initialize router
setContext({
app: getDefaultAppContext()
})
// Use router scenarios
const router = ctx.app.router
// Get relays for reading a profile
const readRelays = router.ForPubkey(pubkey).getUrls()
// Get relays for publishing
const writeRelays = router.FromUser().getUrls()
// Get relays for a thread
const threadRelays = router.Replies(event).getUrls()
```
## Thread Navigation
```typescript
import {ctx} from '@welshman/lib'
import {createEvent, NOTE} from '@welshman/util'
import {publishThunk} from '@welshman/app'
const loadThread = async (event: TrustedEvent) => {
// Get relays for root event
const rootRelays = ctx.app.router
.EventRoots(event)
.getUrls()
// Get relays for replies
const replyRelays = ctx.app.router
.EventParents(event)
.getUrls()
// Get relays for mentions
const mentionRelays = ctx.app.router
.EventMentions(event)
.getUrls()
// Load from all relevant relays
await Promise.all([
subscribe({filters, relays: rootRelays}),
subscribe({filters, relays: replyRelays}),
subscribe({filters, relays: mentionRelays})
])
}
// Posting a reply
const reply = async (parent: TrustedEvent, content: string) => {
const event = createEvent(NOTE, {content})
// Get optimal relays for publishing
const relays = ctx.app.router
.PublishEvent(event)
// Skip .onion relays
.allowOnion(false)
// Allow up to 5 relays
.limit(5)
.getUrls()
return publishThunk({event, relays})
}
```
## Router Features
- Smart relay selection based on context
- Quality scoring of relays
- Fallback strategies
- Handling of special relay types (.onion, local)
- Automatic weight calculation
- Connection state awareness
- NIP-65 compliance
The router is central to efficient nostr operations, ensuring events reach their intended audience while minimizing unnecessary network traffic.
+186
View File
@@ -0,0 +1,186 @@
# Session Management
The session system provides a unified way to handle different authentication methods:
- Secret Key NIP-01
- Nostr Extensions NIP-07
- Bunker URL NIP-46
- Amber or in-device NIP-55
while managing user state and encryption capabilities.
## Overview
Sessions are stored in local storage and can be:
- Persisted across page reloads
- Used with multiple accounts
- Switched dynamically
- Backed by different signing methods
## Basic Usage
```typescript
import {ctx, setContext} from '@welshman/lib'
import {
getDefaultNetContext,
getDefaultAppContext,
pubkey,
sessions,
session,
addSession,
getNip07
} from '@welshman/app'
// Set up app config
setContext({
net: getDefaultNetContext(),
app: getDefaultAppContext(),
})
// Log in via NIP-07 extension (browser wallet)
if (await getNip07()) {
addSession({
method: 'nip07',
pubkey: await getNip07().getPublicKey()
})
}
// Get current session
console.log(session.get()) // Current active session
console.log(pubkey.get()) // Current pubkey
```
## Multiple Sessions
```typescript
import {sessions, pubkey, addSession, dropSession} from '@welshman/app'
// Add multiple sessions
addSession({method: 'nip07', pubkey: 'abc...'})
addSession({method: 'nip46', pubkey: 'def...', secret: '123'})
// Switch between sessions
pubkey.set('abc...') // Activates that session
// Remove a session
dropSession('abc...')
// List all sessions
console.log(sessions.get())
```
## NIP-46 (Bunker) Authentication
```typescript
import {Nip46Broker, Nip46Signer} from '@welshman/signer'
import {addSession} from '@welshman/app'
// Connect to a bunker
const clientSecret = makeSecret()
const relays = ['wss://relay.damus.io']
const broker = Nip46Broker.get({relays, clientSecret})
// Generate nostrconnect URL for the bunker
const connectUrl = await broker.makeNostrconnectUrl({
name: "My App",
url: "https://myapp.com"
})
// Wait for user to approve in bunker
const response = await broker.waitForNostrconnect(connectUrl)
// Create session
addSession({
method: 'nip46',
pubkey: response.event.pubkey,
secret: clientSecret,
handler: {
pubkey: response.event.pubkey,
relays
}
})
```
## Using Session Signer
```typescript
import {signer, session} from '@welshman/app'
import {createEvent, NOTE} from '@welshman/util'
// Current session's signer is always ready to use
const event = await signer.get().sign(
createEvent(NOTE, {content: "Hello Nostr!"})
)
// Encrypt content for private notes
const encrypted = await signer.get().nip44.encrypt(
pubkey,
"Secret message"
)
```
## Session Persistence
Sessions are automatically persisted to local storage. On page load:
```typescript
import {pubkey, sessions} from '@welshman/app'
// Sessions load automatically from local storage
console.log(sessions.get()) // All stored sessions
// the current active session
console.log(session.get())
// Last active pubkey is restored
console.log(pubkey.get())
```
## Session Types
```typescript
type SessionNip07 = {
method: "nip07"
pubkey: string
}
type SessionNip46 = {
method: "nip46"
pubkey: string
secret: string
handler: {
pubkey: string
relays: string[]
}
}
type SessionNip01 = {
method: "nip01"
pubkey: string
secret: string
}
```
## Error Handling
```typescript
import {tryCatch} from '@welshman/lib'
import {addSession, getNip07} from '@welshman/app'
const login = async () => {
const nip07 = await tryCatch(getNip07)
if (!nip07) {
throw new Error("No NIP-07 extension found")
}
const pubkey = await tryCatch(
() => nip07.getPublicKey()
)
if (!pubkey) {
throw new Error("Failed to get public key")
}
addSession({method: 'nip07', pubkey})
}
```
+68
View File
@@ -0,0 +1,68 @@
# Storage
The storage system provides IndexedDB persistence for stores and repositories.
It's critical to initialize this early in your application lifecycle to ensure data consistency.
```typescript
import {
initStorage,
storageAdapters,
throttled,
repository,
tracker,
relays,
handles,
freshness,
plaintext
} from '@welshman/app'
// Real world example from Coracle
const initializeStorage = async () => {
const ready = initStorage("coracle-db", 1, {
// Persist relay info
relays: {
keyPath: "url",
store: throttled(3000, relays)
},
// Persist NIP-05 handles
handles: {
keyPath: "nip05",
store: throttled(3000, handles)
},
// Track data freshness
freshness: storageAdapters.fromObjectStore(
freshness,
{throttle: 3000}
),
// Store decrypted content
plaintext: storageAdapters.fromObjectStore(
plaintext,
{throttle: 3000}
),
// Store events and their sources
events: storageAdapters.fromRepositoryAndTracker(
repository,
tracker,
{throttle: 3000}
)
})
// Wait for storage to be ready
await ready
// App can now start loading data
}
```
The storage system:
- Persists data across page reloads
- Throttles writes for performance
- Handles store migrations
- Syncs bidirectionally
- Supports custom adapters
Initialize storage before making any subscriptions or loading data to ensure proper caching behavior.
+108
View File
@@ -0,0 +1,108 @@
# Stores and Loaders
The `@welshman/app` package provides a powerful system of collection-based reactive stores and loader utilities.
These utilities follow a consistent pattern for working with various types of Nostr data, making it easy to:
1. Query data from the repository
2. Transform it into application-specific structures
3. Access it reactively in your UI
4. Trigger network loading when needed
## Core Concept
Each collection-based module exports a similar set of utilities:
```typescript
// Common pattern across collection-based modules
export const {
// Main collection store (derived from repository)
store: follows,
// Indexed map for efficient lookup
indexStore: followsByPubkey,
// Function to get a reactive store for a specific item
deriveItem: deriveFollows,
// Function to trigger loading an item from the network
loadItem: loadFollows
} = collection({
name: "collection-name",
store: baseStore,
getKey: item => item.keyProperty,
load: async (key) => { /* Loading logic */ }
})
```
## Available Collections
| Collection | Key | Kind | Description |
|------------|-----|------|-------------|
| `follows` | pubkey | 3 | User follow lists |
| `mutes` | pubkey | 10000 | User mute lists |
| `pins` | pubkey | 10001 | User pinned items |
| `profiles` | pubkey | 0 | User profile metadata |
| `relaySelections` | pubkey | 10002 | User relay preferences |
| `inboxRelaySelections` | pubkey | 10005 | User inbox relay settings |
| `zappers` | lnurl | - | Lightning zapper metadata |
| `handles` | nip05 | - | NIP-05 identifier metadata |
## Usage Examples
### Loading and Accessing Data
```typescript
import { loadProfile, deriveProfile, profilesByPubkey } from '@welshman/app'
// Trigger loading a profile from the network
await loadProfile('pubkey123')
// Get a reactive store for a specific profile
const profile = deriveProfile('pubkey123')
// Access all profiles by pubkey
const allProfiles = profilesByPubkey.get()
const specificProfile = allProfiles.get('pubkey123')
```
### User-Specific Collections
Several modules provide user-specific derived stores that automatically load data for the currently signed-in user:
```typescript
import { userProfile, userFollows, userMutes, userPins } from '@welshman/app'
// These are derived stores that automatically:
// 1. Watch for changes to the current user's pubkey
// 2. Load the appropriate data when the user changes
// 3. Provide the data reactively
userProfile.subscribe(profile => {
// Current user's profile data
})
userFollows.subscribe(follows => {
// Current user's follow list
})
```
### Web of Trust Utilities
The `wot.ts` module provides additional utilities for analyzing the social graph:
```typescript
import { getFollows, getFollowers, getNetwork, getWotScore } from '@welshman/app'
// Get users followed by a pubkey
const followedUsers = getFollows('pubkey123')
// Get users following a pubkey
const followers = getFollowers('pubkey123')
// Get extended network (follows-of-follows)
const network = getNetwork('pubkey123')
// Calculate trust score between users
const score = getWotScore('userPubkey', 'targetPubkey')
```
+153
View File
@@ -0,0 +1,153 @@
# Subscription System
The subscription system extends Nostr's base subscription model with intelligent caching, repository integration, and configurable behaviors.
## Key Concepts
- **Local Repository**: Events are automatically cached and tracked
- **Cache Intelligence**: Smart decisions about when to use cached data
- **Relay Integration**: Works with the router for optimal relay selection
- **Configurable Behavior**: Control caching and timeouts
## Configuration Options
```typescript
type SubscribeRequest = {
// Required
filters: Filter[] // What to query
// Behavior Control
closeOnEose?: boolean // Auto-close and use cache
timeout?: number // Max time to wait
authTimeout?: number // Time for auth negotiation
requestDelay?: number // Delay between batched requests
// Optional
relays?: string[] // Specific relays to query
// Event Handlers
onEvent?: (event: TrustedEvent) => void
onEose?: (url: string) => void
onComplete?: () => void
}
```
## Cache Behavior Control
The `closeOnEose` parameter is crucial for controlling caching behavior:
```typescript
// WITH closeOnEose: true (default for load())
// - Checks cache first
// - Returns cached results if complete
// - Closes after EOSE
// - Good for: Known events, historical data
const loadKnownEvent = async (id: string) => {
const events = await load({
filters: [{ids: [id]}],
closeOnEose: true
})
return events[0]
}
// WITH closeOnEose: false
// - Always queries relays
// - Stays open for updates
// - Ignores cache completeness
// - Good for: Replaceable events, live data
const watchProfile = (pubkey: string) => {
return subscribe({
filters: [{
kinds: [PROFILE],
authors: [pubkey]
}],
closeOnEose: false // Force relay query
})
}
```
## Common Usage Patterns
### One-time Queries
```typescript
// Load specific event
const event = await load({
filters: [{ids: [eventId]}]
// closeOnEose: true by default
})
// Load latest profile
const profile = await load({
filters: [{
kinds: [PROFILE],
authors: [pubkey],
limit: 1
}],
closeOnEose: false // Get latest from network
})
```
### Live Subscriptions
```typescript
// Watch for updates
const sub = subscribe({
filters: [{
kinds: [NOTE],
since: now() // Only new events
}],
closeOnEose: false, // Stay open
})
sub.on('event', (url, event) => {
// Handle live events
})
```
### Smart Caching
```typescript
// Profile loader with refresh control
const loadProfile = async (pubkey: string, options = {}) => {
const {
forceRefresh = false, // Skip cache
timeout = 3000, // Max wait time
relays = [] // Optional relay override
} = options
// Get optimal relays if not specified
const targetRelays = relays.length > 0
? relays
: ctx.app.router.ForPubkey(pubkey).getUrls()
return new Promise((resolve) => {
const sub = subscribe({
filters: [{
kinds: [PROFILE],
authors: [pubkey],
limit: 1
}],
relays: targetRelays,
closeOnEose: !forceRefresh, // Control cache behavior
timeout,
onEvent: (url, event) => {
resolve(event)
sub.close()
},
onComplete: () => resolve(null)
})
})
}
```
## Repository Integration
All events from subscriptions are automatically:
- Saved to the repository
- Tracked to their source relay
- Checked against deletion status
The repository serves as an intelligent cache layer, making subsequent queries for the same data faster.
+69
View File
@@ -0,0 +1,69 @@
# Tag Utilities
The tag utilities provide helper functions for creating properly formatted Nostr event tags with correct relay hints and metadata.
These are especially useful when creating events that reference other events or users.
## Tag Creators
### User Tags
```typescript
import {tagPubkey} from '@welshman/app'
// Create a p-tag with relay hint and profile name
const tag = tagPubkey(authorPubkey)
// => ["p", pubkey, "wss://relay.example.com", "username"]
```
### Event Reference Tags
```typescript
import {
tagEvent, // Basic event reference
tagEventForQuote, // For quoting events
tagEventForReply, // For reply threads
tagEventForComment, // For NIP-23 comments
tagEventForReaction // For reactions
} from '@welshman/app'
// Real world example: Creating a reply
const createReply = async (parent: TrustedEvent, content: string) => {
// Get proper tags for a reply, including:
// - All referenced pubkeys
// - Root/reply markers
// - Inherited mentions
// - Relay hints
const tags = tagEventForReply(parent)
const event = await signer.get().sign(
createEvent(NOTE, {
content,
tags,
created_at: now()
})
)
return publishThunk({
event,
// Use relay hints from tags
relays: ctx.app.router.PublishEvent(event).getUrls()
})
}
```
All tag creators:
- Add appropriate relay hints using the router
- Handle replaceable/parameterized events
- Follow adequate NIP-10/NIP-22 conventions for threading
- Include metadata like usernames
- Deduplicate references
- Preserve tag order
The tagging system is crucial for:
- Thread construction
- Event reactions
- User mentions
- Zap splits
Tag utilities ensure consistent and correct tag creation across the application while integrating with the router for relay hints.
+52
View File
@@ -0,0 +1,52 @@
# Thunks
Thunks provide optimistic updates for event publishing. They immediately update the local repository while handling the actual signing and publishing asynchronously, making the UI feel more responsive.
## Overview
A thunk:
- Updates local state immediately
- Handles event signing in the background
- Manages publish status per relay
- Supports soft-undo via abort
- Can be delayed/cancelled
- Tracks successful publishes
## Basic Usage
```typescript
import {publishThunk} from '@welshman/app'
import {createEvent, NOTE} from '@welshman/util'
const publish = async (content: string) => {
// Get optimal relays for publishing
const relays = ctx.app.router
.FromUser()
.getUrls()
// Create and publish thunk
const thunk = await publishThunk({
event: createEvent(NOTE, {content}),
relays,
delay: 3000, // 3s window for abort
})
// Track publish status
thunk.status.subscribe(statuses => {
for (const [url, {status, message}] of Object.entries(statuses)) {
console.log(`${url}: ${status} ${message}`)
}
})
// Can abort within delay window
setTimeout(() => {
if (userWantsToCancel) {
thunk.controller.abort()
}
}, 1000)
// Wait for completion
const results = await thunk.result
return results
}
```
+39
View File
@@ -0,0 +1,39 @@
# Topics
The topics system provides a reactive way to track and count hashtags across all events in the repository. It automatically updates as new events arrive or are removed.
```typescript
import {topics} from '@welshman/app'
// In a Svelte component
<script>
// Reactive list of all topics with counts
$: topicList = $topics
.sort((a, b) => b.count - a.count)
.slice(0, 20)
</script>
<div class="topics">
{#each topicList as {name, count}}
<a href="/t/{name}">
#{name}
<span class="count">({count})</span>
</a>
{/each}
</div>
```
The store:
- Updates automatically with new events
- Maintains topic counts
- Is throttled to prevent excess updates
- Is case-insensitive
- Integrates with the repository
Think of it as a live tag cloud that stays in sync with your local event cache.
This is commonly used for:
- Tag clouds
- Topic discovery
- Content organization
- Trending topics
+63
View File
@@ -0,0 +1,63 @@
# Web of Trust (WOT) Module
The `wot.ts` module provides utilities for implementing a Web of Trust system within Nostr applications. This system analyzes social connections (follows and mutes) to build a reputation graph that can be used for content filtering, user scoring, and discovery.
## Core Concepts
- **Follow Trust**: Users gain positive reputation when followed by those in your network
- **Mute Distrust**: Users lose reputation when muted by those in your network
- **WOT Graph**: A reactive weighted directed graph representing trust relationships
- **Contextual Scoring**: Reputation scores that adapt based on user's social graph
## API Reference
### Social Graph Navigation
```typescript
// Get users followed by a specific pubkey
getFollows(pubkey: string): string[]
// Get users who have muted a specific pubkey
getMutes(pubkey: string): string[]
// Get followers of a specific pubkey
getFollowers(pubkey: string): string[]
// Get users who have muted a specific pubkey
getMuters(pubkey: string): string[]
// Get the extended network (follows-of-follows) for a pubkey
getNetwork(pubkey: string): string[]
```
### Trust Analysis
```typescript
// Get follows of a user who also follow a target
getFollowsWhoFollow(pubkey: string, target: string): string[]
// Get follows of a user who have muted a target
getFollowsWhoMute(pubkey: string, target: string): string[]
// Calculate trust score between users
getWotScore(pubkey: string, target: string): number
```
### Reactive Stores
```typescript
// Map of follower lists by pubkey
followersByPubkey: Readable<Map<string, Set<string>>>
// Map of muter lists by pubkey
mutersByPubkey: Readable<Map<string, Set<string>>>
// The full WOT graph with scores (pubkey → score)
wotGraph: Readable<Map<string, number>>
// The maximum WOT score in the graph
maxWot: Readable<number>
// Derive the WOT score for a specific user
deriveUserWotScore(targetPubkey: string): Readable<number>
```
+1
View File
@@ -0,0 +1 @@
window.hierarchyData = "eJyd1VFv2yAQB/DvwjPNOGwM+HHTHipt09TsrYoq16ELKoHIkE1Tle8+4dQrSSfl4qdIzv39850Bv5AhhBRJe6+kbChwaMSKksE8OdMnG3wk7QvJ/+Vf320NacnnrU3JDISSZ+vXpAWuKNkPjrSkd12MJn54+G1c3Gw7/+Ds4+I1sNikrSP0WERakuL6Jt/h5niBkn5j3XownrT3WlaKat0ABQaVoMC4qiiwmisKTORnZZLn60ozCsBEQwGEBAoglaAAuhYUuKiAAm+UWB0oyTct+vgUvD92iWvFm7R4y1zoJnO6gXJs/SbgoVx9mRiHUxhfQt85PDKWYxSuqkL5unfJ4pWxHKPUvHw930O4opVcjTHEyVK+M677g0fGcowiuThX4pVMxDhKs8JZ7h9jP9jddUu6TCHMvNUK88fQ9c/YwyBzrwGMVOty1X2zOwZL+9NjtTjWLoocBhWVPkPlTFRegUo4Revm4xDQc31DpxwGlUqco3emN/bXHHZKYmBdv4OXxq/nsMccAuVMy3forPc65TAoADtFhZiHTjkU2pTjvT0DuWgm0Ppkhqeu/595e8H7lz39bo+blo67iI4zp+MQVuODiQrmnb37ZB368M1f+xNmF6JNYbjOmkKXwMPhL+By/ic="
+99
View File
@@ -0,0 +1,99 @@
:root {
--light-hl-0: #AF00DB;
--dark-hl-0: #C586C0;
--light-hl-1: #000000;
--dark-hl-1: #D4D4D4;
--light-hl-2: #001080;
--dark-hl-2: #9CDCFE;
--light-hl-3: #A31515;
--dark-hl-3: #CE9178;
--light-hl-4: #008000;
--dark-hl-4: #6A9955;
--light-hl-5: #795E26;
--dark-hl-5: #DCDCAA;
--light-hl-6: #0000FF;
--dark-hl-6: #569CD6;
--light-hl-7: #0070C1;
--dark-hl-7: #4FC1FF;
--light-hl-8: #098658;
--dark-hl-8: #B5CEA8;
--light-hl-9: #267F99;
--dark-hl-9: #4EC9B0;
--light-hl-10: #000000FF;
--dark-hl-10: #D4D4D4;
--light-code-background: #FFFFFF;
--dark-code-background: #1E1E1E;
}
@media (prefers-color-scheme: light) { :root {
--hl-0: var(--light-hl-0);
--hl-1: var(--light-hl-1);
--hl-2: var(--light-hl-2);
--hl-3: var(--light-hl-3);
--hl-4: var(--light-hl-4);
--hl-5: var(--light-hl-5);
--hl-6: var(--light-hl-6);
--hl-7: var(--light-hl-7);
--hl-8: var(--light-hl-8);
--hl-9: var(--light-hl-9);
--hl-10: var(--light-hl-10);
--code-background: var(--light-code-background);
} }
@media (prefers-color-scheme: dark) { :root {
--hl-0: var(--dark-hl-0);
--hl-1: var(--dark-hl-1);
--hl-2: var(--dark-hl-2);
--hl-3: var(--dark-hl-3);
--hl-4: var(--dark-hl-4);
--hl-5: var(--dark-hl-5);
--hl-6: var(--dark-hl-6);
--hl-7: var(--dark-hl-7);
--hl-8: var(--dark-hl-8);
--hl-9: var(--dark-hl-9);
--hl-10: var(--dark-hl-10);
--code-background: var(--dark-code-background);
} }
:root[data-theme='light'] {
--hl-0: var(--light-hl-0);
--hl-1: var(--light-hl-1);
--hl-2: var(--light-hl-2);
--hl-3: var(--light-hl-3);
--hl-4: var(--light-hl-4);
--hl-5: var(--light-hl-5);
--hl-6: var(--light-hl-6);
--hl-7: var(--light-hl-7);
--hl-8: var(--light-hl-8);
--hl-9: var(--light-hl-9);
--hl-10: var(--light-hl-10);
--code-background: var(--light-code-background);
}
:root[data-theme='dark'] {
--hl-0: var(--dark-hl-0);
--hl-1: var(--dark-hl-1);
--hl-2: var(--dark-hl-2);
--hl-3: var(--dark-hl-3);
--hl-4: var(--dark-hl-4);
--hl-5: var(--dark-hl-5);
--hl-6: var(--dark-hl-6);
--hl-7: var(--dark-hl-7);
--hl-8: var(--dark-hl-8);
--hl-9: var(--dark-hl-9);
--hl-10: var(--dark-hl-10);
--code-background: var(--dark-code-background);
}
.hl-0 { color: var(--hl-0); }
.hl-1 { color: var(--hl-1); }
.hl-2 { color: var(--hl-2); }
.hl-3 { color: var(--hl-3); }
.hl-4 { color: var(--hl-4); }
.hl-5 { color: var(--hl-5); }
.hl-6 { color: var(--hl-6); }
.hl-7 { color: var(--hl-7); }
.hl-8 { color: var(--hl-8); }
.hl-9 { color: var(--hl-9); }
.hl-10 { color: var(--hl-10); }
pre, code { background: var(--code-background); }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
# @welshman/content
`@welshman/content` is a comprehensive content processing library designed specifically for Nostr applications.
It provides a robust system for parsing, processing, and rendering Nostr content while handling various special formats and entities common in the Nostr ecosystem.
## Core Concepts
The package is built around two main components:
1. **Parser**: Transforms raw content into structured elements
```typescript
const parsed = parse({
content: "Hello #nostr, check nostr:npub1...",
tags: [["p", "pubkey123"]]
})
```
2. **Renderer**: Converts parsed content into desired output format
```typescript
const html = renderAsHtml(parsed).toString()
const text = renderAsText(parsed).toString()
```
## Common Use Cases
- Rendering Nostr notes with proper entity linking
- Processing and displaying user content safely
- Handling rich text content in Nostr clients
- Converting between different content formats
- Creating customized content displays
## Quick Example
```typescript
import { parse, renderAsHtml, truncate } from '@welshman/content'
// Parse and process content
const parsed = parse({
content: `
Hello #nostr!
Check out this note: nostr:note1...
https://example.com/image.jpg
`,
tags: [["p", "pubkey123"]]
})
// Truncate if needed
const truncated = truncate(parsed, {
maxLength: 500,
mediaLength: 150
})
// Render as HTML
const html = renderAsHtml(truncated, {
entityBase: "https://your-app.com/"
}).toString()
```
## Installation
```bash
npm install @welshman/content
```
This package is essential for applications that need to handle Nostr content in a structured and safe way, providing all the necessary tools for parsing, processing, and rendering Nostr-specific content formats.
+181
View File
@@ -0,0 +1,181 @@
# Content Parser
The content parser system in `@welshman/content` provides a powerful way to parse Nostr content into structured elements.
It handles various types of content including Nostr entities, links, code blocks, and special formats.
## Content Types
### Basic Types
```typescript
enum ParsedType {
Text = "text", // Plain text
Newline = "newline", // Line breaks
Topic = "topic", // Hashtags (#nostr)
Code = "code", // Code blocks (inline and multi-line)
Link = "link", // URLs
LinkGrid = "link-grid" // Grid of media links
}
```
### Nostr-specific Types
```typescript
enum ParsedType {
Event = "event", // Nostr events (note1/nevent1)
Profile = "profile", // Profiles (npub1/nprofile1)
Address = "address", // Addresses (naddr1)
}
```
### Special Format Types
```typescript
enum ParsedType {
Cashu = "cashu", // Cashu tokens
Invoice = "invoice", // Lightning invoices
Ellipsis = "ellipsis" // Truncation marker
}
```
## Parsing Content
### Main Parser
```typescript
const parse = ({
content = "",
tags = []
}: {
content?: string
tags?: string[][]
}) => Parsed[]
// Example
const parsed = parse({
content: "Hello #nostr, check nostr:npub1...",
tags: [["p", "pubkey123"]]
})
```
### Available Parsers
The system includes specialized parsers for each content type:
```typescript
// Nostr Entities
parseAddress(text: string, context: ParseContext): ParsedAddress | void
parseEvent(text: string, context: ParseContext): ParsedEvent | void
parseProfile(text: string, context: ParseContext): ParsedProfile | void
// Code Blocks
parseCodeBlock(text: string, context: ParseContext): ParsedCode | void
parseCodeInline(text: string, context: ParseContext): ParsedCode | void
// Special Formats
parseCashu(text: string, context: ParseContext): ParsedCashu | void
parseInvoice(text: string, context: ParseContext): ParsedInvoice | void
// Basic Content
parseLink(text: string, context: ParseContext): ParsedLink | void
parseNewline(text: string, context: ParseContext): ParsedNewline | void
parseTopic(text: string, context: ParseContext): ParsedTopic | void
```
## Content Processing
### Truncation
```typescript
type TruncateOpts = {
minLength?: number // Minimum content length (default: 500)
maxLength?: number // Maximum content length (default: 700)
mediaLength?: number // Length value for media items (default: 200)
entityLength?: number // Length value for entities (default: 30)
}
const truncate = (
content: Parsed[],
options?: TruncateOpts
) => Parsed[]
// Example
const truncated = truncate(parsed, {
maxLength: 1000,
mediaLength: 150
})
```
### Link Processing
```typescript
// Consolidate consecutive image links into grids
const reduceLinks = (content: Parsed[]) => Parsed[]
// Example
const processed = reduceLinks(parsed)
```
## Type Guards
```typescript
// Basic content
isText(parsed: Parsed): parsed is ParsedText
isNewline(parsed: Parsed): parsed is ParsedNewline
isCode(parsed: Parsed): parsed is ParsedCode
isTopic(parsed: Parsed): parsed is ParsedTopic
// Links and media
isLink(parsed: Parsed): parsed is ParsedLink
isImage(parsed: Parsed): parsed is ParsedLink
isLinkGrid(parsed: Parsed): parsed is ParsedLinkGrid
// Nostr entities
isEvent(parsed: Parsed): parsed is ParsedEvent
isProfile(parsed: Parsed): parsed is ParsedProfile
isAddress(parsed: Parsed): parsed is ParsedAddress
// Special formats
isCashu(parsed: Parsed): parsed is ParsedCashu
isInvoice(parsed: Parsed): parsed is ParsedInvoice
isEllipsis(parsed: Parsed): parsed is ParsedEllipsis
```
## Complete Example
```typescript
// Parse content with tags
const parsed = parse({
content: `
Hello #nostr!
Check out this note: nostr:note1...
And this profile: nostr:npub1...
Some code: \`console.log("hello")\`
https://example.com/image.jpg
https://example.com/image2.jpg
`,
tags: [
["p", "pubkey123"],
["e", "event456"]
]
})
// Process the content
const processed = reduceLinks(parsed)
// Truncate if needed
const final = truncate(processed, {
maxLength: 500,
mediaLength: 150
})
// Check types and handle accordingly
final.forEach(item => {
if (isImage(item)) {
// Handle image
} else if (isProfile(item)) {
// Handle profile reference
} else if (isCode(item)) {
// Handle code block
}
})
```
This parser system provides a robust foundation for handling Nostr content, with support for various content types and processing needs. The type-safe approach ensures reliable content handling while maintaining flexibility for different use cases.
+195
View File
@@ -0,0 +1,195 @@
# Content Renderer
The renderer system in `@welshman/content` provides flexible ways to convert parsed content into text or HTML output. It includes customizable rendering options and specialized handlers for each content type.
## Renderer Class
```typescript
class Renderer {
constructor(readonly options: RenderOptions)
// Core methods
toString(): string
addText(value: string): void
addNewlines(count: number): void
addLink(href: string, display: string): void
addEntityLink(entity: string): void
}
```
## Render Options
```typescript
type RenderOptions = {
// String to use for newlines
newline: string
// Base URL for Nostr entities
entityBase: string
// Custom link rendering
renderLink: (href: string, display: string) => string
// Custom entity rendering
renderEntity: (entity: string) => string
}
```
## Built-in Renderers
### Text Renderer
```typescript
const textRenderOptions = {
newline: "\n",
entityBase: "",
renderLink: (href, display) => href,
renderEntity: (entity) => entity.slice(0, 16) + "…"
}
const textRenderer = makeTextRenderer({
// Override default options if needed
})
```
### HTML Renderer
```typescript
const htmlRenderOptions = {
newline: "\n",
entityBase: "https://njump.me/",
renderLink: (href, display) => {
const element = document.createElement("a")
element.href = sanitizeUrl(href)
element.target = "_blank"
element.innerText = display
return element.outerHTML
},
renderEntity: (entity) => entity.slice(0, 16) + "…"
}
const htmlRenderer = makeHtmlRenderer({
// Override default options if needed
})
```
## Content Type Renderers
```typescript
// Basic content
renderText(p: ParsedText, r: Renderer): void
renderNewline(p: ParsedNewline, r: Renderer): void
renderCode(p: ParsedCode, r: Renderer): void
renderTopic(p: ParsedTopic, r: Renderer): void
// Links
renderLink(p: ParsedLink, r: Renderer): void
// Nostr entities
renderEvent(p: ParsedEvent, r: Renderer): void
renderProfile(p: ParsedProfile, r: Renderer): void
renderAddress(p: ParsedAddress, r: Renderer): void
// Special formats
renderCashu(p: ParsedCashu, r: Renderer): void
renderInvoice(p: ParsedInvoice, r: Renderer): void
renderEllipsis(p: ParsedEllipsis, r: Renderer): void
```
## Usage Examples
### Basic Text Rendering
```typescript
const parsed = parse({
content: "Hello #nostr, check nostr:npub1...",
tags: []
})
// Render as plain text
const text = renderAsText(parsed).toString()
// Render as HTML
const html = renderAsHtml(parsed).toString()
```
### Custom Rendering Options
```typescript
// Custom text renderer
const customText = renderAsText(parsed, {
entityBase: "nostr:",
renderEntity: (entity) => entity.slice(0, 8)
}).toString()
// Custom HTML renderer
const customHtml = renderAsHtml(parsed, {
entityBase: "https://example.com/",
renderLink: (href, display) => `<a class="custom-link" href="${href}">${display}</a>`,
renderEntity: (entity) => `<span class="entity">${entity}</span>`
}).toString()
```
### Rendering Individual Elements
```typescript
const renderer = makeHtmlRenderer()
// Render single element
renderOne({
type: ParsedType.Link,
value: {
url: new URL("https://example.com"),
meta: {},
isMedia: false
},
raw: "https://example.com"
}, renderer)
// Render multiple elements
renderMany([
{
type: ParsedType.Text,
value: "Hello ",
raw: "Hello "
},
{
type: ParsedType.Topic,
value: "nostr",
raw: "#nostr"
}
], renderer)
```
### Complete Example
```typescript
// Parse and process content
const parsed = parse({
content: `
Check out this profile: nostr:npub1...
Code example:
\`console.log("hello")\`
#nostr #bitcoin
https://example.com/image.jpg
`,
tags: []
})
// Create custom renderer
const renderer = makeHtmlRenderer({
entityBase: "https://example.com/",
renderLink: (href, display) => {
if (href.endsWith('.jpg')) {
return `<img src="${href}" alt="${display}">`
}
return `<a href="${href}">${display}</a>`
},
renderEntity: (entity) => {
return `<span class="entity">${entity.slice(0, 8)}</span>`
}
})
// Render content
const html = render(parsed, renderer).toString()
```
The renderer system provides a flexible way to output parsed content in various formats while maintaining control over the rendering process. Its modular design allows for easy customization and extension for specific application needs.
+107
View File
@@ -0,0 +1,107 @@
# DVM (Data Vending Machine) Handler
The DVM Handler module provides a framework for creating and managing Data Vending Machines in the Nostr ecosystem.
A DVM is a service that listens for specific kinds of events and responds with processed data.
## Core Concepts
### DVM Handler
```typescript
type DVMHandler = {
stop?: () => void
handleEvent: (e: TrustedEvent) => AsyncGenerator<StampedEvent>
}
```
A handler defines how to process specific kinds of events and generate responses.
### DVM Options
```typescript
type DVMOpts = {
sk: string // Private key for signing responses
relays: string[] // Relays to connect to
handlers: Record<string, CreateDVMHandler> // Event handlers by kind
expireAfter?: number // Response expiration time in seconds
requireMention?: boolean // Require DVM to be mentioned in event
}
```
## Creating a DVM
```typescript
import { DVM } from '@welshman/dvm'
// Create handlers for different event kinds
const handlers = {
// Handler for kind 5001
"5001": (dvm: DVM) => ({
handleEvent: async function*(event: TrustedEvent) {
// Process event and yield responses
yield {
kind: 6001,
content: "Processed result",
created_at: now(),
tags: []
}
}
})
}
// Initialize DVM
const dvm = new DVM({
sk: "your-private-key",
relays: ["wss://relay.example.com"],
handlers,
expireAfter: 3600, // 1 hour
requireMention: true
})
// Start the DVM
await dvm.start()
```
## Example Implementation
```typescript
import { DVM, CreateDVMHandler } from '@welshman/dvm'
import { now } from '@welshman/lib'
// Create a search handler
const createSearchHandler: CreateDVMHandler = (dvm) => ({
handleEvent: async function*(event) {
const query = event.content
const results = await performSearch(query)
yield {
kind: 6001,
content: JSON.stringify(results),
created_at: now(),
tags: [
["search", query],
["results", String(results.length)]
]
}
}
})
// Initialize DVM
const searchDVM = new DVM({
sk: process.env.DVM_KEY!,
relays: ["wss://relay1.com", "wss://relay2.com"],
handlers: {
"5001": createSearchHandler
},
expireAfter: 24 * 60 * 60, // 24 hours
requireMention: true
})
// Start DVM
await searchDVM.start()
// Stop DVM when needed
process.on('SIGINT', () => {
searchDVM.stop()
})
```
The DVM Handler provides a robust foundation for building Nostr data services, with built-in support for common requirements like deduplication, response signing, and metadata management.
+18
View File
@@ -0,0 +1,18 @@
# @welshman/dvm
`@welshman/dvm` is a comprehensive package for building and interacting with Data Vending Machines (DVMs) in the Nostr ecosystem. It provides both server-side DVM implementation capabilities and client-side request handling.
## What is a DVM?
A Data Vending Machine (DVM) is a Nostr service that:
- Listens for specific kinds of events
- Processes these events according to defined rules
- Responds with new events containing processed data
- Optionally provides progress updates during processing
## Installation
```bash
npm install @welshman/dvm
```
+119
View File
@@ -0,0 +1,119 @@
# DVM Request
The DVM Request module provides utilities for making requests to Data Vending Machines (DVMs) and handling their responses.
It includes support for progress tracking and result handling.
## Core Types
### DVMRequestOptions
```typescript
type DVMRequestOptions = {
event: SignedEvent // The event to send to the DVM
relays: string[] // Relays to use
timeout?: number // Request timeout in milliseconds
autoClose?: boolean // Auto-close subscription after result
reportProgress?: boolean // Listen for progress events
}
```
### DVMEvent Enum
```typescript
enum DVMEvent {
Progress = "progress", // DVM progress updates (kind 7000)
Result = "result" // Final DVM result
}
```
## Making DVM Requests
### Basic Usage
```typescript
import { makeDvmRequest, DVMEvent } from '@welshman/dvm'
const request = makeDvmRequest({
event: signedEvent,
relays: ["wss://relay.example.com"],
timeout: 30000, // 30 seconds
})
// Handle results
request.emitter.on(DVMEvent.Result, (url, event) => {
console.log('Received result:', event)
})
// Handle progress updates
request.emitter.on(DVMEvent.Progress, (url, event) => {
console.log('Progress update:', event)
})
```
## Response Handling
### Result Events
```typescript
request.emitter.on(DVMEvent.Result, (url: string, event: TrustedEvent) => {
// Handle the DVM result
const result = JSON.parse(event.content)
// Process tags
const requestTag = event.tags.find(t => t[0] === 'request')
const expirationTag = event.tags.find(t => t[0] === 'expiration')
})
```
### Progress Updates
```typescript
request.emitter.on(DVMEvent.Progress, (url: string, event: TrustedEvent) => {
// Handle progress update (kind 7000)
const progress = JSON.parse(event.content)
console.log(`Progress: ${progress.percentage}%`)
})
```
## Complete Example
```typescript
import { makeDvmRequest, DVMEvent } from '@welshman/dvm'
import { createEvent, finalizeEvent } from '@welshman/util'
async function queryDVM() {
// Create the request event
const event = createEvent(5001, {
content: JSON.stringify({
query: "search terms"
})
})
// Sign the event
const signedEvent = finalizeEvent(event, privateKey)
// Make the request
const dvmRequest = makeDvmRequest({
event: signedEvent,
relays: ["wss://relay.example.com"],
timeout: 30000,
reportProgress: true
})
// Handle progress updates
dvmRequest.emitter.on(DVMEvent.Progress, (url, event) => {
console.log('Progress:', event.content)
})
// Return a promise that resolves with the result
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
dvmRequest.sub.close()
reject(new Error('DVM request timeout'))
}, 30000)
dvmRequest.emitter.on(DVMEvent.Result, (url, event) => {
clearTimeout(timeout)
resolve(event)
})
})
}
```
This module simplifies the process of making requests to DVMs while providing flexibility in handling responses and progress updates.
+154
View File
@@ -0,0 +1,154 @@
# @welshman/editor
`@welshman/editor` provides a comprehensive Nostr-ready text editor, built on top of [nostr-editor](https://github.com/cesardeazevedo/nostr-editor).
This package powers the editors of [Coracle](https://coracle.social) and [Flotilla](https://flotilla.social).
## Installation
```bash
npm install @welshman/editor
```
## WelshmanExtension
The `WelshmanExtension` is the main entry point of the package, providing a pre-configured collection of extensions optimized for Nostr content creation.
### Configuration
```typescript
interface WelshmanOptions {
// Required: Function to sign events
sign: (event: StampedEvent) => Promise<SignedEvent>
// Required: Handler for submit action
submit: () => void
// File upload configuration
defaultUploadUrl?: string // Default: "https://nostr.build"
defaultUploadType?: "nip96" | "blossom" // Default: "nip96"
// Extension configuration
extensions?: WelshmanExtensionOptions
}
```
### Included Extensions
The extension bundles and configures multiple TipTap and nostr-editor extensions:
#### Core TipTap Extensions
- Document
- Text
- Paragraph
- History
- CodeBlock
- CodeInline
- Dropcursor
- Gapcursor
- Placeholder
#### Nostr-specific Extensions
- NostrExtension (base)
- Bolt11Extension (Lightning invoices)
- FileUploadExtension
- ImageExtension
- LinkExtension
- NAddrExtension (Nostr addresses)
- NEventExtension (Nostr events)
- NProfileExtension (Nostr profiles)
- TagExtension
- VideoExtension
- NSecRejectExtension
#### Custom Extensions
- BreakOrSubmit (Enter key handling)
- WordCount
### Usage
```typescript
import { Editor } from '@tiptap/core'
import { WelshmanExtension } from '@welshman/editor'
const editor = new Editor({
extensions: [
WelshmanExtension.configure({
// Required: Event signing function
sign: async (event) => {
return signEvent(event)
},
// Required: Submit handler
submit: () => {
handleSubmit(editor.getText())
},
// Optional: Custom upload configuration
defaultUploadUrl: "https://nostr.build",
defaultUploadType: "nip96",
// Optional: Extension configuration
extensions: {
// Disable specific extensions
wordCount: false,
tag: false,
// Configure extensions
placeholder: {
config: {
placeholder: 'What\'s on your mind?'
}
},
// Extend existing extensions
codeBlock: {
extend: {
renderText: (props) => '```' + props.node.textContent + '```'
}
},
fileUpload: {
config: {
immediateUpload: true,
allowedMimeTypes: [
"image/jpeg",
"image/png",
"video/mp4"
]
}
}
}
})
]
})
```
### Extension Configuration
Each extension can be configured using the `WelshmanExtensionOptions`:
```typescript
type WelshmanExtensionOptions = {
[ExtensionName: string]: {
// Disable the extension
false |
// Configure the extension
{
// Extension-specific configuration
config?: Partial<ExtensionConfig>
// Extend the extension's functionality
extend?: Partial<ExtensionAPI>
}
}
}
```
### Custom Components
The extension includes Svelte components for rendering various Nostr entities in the editor:
- EditBolt11: Lightning invoice
- EditMedia: Image and video
- EditEvent: Nostr event
- EditMention: Nostr profile mention
+101
View File
@@ -0,0 +1,101 @@
# Feed Compiler
The `FeedCompiler` class is responsible for transforming feed definitions into executable relay requests. It handles the complex task of converting various feed types into optimized filters and relay selections.
## Overview
```typescript
class FeedCompiler {
constructor(readonly options: FeedOptions)
canCompile(feed: Feed): boolean
compile(feed: Feed): Promise<RequestItem[]>
}
```
## Feed Compilation Process
The compiler transforms feed definitions into `RequestItem[]`, where each item contains:
```typescript
type RequestItem = {
relays?: string[] // Specific relays to query
filters?: Filter[] // Nostr filters to apply
}
```
## Examples
### Basic Feed Compilation
```typescript
const compiler = new FeedCompiler(options)
// Simple author feed
const feed = [FeedType.Author, "pubkey1", "pubkey2"]
const requests = await compiler.compile(feed)
// => [{ filters: [{ authors: ["pubkey1", "pubkey2"] }] }]
```
### Complex Feed Compilation
```typescript
// Complex feed with multiple operations
const feed = [
FeedType.Intersection,
[FeedType.Kind, 1],
[
FeedType.Union,
[FeedType.Scope, Scope.Follows],
[FeedType.List, { addresses: ["trending"] }]
]
]
const requests = await compiler.compile(feed)
// Compiles to optimized filters for relay queries
```
### DVM Integration
```typescript
const feed = [
FeedType.DVM,
{
kind: 5300,
mappings: [
["p", [FeedType.Author]],
["t", [FeedType.Tag, "#t"]]
]
}
]
const requests = await compiler.compile(feed)
// Queries DVM and compiles resulting tags into feeds
```
## Implementation Notes
### Optimization Strategies
1. **Filter Merging**: Similar filters are combined when possible
```typescript
// Before: [{ authors: ["a"] }, { authors: ["b"] }]
// After: [{ authors: ["a", "b"] }]
```
2. **Relay Grouping**: Requests are grouped by relay where possible
```typescript
// Filters are organized by relay to minimize connections
filtersByRelay: Map<string, Filter[]>
```
3. **Deduplication**: Duplicate values are removed using `uniq`
```typescript
uniq(scopes.flatMap(this.options.getPubkeysForScope))
```
### Error Handling
The compiler includes various safety checks:
```typescript
canCompile(feed: Feed): boolean {
// Checks if feed type is supported
// Recursively checks sub-feeds
}
```
+132
View File
@@ -0,0 +1,132 @@
# Feed Controller
The `FeedController` class is responsible for managing and executing feed queries in a performant and organized manner. It handles the compilation of feed definitions into executable queries and manages the loading of events based on those queries.
## Usage
```typescript
import { FeedController } from '@welshman/feeds'
const controller = new FeedController({
feed: yourFeedDefinition,
request: async ({ filters, relays, onEvent }) => {
// Your implementation for fetching events
},
requestDVM: async ({ kind, tags, relays, onEvent }) => {
// Your implementation for DVM requests
},
getPubkeysForScope: (scope) => {
// Return pubkeys for given scope
return ['pubkey1', 'pubkey2']
},
getPubkeysForWOTRange: (min, max) => {
// Return pubkeys within WOT range
return ['pubkey1', 'pubkey2']
},
onEvent: (event) => {
// Handle received events
},
onExhausted: () => {
// Called when no more events are available
},
useWindowing: true, // Optional: enable time-window based loading
})
```
## API Reference
### Constructor
```typescript
constructor(options: FeedOptions)
```
Creates a new feed controller with the given options:
- `feed`: The feed definition to execute
- `request`: Function to fetch events from relays
- `requestDVM`: Function to fetch events from DVMs
- `getPubkeysForScope`: Function to get pubkeys for a scope
- `getPubkeysForWOTRange`: Function to get pubkeys within a WOT range
- `onEvent`: Optional callback for received events
- `onExhausted`: Optional callback when feed is exhausted
- `useWindowing`: Optional flag to enable time-window based loading
### Methods
#### `load(limit: number): Promise<void>`
```typescript
const controller = new FeedController(options)
await controller.load(10) // Load 10 events
```
Loads events from the feed up to the specified limit.
#### `getLoader(): Promise<(limit: number) => Promise<void>>`
Gets the loader function for this feed. Usually called internally by `load()`.
#### `getRequestItems(): Promise<RequestItem[] | undefined>`
Gets the compiled request items for this feed. Usually called internally.
## Advanced Features
### Time Windowing
When `useWindowing` is enabled, the controller uses a time-based window approach to load events:
```typescript
const controller = new FeedController({
...options,
useWindowing: true
})
```
This is useful for:
- Loading recent events first
- Handling large datasets efficiently
- Progressive loading of historical data
## Examples
### Basic Loading
```typescript
const controller = new FeedController(options)
await controller.load(20) // Load 20 events
```
### Custom Loading Strategy
```typescript
const controller = new FeedController({
...options,
useWindowing: true,
onEvent: (event) => {
console.log('Received event:', event.id)
},
onExhausted: () => {
console.log('No more events available')
}
})
// Load events in batches
async function loadAllEvents() {
while (!exhausted) {
await controller.load(10)
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
```
### Error Handling
```typescript
try {
await controller.load(10)
} catch (error) {
if (error.message.includes('relay')) {
// Handle relay errors
} else {
// Handle other errors
}
}
```
+341
View File
@@ -0,0 +1,341 @@
# Feed Types and Core Definitions
This module defines the core types and structures used to build Nostr feeds.
It provides a type-safe way to define complex feed compositions using various filtering mechanisms and set operations.
## Feed Types
```typescript
enum FeedType {
Address = "address", // Filter by event addresses
Author = "author", // Filter by author pubkeys
CreatedAt = "created_at", // Filter by timestamp
DVM = "dvm", // Data Vending Machine based feed
Difference = "difference", // Set difference operation
ID = "id", // Filter by event IDs
Intersection = "intersection", // Set intersection operation
Global = "global", // Global feed (no filters)
Kind = "kind", // Filter by event kinds
List = "list", // List-based feed
Label = "label", // Label-based feed
WOT = "wot", // Web of Trust based feed
Relay = "relay", // Relay-specific feed
Scope = "scope", // Scoped feed (followers, network)
Search = "search", // Search-based feed
Tag = "tag", // Filter by specific tags
Union = "union" // Set union operation
}
```
## Scope Types
```typescript
enum Scope {
Followers = "followers", // People who follow the user
Follows = "follows", // People the user follows
Network = "network", // Extended network
Self = "self" // The signed in user
}
```
## Feed Definitions
Each feed type has its own structure:
### Basic Filter Feeds
```typescript
type AddressFeed = [type: FeedType.Address, ...addresses: string[]]
type AuthorFeed = [type: FeedType.Author, ...pubkeys: string[]]
type IDFeed = [type: FeedType.ID, ...ids: string[]]
type KindFeed = [type: FeedType.Kind, ...kinds: number[]]
type TagFeed = [type: FeedType.Tag, key: string, ...values: string[]]
```
### Time-based Feeds
```typescript
type CreatedAtItem = {
since?: number
until?: number
relative?: string[] // For relative time references
}
type CreatedAtFeed = [type: FeedType.CreatedAt, ...items: CreatedAtItem[]]
```
### Advanced Filter Feeds
```typescript
// DVM-based feed
type DVMItem = {
kind: number
tags?: string[][]
relays?: string[]
mappings?: TagFeedMapping[]
}
type DVMFeed = [type: FeedType.DVM, ...items: DVMItem[]]
// List-based feed
type ListItem = {
addresses: string[]
mappings?: TagFeedMapping[]
}
type ListFeed = [type: FeedType.List, ...items: ListItem[]]
// Label-based feed
type LabelItem = {
relays?: string[]
authors?: string[]
[key: `#${string}`]: string[]
mappings?: TagFeedMapping[]
}
type LabelFeed = [type: FeedType.Label, ...items: LabelItem[]]
// Web of Trust feed
type WOTItem = {
min?: number
max?: number
}
type WOTFeed = [type: FeedType.WOT, ...items: WOTItem[]]
```
## Tag Feed Mapping
`TagFeedMapping` is a mechanism to convert event tags into feed definitions. It's particularly useful when working with DVMs, Lists, and Labels where you want to interpret tags in a specific way.
```typescript
type TagFeedMapping = [string, Feed]
```
### Usage
```typescript
// Example mappings
const mappings: TagFeedMapping[] = [
// Convert 'p' tags into author feeds
["p", [FeedType.Author]],
// Convert 't' tags into hashtag filters
["t", [FeedType.Tag, "#t"]],
// Convert 'e' tags into event ID feeds
["e", [FeedType.ID]],
// Convert 'r' tags into relay feeds
["r", [FeedType.Relay]]
]
// Using mappings in a DVM feed
const dvmFeed: Feed = [
FeedType.DVM,
{
kind: 5300,
mappings: mappings
}
]
// Using mappings in a List feed
const listFeed: Feed = [
FeedType.List,
{
addresses: ["list_id"],
mappings: mappings
}
]
```
### Default Mappings
The system comes with default mappings for common tags:
```typescript
const defaultTagFeedMappings: TagFeedMapping[] = [
["a", [FeedType.Address]], // Address references
["e", [FeedType.ID]], // Event references
["p", [FeedType.Author]], // Person/Pubkey references
["r", [FeedType.Relay]], // Relay references
["t", [FeedType.Tag, "#t"]], // Hashtags
]
```
## Set Operation Feeds
### Union Feed
A Union feed combines multiple feeds with an OR operation. Events matching any of the constituent feeds will be included.
```typescript
type UnionFeed = [type: FeedType.Union, ...feeds: Feed[]]
// Example: Events from either Alice OR Bob
const unionFeed: UnionFeed = [
FeedType.Union,
[FeedType.Author, "alice_pubkey"],
[FeedType.Author, "bob_pubkey"]
]
// Example: Events from a list OR matching a search term
const complexUnion: UnionFeed = [
FeedType.Union,
[FeedType.List, { addresses: ["trending_list"] }],
[FeedType.Search, "bitcoin"]
]
```
### Intersection Feed
An Intersection feed combines multiple feeds with an AND operation. Only events that match all constituent feeds will be included.
```typescript
type IntersectionFeed = [type: FeedType.Intersection, ...feeds: Feed[]]
// Example: Text notes (kind 1) from trusted authors
const intersectionFeed: IntersectionFeed = [
FeedType.Intersection,
[FeedType.Kind, 1],
[FeedType.WOT, { min: 0.5 }]
]
// Example: Recent posts from followed users
const timeAndScope: IntersectionFeed = [
FeedType.Intersection,
[FeedType.CreatedAt, { since: Date.now() - 86400000 }], // Last 24h
[FeedType.Scope, Scope.Follows]
]
```
### Difference Feed
A Difference feed excludes events from the second feed from the first feed (NOT operation).
```typescript
type DifferenceFeed = [type: FeedType.Difference, ...feeds: Feed[]]
// Example: Posts from everyone except blocked users
const differenceFeed: DifferenceFeed = [
FeedType.Difference,
[FeedType.Global], // All events
[FeedType.List, { addresses: ["blocked_users"] }] // Except from blocked users
]
// Example: Posts from follows except reposts
const noReposts: DifferenceFeed = [
FeedType.Difference,
[FeedType.Scope, Scope.Follows],
[FeedType.Kind, 6] // Kind 6 is repost
]
```
### Complex Combinations
You can nest set operations to create sophisticated feed definitions:
```typescript
// Posts that are either:
// - from trusted authors AND about bitcoin
// - OR from a curated list
const complexFeed: Feed = [
FeedType.Union,
[
FeedType.Intersection,
[FeedType.WOT, { min: 0.7 }],
[FeedType.Search, "bitcoin"]
],
[FeedType.List, { addresses: ["curated_content"] }]
]
// Posts that are:
// - from follows
// - AND (from the last 24h OR highly rated by DVMs)
// - AND NOT marked as sensitive content
const advancedFeed: Feed = [
FeedType.Difference,
[
FeedType.Intersection,
[FeedType.Scope, Scope.Follows],
[
FeedType.Union,
[FeedType.CreatedAt, { since: Date.now() - 86400000 }],
[FeedType.DVM, { kind: 5300, pubkey: "rating_dvm" }]
]
],
[FeedType.Label, { authors: ["content_warning_dvm"] }]
]
```
## Feed Controller Options
The `FeedOptions` interface defines the configuration required to execute a feed:
```typescript
interface FeedOptions {
// The feed definition to execute
feed: Feed
// Function to request events from relays
request: (opts: RequestOpts) => Promise<void>
// Function to request events from DVMs
requestDVM: (opts: DVMOpts) => Promise<void>
// Function to get pubkeys for a given scope
getPubkeysForScope: (scope: Scope) => string[]
// Function to get pubkeys within a WOT range
getPubkeysForWOTRange: (minWOT: number, maxWOT: number) => string[]
// Event handler
onEvent?: (event: TrustedEvent) => void
// Called when feed is exhausted
onExhausted?: () => void
// Enable time-window based loading
useWindowing?: boolean
// Optional abort controller
abortController?: AbortController
}
```
## Examples
### Simple Author Feed
```typescript
const authorFeed: Feed = [FeedType.Author, "pubkey1", "pubkey2"]
```
### Time-filtered Feed
```typescript
const recentFeed: Feed = [
FeedType.CreatedAt,
{
since: Date.now() - 24 * 60 * 60 * 1000, // Last 24 hours
relative: ["since"]
}
]
```
### Complex Feed Composition
```typescript
const complexFeed: Feed = [
FeedType.Intersection,
[FeedType.Kind, 1], // Text notes
[FeedType.WOT, { min: 0.5 }], // Trusted authors
[
FeedType.Union,
[FeedType.Scope, Scope.Follows], // From follows
[FeedType.List, { addresses: ["list_id"] }] // Or from list
]
]
```
### DVM Feed with Mappings
```typescript
const dvmFeed: Feed = [
FeedType.DVM,
{
kind: 5300,
mappings: [
["p", [FeedType.Author]], // Map 'p' tags to authors
["t", [FeedType.Tag, "#t"]] // Map 't' tags to hashtags
]
}
]
```
This core module provides the foundation for building complex, type-safe feed definitions that can be executed by the [feed controller](/feeds/controller).
+19
View File
@@ -0,0 +1,19 @@
# @welshman/feeds
A powerful package for building and executing dynamic Nostr feeds.
It provides a declarative way to define complex feed compositions using set operations (union, intersection, difference) and various filtering mechanisms.
## What's Included
- **Feed Core** - Declarative feed definition with composable operations
- **Feed Compiler** - Transforms feed definitions into optimized relay requests
- **Feed Controller** - Manages feed execution and event loading
- **Feed Utils** - Helper functions for creating and manipulating feeds
- **Feed Types** - Supports authors, kinds, tags, DVMs, lists, WOT, and more
## Installation
```bash
npm install @welshman/feeds
```
+175
View File
@@ -0,0 +1,175 @@
# Feed Utilities
The utils module provides helper functions for creating, type-checking, and manipulating feed definitions. It includes factory functions, type guards, feed transformation utilities, and feed traversal tools.
## Feed Factory Functions
Create strongly-typed feed definitions:
```typescript
// Basic Feeds
const authors = makeAuthorFeed("pubkey1", "pubkey2")
const kinds = makeKindFeed(1, 6)
const search = makeSearchFeed("bitcoin", "nostr")
const global = makeGlobalFeed()
// Time-based Feeds
const recent = makeCreatedAtFeed({
since: Date.now() - 86400000,
relative: ["since"]
})
// Advanced Feeds
const dvm = makeDVMFeed({
kind: 5300,
mappings: [["p", [FeedType.Author]]]
})
const list = makeListFeed({
addresses: ["list_id"],
mappings: [["t", [FeedType.Tag, "#t"]]]
})
// Set Operations
const union = makeUnionFeed(authors, kinds)
const intersection = makeIntersectionFeed(authors, recent)
const difference = makeDifferenceFeed(global, authors)
```
## Type Guards
Check feed types safely:
```typescript
const feed: Feed = makeDVMFeed({ kind: 5300 })
if (isDVMFeed(feed)) {
// feed is now typed as DVMFeed
const [kind] = feed.slice(1)
}
if (hasSubFeeds(feed)) {
// feed is now typed as UnionFeed | IntersectionFeed | DifferenceFeed
const subFeeds = getFeedArgs(feed)
}
```
## Feed Transformations
### Tag to Feed Conversion
```typescript
// Default tag mappings
const defaultTagFeedMappings: TagFeedMapping[] = [
["a", [FeedType.Address]], // address tags
["e", [FeedType.ID]], // event references
["p", [FeedType.Author]], // people/pubkeys
["r", [FeedType.Relay]], // relay URLs
["t", [FeedType.Tag, "#t"]], // hashtags
]
// Convert event tags to feeds
const tags = [["p", "pubkey1"], ["t", "bitcoin"]]
const feeds = feedsFromTags(tags)
// => [[FeedType.Author, "pubkey1"], [FeedType.Tag, "#t", "bitcoin"]]
// Convert tags to a single intersection feed
const feed = feedFromTags(tags)
// => [FeedType.Intersection, [FeedType.Author, "pubkey1"], [FeedType.Tag, "#t", "bitcoin"]]
```
### Filter to Feed Conversion
```typescript
// Convert a single filter to feeds
const filter = {
kinds: [1],
authors: ["pubkey1"],
"#t": ["bitcoin"],
since: 1234567890
}
const feeds = feedsFromFilter(filter)
// => [
// [FeedType.CreatedAt, { since: 1234567890 }],
// [FeedType.Kind, 1],
// [FeedType.Author, "pubkey1"],
// [FeedType.Tag, "#t", "bitcoin"]
// ]
// Convert a filter to an intersection feed
const feed = feedFromFilter(filter)
// Convert multiple filters to a union feed
const feeds = feedFromFilters([filter1, filter2])
```
## Feed Traversal
Walk through a feed tree and visit each node:
```typescript
const feed = makeIntersectionFeed(
makeAuthorFeed("pubkey1"),
makeUnionFeed(
makeKindFeed(1),
makeTagFeed("#t", "bitcoin")
)
)
walkFeed(feed, (node) => {
console.log(`Visiting feed of type: ${node[0]}`)
})
```
## Type Extraction
Get typed arguments from feeds:
```typescript
function getFeedArgs(feed: IntersectionFeed): Feed[]
function getFeedArgs(feed: AuthorFeed): string[]
function getFeedArgs(feed: CreatedAtFeed): CreatedAtItem[]
function getFeedArgs(feed: WOTFeed): WOTItem[]
// ... and so on for each feed type
const feed = makeAuthorFeed("pubkey1", "pubkey2")
const pubkeys = getFeedArgs(feed) // => ["pubkey1", "pubkey2"]
```
## Best Practices
1. Use factory functions instead of raw arrays:
```typescript
// Good
const feed = makeAuthorFeed("pubkey1")
// Avoid
const feed = [FeedType.Author, "pubkey1"]
```
2. Use type guards for safe type narrowing:
```typescript
if (isAuthorFeed(feed)) {
const pubkeys = getFeedArgs(feed) // Properly typed
}
```
3. Use feed transformations for dynamic feed creation:
```typescript
// Convert event tags to feeds
const feeds = feedsFromTags(event.tags)
// Convert filters to feeds
const feed = feedFromFilter(filter)
```
4. Use feed traversal for analysis or transformation:
```typescript
const kinds = new Set<number>()
walkFeed(feed, (node) => {
if (isKindFeed(node)) {
getFeedArgs(node).forEach(k => kinds.add(k))
}
})
```
+57
View File
@@ -0,0 +1,57 @@
# Getting Started
Welshman is modular - install only what you need:
```bash
# Core nostr utilities (events, filters, tags)
npm i @welshman/util
# Networking and relay management
npm i @welshman/net
# Content parsing and rendering
npm i @welshman/content
# Event signing and encryption
npm i @welshman/signer
# Dynamic feed compilation
npm i @welshman/feeds
```
For Svelte applications, additional packages provide reactive state management:
```bash
# Svelte stores and state management
npm i @welshman/store
# Full application framework (requires Svelte)
npm i @welshman/app
# Rich text editor component (requires Svelte)
npm i @welshman/editor
```
Choose packages based on your needs:
- Building a framework-agnostic client? Start with:
```bash
npm i @welshman/util @welshman/net @welshman/signer @welshman/feeds
```
- Building a Svelte client? Add state management:
```bash
npm i @welshman/store @welshman/app
```
- Need content features? Include:
```bash
npm i @welshman/content
```
- Want the full Svelte stack used by Coracle.social and Flotilla?
```bash
npm i @welshman/util @welshman/net @welshman/signer @welshman/feeds @welshman/store @welshman/app @welshman/content @welshman/editor
```
Each package is independent but integrates seamlessly. The core packages (`util`, `net`, `signer`, `feeds`, `content`) work with any framework, while `store`, `app` and `editor` are built for Svelte applications.
+45
View File
@@ -0,0 +1,45 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "Welshman"
text: Nostr building blocks for the web
tagline: A series of independent libraries for managing every aspect of your Nostr application.
actions:
- theme: brand
text: What is Welshman?
link: /what-is-welshman
- theme: alt
text: Quickstart
link: /getting-started
- theme: alt
text: Github
link: https://github.com/coracle-social/welshman
features:
- title: "@welshman/content"
details: Parser and renderer for nostr note with customizable formatting options.
link: "/content"
- title: "@welshman/dvm"
details: Tools for building and interacting with nostr Data Vending Machines (DVMs)
link: "/dvm"
- title: "@welshman/editor"
details: Rich text editor component with support for mentions and embeds.
link: "/editor"
- title: "@welshman/feeds"
details: Dynamic feed compiler and loader with filtering and composition.
link: "/feeds"
- title: "@welshman/util"
details: Core Nostr utilities for events, filters, and data structures.
link: "/util"
- title: "@welshman/net"
details: Networking layer for Nostr with relay connection management and message status handling.
link: "/net"
- title: "@welshman/signer"
details: Implementations of various nostr signing methods (NIP-01, NIP-07, NIP-46, NIP-55).
link: "/signer"
- title: "@welshman/store"
details: Svelte store utilities optimized for nostr state management.
link: "/store"
---
+141
View File
@@ -0,0 +1,141 @@
# Deferred Promises
The Deferred module provides utilities for creating promises with exposed resolve/reject functions and typed error handling. This is particularly useful for managing asynchronous operations where you need external control over promise resolution.
## Types
### CustomPromise
```typescript
type CustomPromise<T, E> = Promise<T> & {
__errorType: E
}
```
A Promise type with strongly typed error information.
### Deferred
```typescript
type Deferred<T, E = T> = CustomPromise<T, E> & {
resolve: (arg: T) => void
reject: (arg: E) => void
}
```
A Promise with exposed resolve/reject functions and typed error handling.
## Core Functions
### makePromise
```typescript
function makePromise<T, E>(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason: E) => void
) => void
): CustomPromise<T, E>
```
Creates a Promise with strongly typed error information.
### defer
```typescript
function defer<T, E = T>(): Deferred<T, E>
```
Creates a Deferred promise with resolve/reject methods exposed.
## Usage Examples
### Basic Usage
```typescript
// Create a deferred promise
const deferred = defer<string, Error>()
// Resolve later
setTimeout(() => {
deferred.resolve('Success!')
}, 1000)
// Use like a regular promise
await deferred // => 'Success!'
```
### With Typed Errors
```typescript
interface ApiError {
code: number
message: string
}
const request = defer<Response, ApiError>()
try {
const response = await fetch('/api')
request.resolve(response)
} catch (error) {
request.reject({
code: 500,
message: error.message
})
}
```
### External Promise Control
```typescript
class AsyncOperation {
private ready = defer<boolean>()
initialize() {
// Setup async operation
this.ready.resolve(true)
}
async waitUntilReady() {
return this.ready
}
}
```
### With Timeout
```typescript
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = defer<T>()
setTimeout(() => {
timeout.reject(new Error('Timeout'))
}, ms)
return Promise.race([promise, timeout])
}
// Usage
try {
const result = await withTimeout(slowOperation(), 5000)
} catch (error) {
console.log('Operation timed out')
}
```
### Event to Promise
```typescript
function eventToPromise<T>(
emitter: EventEmitter,
successEvent: string,
errorEvent: string
): Deferred<T, Error> {
const deferred = defer<T, Error>()
emitter.once(successEvent, (data: T) => {
deferred.resolve(data)
})
emitter.once(errorEvent, (error: Error) => {
deferred.reject(error)
})
return deferred
}
```
+17
View File
@@ -0,0 +1,17 @@
# @welshman/lib
A lightweight TypeScript utility library with zero dependencies, providing essential tools for modern JavaScript development.
## What's Included
- **Deferred Promises** - Create promises with exposed resolve/reject methods
- **LRU Cache** - Efficient caching with automatic eviction policies
- **Utility Functions** - Helpers for arrays, objects, strings, and more
- **Worker Queue** - Process tasks asynchronously with batching and throttling
## Installation
```bash
npm install @welshman/lib
```
+109
View File
@@ -0,0 +1,109 @@
# LRU Cache
The LRU (Least Recently Used) Cache implementation provides efficient caching with automatic eviction of least recently used items when the cache reaches its maximum size.
## Basic Usage
```typescript
// Create cache with max size
const cache = new LRUCache<string, number>(3)
// Add items
cache.set('a', 1)
cache.set('b', 2)
cache.set('c', 3)
// Access items
cache.get('a') // => 1
// Check if key exists
cache.has('b') // => true
// Adding beyond max size evicts least recently used
cache.set('d', 4) // Evicts oldest item
```
## API Reference
### Constructor
```typescript
constructor(maxSize: number = Infinity)
```
Creates a new LRU cache with specified maximum size.
### Methods
#### set(key: T, value: U)
```typescript
set(key: T, value: U): void
```
Adds or updates an item in the cache. If cache is at maximum size, evicts least recently used item.
#### get(key: T)
```typescript
get(key: T): U | undefined
```
Retrieves item from cache. Also marks item as recently used.
#### has(key: T)
```typescript
has(key: T): boolean
```
Checks if key exists in cache without affecting usage tracking.
## Cache Decorator
The package also provides a convenient decorator function for creating memoized functions with LRU caching:
```typescript
function cached<T, V, Args extends any[]>({
maxSize,
getKey,
getValue,
}: {
maxSize: number
getKey: (args: Args) => T
getValue: (args: Args) => V
}): (...args: Args) => V
```
### Usage Example
```typescript
// Create cached function
const getUser = cached({
maxSize: 1000,
getKey: (args) => args[0], // Use first argument as cache key
getValue: async (args) => {
const [id] = args
return await fetchUser(id)
}
})
// Use cached function
const user1 = await getUser(123)
const user2 = await getUser(123) // Returns cached result
```
### Simple Cache Helper
For basic caching needs, there's also a simplified cache creator:
```typescript
function simpleCache<V, Args extends any[]>(
getValue: (args: Args) => V
) {
return cached({
maxSize: 100000,
getKey: xs => xs.join(':'),
getValue
})
}
// Usage
const cachedFn = simpleCache(async (id: string) => {
return await expensiveOperation(id)
})
```
+256
View File
@@ -0,0 +1,256 @@
# Utility Functions
The `Tools.ts` module provides a comprehensive collection of utility functions for common programming tasks. It includes functions for array manipulation, object handling, type checking, math operations, and more.
## Types
```typescript
type Nil = null | undefined
type Maybe<T> = T | undefined
type Obj<T = any> = Record<string, T>
```
## Categories
### Type Checking & Basic Operations
```typescript
// Check if value is null or undefined
isNil(x: any): boolean
// Execute function if value exists
ifLet<T>(x: T | undefined, f: (x: T) => void)
// Return value unchanged
identity<T>(x: T): T
// Create function that always returns same value
always<T>(x: T): () => T
// Logical NOT
not(x: any): boolean
// Create complement of a predicate function
complement<T extends unknown[]>(f: (...args: T) => any): (...args: T) => boolean
```
### Array Operations
```typescript
// Get first element
first<T>(xs: T[]): T | undefined
// Get first element of first array
ffirst<T>(xs: T[][]): T | undefined
// Get last element
last<T>(xs: T[]): T | undefined
// Drop first n elements
drop<T>(n: number, xs: T[]): T[]
// Take first n elements
take<T>(n: number, xs: T[]): T[]
// Remove duplicates
uniq<T>(xs: T[]): T[]
// Remove duplicates by key function
uniqBy<T>(f: (x: T) => any, xs: T[]): T[]
// Create array of n items using generator function
initArray<T>(n: number, f: () => T): T[]
// Split array into chunks
chunk<T>(chunkLength: number, xs: T[]): T[][]
// Split array into n chunks
chunks<T>(n: number, xs: T[]): T[][]
```
### Object Operations
```typescript
// Create object excluding specified keys
omit<T extends Obj>(ks: string[], x: T): T
// Create object excluding entries with specified values
omitVals<T extends Obj>(xs: any[], x: T): T
// Create object with only specified keys
pick<T extends Obj>(ks: string[], x: T): T
// Transform object keys
mapKeys<T extends Obj>(f: (v: string) => string, x: T): T
// Transform object values
mapVals<V, U>(f: (v: V) => U, x: Record<string, V>): Record<string, U>
// Merge objects (left priority)
mergeLeft<T extends Obj>(a: T, b: T): T
// Merge objects (right priority)
mergeRight<T extends Obj>(a: T, b: T): T
// Deep merge objects
deepMergeLeft(a: Obj, b: Obj): Obj
deepMergeRight(a: Obj, b: Obj): Obj
```
### Number Operations
```typescript
// Convert Maybe<number> to number
num(x: Maybe<number>): number
// Basic arithmetic with Maybe<number>
add(x: Maybe<number>, y: Maybe<number>): number
sub(x: Maybe<number>, y: Maybe<number>): number
mul(x: Maybe<number>, y: Maybe<number>): number
div(x: Maybe<number>, y: number): number
// Increment/Decrement
inc(x: Maybe<number>): number
dec(x: Maybe<number>): number
// Comparisons
lt(x: Maybe<number>, y: Maybe<number>): boolean
lte(x: Maybe<number>, y: Maybe<number>): boolean
gt(x: Maybe<number>, y: Maybe<number>): boolean
gte(x: Maybe<number>, y: Maybe<number>): boolean
// Array number operations
max(xs: Maybe<number>[]): number
min(xs: Maybe<number>[]): number
sum(xs: Maybe<number>[]): number
avg(xs: Maybe<number>[]): number
```
### String Operations
```typescript
// Truncate string with ellipsis
ellipsize(s: string, l: number, suffix = "..."): string
// URL operations
stripProtocol(url: string): string
displayUrl(url: string): string
displayDomain(url: string): string
// Bech32 encoding/decoding
hexToBech32(prefix: string, hex: string): string
bech32ToHex(b32: string): string
```
### Collection Operations
```typescript
// Create union of arrays
union<T>(a: T[], b: T[]): T[]
// Get intersection of arrays
intersection<T>(a: T[], b: T[]): T[]
// Get difference of arrays
difference<T>(a: T[], b: T[]): T[]
// Remove element from array
remove<T>(a: T, xs: T[]): T[]
// Filter array by another array
without<T>(a: T[], b: T[]): T[]
// Toggle element in array
toggle<T>(x: T, xs: T[]): T[]
// Group array by key function
groupBy<T, K>(f: (x: T) => K, xs: T[]): Map<K, T[]>
// Create map from array
indexBy<T, K>(f: (x: T) => K, xs: T[]): Map<K, T>
```
### Time Constants
```typescript
const MINUTE = 60
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR
const WEEK = 7 * DAY
const MONTH = 30 * DAY
const QUARTER = 90 * DAY
const YEAR = 365 * DAY
// Get current timestamp in seconds
now(): number
// Get timestamp from ago in seconds
ago(unit: number, count = 1): number
// Convert seconds to milliseconds
ms(seconds: number): number
```
### Function Utilities
```typescript
// Create function that executes once
once(f: (...args: any) => void): (...args: any) => void
// Memoize function results
memoize<T>(f: (...args: any[]) => T): (...args: any[]) => T
// Create throttled function
throttle<F extends (...args: any[]) => any>(
ms: number,
f: F
): F
// Create batching function
batch<T>(
t: number,
f: (xs: T[]) => void
): (x: T) => void
```
### Network Utilities
```typescript
// Fetch JSON with options
fetchJson(url: string, opts?: FetchOpts): Promise<any>
// Post JSON data
postJson<T>(url: string, data: T, opts?: FetchOpts): Promise<any>
// Upload file
uploadFile(url: string, file: File): Promise<any>
```
## Usage Examples
```typescript
// Array operations
const nums = [1, 2, 2, 3, 3, 3]
uniq(nums) // => [1, 2, 3]
// Object operations
const obj = {a: 1, b: 2, c: 3}
omit(['a', 'b'], obj) // => {c: 3}
// Number operations
add(5, undefined) // => 5
inc(undefined) // => 1
// Time operations
ago(DAY, 2) // => timestamp from 2 days ago
// URL operations
displayUrl('https://www.example.com/') // => 'example.com'
// Collection operations
const users = [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}]
indexBy(u => u.id, users) // => Map(1 => {id: 1, name: 'Alice'}, ...)
// Function utilities
const throttledFn = throttle(1000, () => console.log('throttled'))
```
+117
View File
@@ -0,0 +1,117 @@
# Worker
The Worker class provides a robust queue processing system with batched operations, throttling, and message routing capabilities. It's designed to handle asynchronous operations efficiently while maintaining control over processing rates and resource usage.
## Overview
```typescript
class Worker<T> {
constructor(readonly opts: WorkerOpts<T> = {})
}
```
The Worker class accepts messages of type `T` and processes them according to configured options and handlers.
## Configuration
```typescript
type WorkerOpts<T> = {
// Function to determine routing key for messages
getKey?: (x: T) => any
// Function to check if message should be deferred
shouldDefer?: (x: T) => boolean
// Maximum messages to process in one batch
chunkSize?: number // default: 50
// Milliseconds between processing batches
delay?: number // default: 50
}
```
## Basic Usage
```typescript
// Create worker for processing messages
const worker = new Worker<Message>({
chunkSize: 10,
delay: 100,
getKey: msg => msg.type
})
// Add message handlers
worker.addHandler('email', async (msg) => {
await sendEmail(msg)
})
worker.addHandler('notification', async (msg) => {
await sendNotification(msg)
})
// Add messages to queue
worker.push({
type: 'email',
content: 'Hello'
})
```
## Features
### Message Routing
Messages can be routed to specific handlers based on a key:
```typescript
const worker = new Worker<Task>({
getKey: task => task.priority
})
// Handle high priority tasks
worker.addHandler('high', async (task) => {
await processUrgent(task)
})
// Handle normal priority tasks
worker.addHandler('normal', async (task) => {
await processNormal(task)
})
```
### Global Handlers
Handle all messages regardless of routing key:
```typescript
worker.addGlobalHandler(async (message) => {
console.log('Processing:', message)
})
```
### Message Deferral
Defer processing of messages that aren't ready:
```typescript
const worker = new Worker<Task>({
shouldDefer: (task) => !task.isReady(),
delay: 1000
})
worker.push(task) // Will retry until task.isReady()
```
### Flow Control
Control message processing:
```typescript
// Pause processing
worker.pause()
// Resume processing
worker.resume()
// Clear queue
worker.clear()
```
+78
View File
@@ -0,0 +1,78 @@
# Connection
The `Connection` class is the core building block for relay communication in `@welshman/net`. It manages the complete lifecycle of a relay connection, including socket handling, message queuing, authentication, and statistics tracking.
## Overview
A Connection handles:
- WebSocket lifecycle
- Message queuing and throttling
- Connection state tracking
- Relay authentication
- Connection statistics
## Basic Usage
```typescript
import {Connection} from '@welshman/net'
// Create connection
const connection = new Connection("wss://relay.example.com")
// Listen for events
connection.on('event', (conn, subId, event) => {
console.log(`Got event from ${conn.url}`)
})
// Send a subscription
connection.send(["REQ", "my-sub", {kinds: [1], limit: 10}])
// Clean up when done
connection.cleanup()
```
## Handling Authentication
The `connection.open()` promise resolves when the WebSocket connection is fully established and ready for communication.
However, it's important to understand the authentication flow:
```typescript
import {Connection} from '@welshman/net'
const connection = new Connection("wss://relay.example.com")
// Basic open
await connection.open()
// Promise resolves when WebSocket is connected
// BUT might not be auth-ready yet!
// Complete open with auth handling
const openRelay = async (url: string) => {
const connection = new Connection(url)
// Open socket
await connection.open()
// Check if relay requires auth
if (connection.auth.status === 'requested') {
try {
// Handle auth challenge
await connection.auth.attempt(3000) // 3s timeout
} catch (e) {
console.error('Auth failed:', e)
return null
}
}
// NOW connection is fully ready
return connection
}
```
The key states after `open()` resolves:
- Socket is connected
- Messages can be queued
- BUT relay might request authentication
- AND authentication might fail
Always check `connection.auth.status` if you need to ensure the connection is fully authenticated before use.
+63
View File
@@ -0,0 +1,63 @@
# Context
The Context system is the backbone of `@welshman/net`, providing global configuration and shared services that are essential for the package's operation. It defines how events are handled, validated, and routed throughout the application.
## Overview
- Global connection pool (`ctx.net.pool`)
- Event validation (`ctx.net.isValid`)
- Event handling (`ctx.net.onEvent`)
- Deletion tracking (`ctx.net.isDeleted`)
- Event signing (`ctx.net.signEvent`)
- Subscription optimization (`ctx.net.optimizeSubscriptions`)
## Basic Usage
```typescript
import {ctx, setContext} from '@welshman/lib'
import {
getDefaultNetContext,
Pool,
hasValidSignature
} from '@welshman/net'
// Setup networking context
setContext({
net: getDefaultNetContext({
// Use shared pool
pool: new Pool(),
// Track events
onEvent: (url, event) => {
tracker.track(event.id, url)
repository.publish(event)
},
// Validate based on source
isValid: (url, event) => {
// Trust local relay
if (url === LOCAL_RELAY_URL) return true
// Validate signature for remote events
return hasValidSignature(event)
},
// Check deletion status
isDeleted: (url, event) =>
repository.isDeleted(event),
// Sign with current user
signEvent: async (event) =>
signer.get().sign(event)
})
})
// Now all package features will use these settings
subscribe(/*...*/) // Uses pool, validates events
publish(/*...*/) // Uses signEvent
```
The Context is used internally by most features in the package.
Without proper context configuration, core features like subscription, publishing, and event validation won't work correctly.
Think of it as the central configuration that defines how your nostr networking behaves.
+56
View File
@@ -0,0 +1,56 @@
# Executor
The Executor class orchestrates event delivery and subscription management across one or more [targets](/net/targets.md). It abstracts the complexity of handling multiple connections into a single interface.
## Overview
The Executor:
- Manages subscriptions
- Handles event publishing
- Supports NIP-77 (negentropy)
- Routes messages to appropriate targets
## Basic Usage
```typescript
import {Executor, Relays} from '@welshman/net'
// Create executor with relay target
const executor = new Executor(
new Relays([
connection1,
connection2
])
)
// Subscribe to events
const sub = executor.subscribe(
[{kinds: [1], limit: 10}],
{
onEvent: (url, event) => {
console.log(`Got event from ${url}`, event)
},
onEose: (url) => {
console.log(`EOSE from ${url}`)
}
}
)
// Publish event
const pub = executor.publish(
signedEvent,
{
onOk: (url, id, success, message) => {
console.log(`Published to ${url}: ${success ? 'OK' : message}`)
}
}
)
// Clean up
sub.unsubscribe()
executor.target.cleanup()
```
The Executor is used internally by higher-level APIs but can be used directly when you need fine-grained control over event routing and subscription management.
It's particularly useful when implementing custom targets or handling special relay configurations (like local relays or relay groups).
+19
View File
@@ -0,0 +1,19 @@
# @welshman/net
Core networking layer for nostr applications, handling relay connections, message management, and event delivery.
## What's Included
- **Connection Management** - WebSocket lifecycle and relay connections
- **Subscription System** - Event filtering and subscription handling
- **Publishing Tools** - Event broadcasting with status tracking
- **Sync Utilities** - NIP-77 (negentropy) event synchronization
- **Connection Pool** - Shared relay connection management
- **Targets** - Flexible message routing strategies
- **Event Tracking** - Monitor which relays have seen events
## Installation
```bash
npm install @welshman/net
```
+37
View File
@@ -0,0 +1,37 @@
# Pool
The Pool class manages a collection of relay connections, providing a centralized way to track and reuse connections across your application.
## Overview
- Creates and caches connections
- Ensures single connection per relay
- Handles cleanup of unused connections
- Provides connection lookup
## Usage
```typescript
import {Pool} from '@welshman/net'
// Create pool
const pool = new Pool()
// Get or create connection
const connection = pool.get("wss://relay.example.com")
// Check if relay is in pool
if (pool.has("wss://relay.example.com")) {
// Use existing connection
}
// Remove connection
pool.remove("wss://relay.example.com")
// Clear all connections
pool.clear()
```
The Pool is typically used internally by the router and executor, but can be used directly for custom connection management.
It ensures efficient connection reuse across your application.
+85
View File
@@ -0,0 +1,85 @@
# Publish
The `Publish` class handles event publishing to relays, managing publish status, relay responses, and error handling.
## Overview
- Sends events to relays
- Tracks publish status per relay
- Handles OK/Error responses
- Manages timeouts
## Basic Usage
```typescript
import {`Publish`, `Publish`Status} from '@welshman/net'
const {Pending, Success, Failure, Timeout, Aborted} = `Publish`Status
// Basic `Publish`
const pub = `Publish`({
event: signedEvent,
relays: ["wss://relay.example.com"],
timeout: 3000 // 3s timeout
})
// Track status
pub.emitter.on('*', (status: `Publish`Status, url: string, message?: string) => {
switch (status) {
case Success:
console.log(``Publish`ed to ${url}`)
break
case Failure:
console.log(`Failed on ${url}: ${message}`)
break
case Timeout:
console.log(`Timeout on ${url}`)
break
}
})
```
## Real World Example
```typescript
const publishWithStatus = async (event: SignedEvent) => {
const pub = `Publish`({
event,
relays: ctx.app.router
.FromUser()
.getUrls(),
timeout: 5000
})
// Track per-relay status
const status = new Map<string, string>()
pub.emitter.on('*', (state: `Publish`Status, url: string) => {
status.set(url, state)
// Log progress
const counts = {
pending: 0,
success: 0,
failed: 0
}
for (const s of status.values()) {
counts[s] = (counts[s] || 0) + 1
}
console.log(
`Progress: ${counts.success}/${status.size}`,
`(${counts.failed} failed)`
)
})
// Wait for completion
return pub.result
}
```
Like [Subscribe](/net/subscribe.md), `Publish` uses [Pool](/net/pool.md) for connections and creates appropriate [Targets](/net/targets.md) via an [Executor](/net/executor.md), but focuses on event publishing rather than subscription management.
Note: The base `@welshman/net` Publish class just handles network publishing.
For optimistic updates and repository integration, use Publish from `@welshman/app`.
+69
View File
@@ -0,0 +1,69 @@
# Socket
The Socket class is exclusively used by the `Connection` class as its low-level WebSocket manager. It's not meant to be used directly by other classes.
Its sole purpose is to provide a reliable, manageable WebSocket connection with nostr-specific handling.
## Core Responsibilities
```typescript
export class Socket {
// Track connection state
status: SocketStatus = "new" | "open" | "opening" | "closing" | "closed" | "error"
// Handle nostr message queue
worker: Worker<Message>
// Core operations
open = async () => {/* Initialize WebSocket */}
close = async () => {/* Clean shutdown */}
send = async (message: Message) => {/* Send with JSON serialization */}
}
```
Key features:
- State tracking
- Message queuing
- JSON serialization
- Error recovery
- Connection lifecycle
Think of it as a thin wrapper that turns raw WebSocket connections into something more suitable for nostr:
```typescript
// Raw WebSocket
ws.send(JSON.stringify(["REQ", "sub1", {kinds: [1]}]))
// With Socket
socket.send(["REQ", "sub1", {kinds: [1]}]) // Handles serialization
```
## Usage Chain
```typescript
// Hierarchy
Socket // WebSocket management
Connection // Uses Socket
Relay Target // Uses Connection
Executor // Uses Target
Subscribe // Uses Executor
Publish // Uses Executor
// In Connection.ts
export class Connection extends Emitter {
socket: Socket
constructor(url: string) {
this.socket = new Socket(this)
}
}
```
It's an internal implementation detail that you shouldn't need to use directly - always interact with the `Connection` class instead, which provides a higher-level interface.
```typescript
// DON'T use Socket directly
const socket = new Socket(/*...*/) // ❌
// DO use Connection
const connection = new Connection(url) // ✅
```
This encapsulation ensures consistent connection management across the library.
+115
View File
@@ -0,0 +1,115 @@
# Subscribe
The Subscribe class manages nostr subscriptions, handling subscription lifecycle, event filtering, and relay responses. It provides a unified interface for subscribing to events across multiple relays.
## Overview
The Subscription:
- Manages REQ/CLOSE lifecycle
- Handles EOSE responses
- Emits filtered events
- Tracks completion state
```typescript
import {subscribe, SubscriptionEvent} from '@welshman/net'
// Create subscription
const sub = subscribe({
filters: [{kinds: [1], limit: 10}],
relays: ["wss://relay.example.com"],
// Optional configurations
closeOnEose: true, // Close after all relays send EOSE
timeout: 3000, // Max time to wait
authTimeout: 300, // Time for auth negotiation
delay: 50 // Delay between batched requests
})
// Handle events
sub.on(SubscriptionEvent.Event, (url, event) => {
console.log(`Got event from ${url}:`, event)
})
sub.on(SubscriptionEvent.Eose, (url) => {
console.log(`Got EOSE from ${url}`)
})
sub.on(SubscriptionEvent.Complete, () => {
console.log('Subscription complete')
})
// Close when done
sub.close()
```
## Architecture
```typescript
import {subscribe, Pool, Executor, Relays} from '@welshman/net'
// Under the hood, subscribe:
// 1. Gets connections from global pool
// 2. Creates a target (usually Relays)
// 3. Uses Executor to manage subscription
// This is roughly equivalent to:
const manualSubscribe = (urls: string[]) => {
// Get connections from pool
const connections = urls.map(url =>
ctx.net.pool.get(url)
)
// Create target
const target = new Relays(connections)
// Create executor
const executor = new Executor(target)
// Subscribe via executor
return executor.subscribe(
[{kinds: [1], limit: 10}],
{
onEvent: (url, event) => {
console.log(`Got event from ${url}`)
}
}
)
}
```
## Real World Example
```typescript
// Combine local and remote relays
const loadProfile = async (pubkey: string) => {
// Get optimal relays
const relays = ctx.app.router
.ForPubkey(pubkey)
.getUrls()
const sub = subscribe({
filters: [{
kinds: [0],
authors: [pubkey],
limit: 1
}],
relays,
// This creates internally:
// 1. Connections via Pool
// 2. Multi target with Local + Relays
// 3. Executor to manage subscription
})
return new Promise(resolve => {
sub.on('event', (url, event) => {
resolve(event)
sub.close()
})
})
}
```
The Subscribe class abstracts away:
- Connection management (via Pool)
- Target creation and setup
- Executor orchestration
+70
View File
@@ -0,0 +1,70 @@
# Sync
The Sync utilities in `@welshman/net` provide methods for synchronizing events between relays and repositories, primarily using NIP-77 (Negentropy) when available, with fallback to traditional sync methods.
## Overview
```typescript
import {sync, pull, push} from '@welshman/net'
// Three main operations:
// 1. pull: Get events from relays
// 2. push: Send events to relays
// 3. sync: Bidirectional sync
```
These utilities are primarily used by:
- `Repository` for syncing with relays
- `FeedController` for initial feed loading
## Basic Usage
```typescript
import {sync, pull, getFilterSelections} from '@welshman/net'
// Sync user profile data
const syncProfiles = async (pubkeys: string[]) => {
await sync({
// What to sync
filters: [{
kinds: [0],
authors: pubkeys
}],
// Which relays
relays: ctx.app.router
.ForPubkeys(pubkeys)
.getUrls(),
// Local events to consider
events: repository.query([{
kinds: [0],
authors: pubkeys
}])
})
}
// Initial feed load with negentropy
const loadFeed = async () => {
await pull({
filters: [{
kinds: [1],
limit: 100
}],
relays: ctx.app.router
.ForUser()
.getUrls(),
events: [], // No local events yet
onEvent: (event) => {
// Handle new events
}
})
}
```
Sync operations:
- Use NIP-77 when supported by relay
- Fall back to traditional sync
- Handle bidirectional sync
- Support filtered sync
- Track sync progress
+137
View File
@@ -0,0 +1,137 @@
# Targets
The targets system provides different strategies for message routing.
Each target type implements a common interface for handling nostr messages but with different routing behaviors.
## Overview
Targets are used by the [Executor](/net/executor.md) class to:
- Route messages to connections
- Handle responses
- Manage connection lifecycles
- Combine multiple routing strategies
## Available Targets
### Echo Target
Simple target that echoes messages back. Useful for testing.
```typescript
import {Echo} from '@welshman/net'
const echo = new Echo()
echo.on('EVENT', (url, event) => {
console.log('Echo received:', event)
})
```
### Local Target
Connects to an in-memory relay implementation.
```typescript
import {Local} from '@welshman/net'
import {Repository, Relay} from '@welshman/util'
// Create local relay
const repository = new Repository()
const relay = new Relay(repository)
const local = new Local(relay)
// Use like any other target
local.send(['REQ', 'sub1', {kinds: [1]}])
```
### Relay Target
Single relay connection target.
```typescript
import {Relay} from '@welshman/net'
const target = new Relay(connection)
target.on('EVENT', (url, event) => {
console.log(`Event from ${url}:`, event)
})
```
### Relays Target
Manages multiple relay connections.
```typescript
import {Relays} from '@welshman/net'
const target = new Relays([
connection1,
connection2,
connection3
])
```
### Multi Target
Combines multiple targets into one.
```typescript
import {Multi, Local, Relays} from '@welshman/net'
// Create multi-target with local and remote relays
const target = new Multi([
new Local(localRelay),
new Relays(remoteConnections)
])
```
## Real World Example
Here's how Coracle might set up its relay infrastructure:
```typescript
import {
Executor,
Multi,
Local,
Relays
} from '@welshman/net'
import {Repository, Relay} from '@welshman/util'
// Setup
const setupRelayInfrastructure = () => {
// Create local repository & relay
const repository = new Repository()
const localRelay = new Relay(repository)
// Get remote connections from pool
const remoteConnections = [
pool.get("wss://relay1.example.com"),
pool.get("wss://relay2.example.com")
]
// Create multi-target executor
const executor = new Executor(
new Multi([
// Local relay for immediate responses
new Local(localRelay),
// Remote relays for network queries
new Relays(remoteConnections)
])
)
// Subscribe using combined target
const sub = executor.subscribe(
[{kinds: [1], limit: 10}],
{
onEvent: (url, event) => {
if (url === LOCAL_RELAY_URL) {
console.log('Got from cache:', event)
} else {
console.log('Got from network:', url, event)
}
}
}
)
return {executor, sub}
}
```
The target system allows for flexible relay configurations while maintaining a consistent interface for the rest of the application. This is particularly useful for:
- Caching with local relays
- Load balancing across relays
- Fallback strategies
- Testing and simulation
Each target type serves a specific purpose but can be combined using `Multi` for complex routing scenarios.
+72
View File
@@ -0,0 +1,72 @@
# Tracker
The Tracker is a simple but crucial class that keeps track of which relays an event was seen on or published to. It's essential for relay selection and event source tracking.
## Overview
```typescript
import {Tracker} from '@welshman/net'
const tracker = new Tracker()
// Track event source
tracker.track(eventId, relayUrl)
// Get relays for event
const relays = tracker.getRelays(eventId) // Set<string>
// Get events from relay
const events = tracker.getIds(relayUrl) // Set<string>
// Check specific relay
const seen = tracker.hasRelay(eventId, relayUrl)
```
## Used By
1. **Repository & Sync**
```typescript
// In sync operations
pull({
events,
relays,
onEvent: (event) => {
tracker.track(event.id, relay)
}
})
```
2. **Subscribe**
```typescript
// In @welshman/app subscribe
sub.on('event', (url, event) => {
// Track where we got the event
tracker.track(event.id, url)
})
```
3. **Publish**
```typescript
// In publish operations
pub.emitter.on('success', (url) => {
// Track where we published
tracker.track(event.id, url)
})
```
4. **Router**
```typescript
// Used for relay selection
const relays = tracker
.getRelays(event.id)
.filter(url =>
isHealthyRelay(url)
)
```
The Tracker:
- Maps events to their source relays
- Maps relays to their known events
- Helps optimize relay selection
Think of it as a memory of where events came from, helping make better decisions about where to find or publish events.
+23
View File
@@ -0,0 +1,23 @@
# @welshman/signer
A comprehensive Nostr signing implementation that supports multiple authentication methods and encryption standards.
It provides a unified interface for working with different signing mechanisms while maintaining compatibility with various Nostr Implementation Possibilities (NIPs).
## What's Included
- **ISigner Interface** - Unified API across all authentication methods
- **NIP-01 Signer** - Core implementation using key-pair cryptography
- **NIP-07 Signer** - Browser extension support (nos2x, Alby, etc.)
- **NIP-46 Signer** - Remote signing with Nostr Connect protocol
- **NIP-55 Signer** - Native app integration via Capacitor
- **NIP-59 Utils** - Gift Wrap protocol for secure event encryption
## Installation
```bash
npm install @welshman/signer
```
+23
View File
@@ -0,0 +1,23 @@
# ISigner Interface
A basic interface that each signer must implement.
It includes methods for signing messages, verifying signatures, and encrypting/decrypting data.
```typescript
interface ISigner {
// Core signing functionality
sign: (event: StampedEvent) => Promise<SignedEvent>
getPubkey: () => Promise<string>
// Encryption capabilities
nip04: {
encrypt: (pubkey: string, message: string) => Promise<string>
decrypt: (pubkey: string, message: string) => Promise<string>
}
nip44: {
encrypt: (pubkey: string, message: string) => Promise<string>
decrypt: (pubkey: string, message: string) => Promise<string>
}
}
```
+41
View File
@@ -0,0 +1,41 @@
# NIP-01 Signer
The `Nip01Signer` class implements the `ISigner` interface and extends it with additional static utility methods:
```typescript
class Nip01Signer implements ISigner {
// Constructor
constructor(private secret: string)
// ISigner implementation
sign: (event: StampedEvent) => Promise<SignedEvent>
getPubkey: () => Promise<string>
nip04: { encrypt, decrypt }
nip44: { encrypt, decrypt }
// Additional static utility methods
static fromSecret(secret: string): Nip01Signer
static ephemeral(): Nip01Signer
}
```
### Additional Methods
The NIP-01 implementation extends the base interface with two static utility methods:
- `static fromSecret(secret: string)`: Alternative constructor for creating a signer from an existing private key
- `static ephemeral()`: Creates a new signer with a randomly generated private key
### Usage Example
```typescript
import { ISigner } from './interfaces'
import { Nip01Signer } from './signers/nip01'
// Using the standard interface
const signer: ISigner = new Nip01Signer(mySecret)
// Using NIP-01 specific utilities
const ephemeralSigner = Nip01Signer.ephemeral()
const fromExistingKey = Nip01Signer.fromSecret(mySecret)
```
+91
View File
@@ -0,0 +1,91 @@
# NIP-07 Signer
The `Nip07Signer` implements the `ISigner` interface by delegating signing operations to a NIP-07 compatible browser extension (like nos2x or Alby). It provides a way to interact with user's keys that are securely stored in their browser extension.
## Browser Detection
```typescript
import { getNip07 } from '@welshman/signer'
// Check if a NIP-07 provider is available
if (getNip07()) {
// Browser has a compatible extension installed
}
```
## Usage
```typescript
import { Nip07Signer } from '@welshman/signer'
// Create a new signer instance
const signer = new Nip07Signer()
// The extension will prompt the user for permission
// when operations are performed
```
## Complete Example
```typescript
import { Nip07Signer, getNip07 } from '@welshman/signer'
import { createEvent, NOTE } from '@welshman/util'
async function example() {
// Check for NIP-07 provider
if (!getNip07()) {
throw new Error('No NIP-07 provider found. Please install a Nostr browser extension.')
}
// Create signer
const signer = new Nip07Signer()
try {
// Get public key (will prompt user)
const pubkey = await signer.getPubkey()
console.log('Public key:', pubkey)
// Create and sign an event (will prompt user)
const event = createEvent(NOTE, {
content: "Hello via browser extension!",
tags: [["t", "test"]]
})
const signedEvent = await signer.sign(event)
console.log('Signed event:', signedEvent)
// Encrypt a message (will prompt user)
const recipientPubkey = "..."
const encrypted = await signer.nip44.encrypt(recipientPubkey, "Secret message")
console.log('Encrypted message:', encrypted)
} catch (error) {
// Handle user rejection or other errors
console.error('Operation failed:', error)
}
}
```
## Request Serialization
The signer implements a lock mechanism to prevent concurrent calls to the extension:
```typescript
class Nip07Signer implements ISigner {
#lock = Promise.resolve()
#then = async <T>(f: (ext: Nip07) => T | Promise<T>) => {
const promise = this.#lock.then(() => {
const ext = getNip07()
if (!ext) throw new Error("Nip07 is not enabled")
return f(ext)
})
// Reset lock after completion or error
this.#lock = promise.then(
() => undefined,
() => undefined
)
return promise
}
}
```
+153
View File
@@ -0,0 +1,153 @@
# NIP-46 (Nostr Connect) Signer
The `Nip46Signer` implements remote signing capabilities through the Nostr Connect protocol (NIP-46). It allows applications to delegate signing operations to a remote signer (like a Nostr Bunker), providing enhanced security by keeping private keys separate from the application.
## Architecture
The implementation consists of two main classes:
- `Nip46Broker`: Handles the communication with the remote signer
- `Nip46Signer`: Implements the `ISigner` interface using the broker
## Getting Started
```typescript
import {
makeSecret,
Nip46Broker,
Nip46Signer
} from '@welshman/signer'
import { createEvent, NOTE } from '@welshman/util'
async function connectToRemoteSigner() {
// Initial setup
const clientSecret = makeSecret()
const relays = ['wss://relay.example.com']
const broker = Nip46Broker.get({ relays, clientSecret })
const signer = new Nip46Signer(broker)
// Generate connection URL
const ncUrl = await broker.makeNostrconnectUrl({
name: "My App",
description: "Testing remote signing"
})
// Show URL to user (e.g., as QR code)
displayQRCode(ncUrl)
try {
// Wait for connection
const response = await broker.waitForNostrconnect(
ncUrl,
new AbortController()
)
// Store signer info for later
const bunkerUrl = broker.getBunkerUrl()
localStorage.setItem('bunkerUrl', bunkerUrl)
// Use the signer
const event = createEvent(NOTE, {
content: "Signed with remote signer!",
tags: [["t", "test"]]
})
const signed = await signer.sign(event)
return signed
} catch (error) {
if (error?.error) {
console.warn(`Signer error: ${error.error}`)
}
throw error
}
}
// Reconnecting with saved bunker URL
async function reconnect() {
const bunkerUrl = localStorage.getItem('bunkerUrl')
if (!bunkerUrl) return null
const {
signerPubkey,
connectSecret,
relays
} = Nip46Broker.parseBunkerUrl(bunkerUrl)
const broker = Nip46Broker.get({
relays,
clientSecret: makeSecret(),
signerPubkey,
connectSecret
})
return new Nip46Signer(broker)
}
```
## Nip46Broker API
### Constructor and Factory
```typescript
// Recommended: use the singleton factory
const broker = Nip46Broker.get({
relays: string[],
clientSecret: string,
connectSecret?: string,
signerPubkey?: string,
algorithm?: "nip04" | "nip44"
})
// Direct instantiation (not recommended)
new Nip46Broker(params)
```
### Connection Methods
```typescript
// Generate a nostrconnect:// URL for the remote signer
broker.makeNostrconnectUrl(metadata: Record<string, string>): Promise<string>
// Wait for connection approval
broker.waitForNostrconnect(
url: string,
abort?: AbortController
): Promise<Nip46ResponseWithResult>
// Get bunker URL for later reconnection
broker.getBunkerUrl(): string
// Parse a bunker URL
Nip46Broker.parseBunkerUrl(url: string): {
signerPubkey: string,
connectSecret: string,
relays: string[]
}
```
### Remote Operations
```typescript
// Basic operations
broker.ping(): Promise<string>
broker.getPublicKey(): Promise<string>
broker.connect(connectSecret?: string, perms?: string): Promise<string>
// Signing and encryption
broker.signEvent(event: StampedEvent): Promise<SignedEvent>
broker.nip04Encrypt(pk: string, message: string): Promise<string>
broker.nip04Decrypt(pk: string, message: string): Promise<string>
broker.nip44Encrypt(pk: string, message: string): Promise<string>
broker.nip44Decrypt(pk: string, message: string): Promise<string>
```
## Nip46Signer Usage
```typescript
const signer = new Nip46Signer(broker)
// All ISigner operations are available
const pubkey = await signer.getPubkey()
const signed = await signer.sign(event)
const encrypted = await signer.nip44.encrypt(pubkey, "message")
const decrypted = await signer.nip44.decrypt(pubkey, encrypted)
```
+165
View File
@@ -0,0 +1,165 @@
# NIP-55 (Native App) Signer
The `Nip55Signer` implements the `ISigner` interface by communicating with native mobile signing applications through the Capacitor plugin system. This implementation is particularly useful for mobile applications that want to leverage native Nostr signing capabilities.
## Prerequisites
The signer requires the Capacitor plugin to be installed:
```bash
npm install nostr-signer-capacitor-plugin
```
## Getting Started
```typescript
import { Nip55Signer, getNip55 } from '@welshman/signer'
// Check for available signing apps
const apps = await getNip55()
if (apps.length > 0) {
const signer = new Nip55Signer(apps[0].packageName)
}
```
## API Reference
### Detecting Available Signers
```typescript
// Returns information about installed signing apps
getNip55(): Promise<AppInfo[]>
interface AppInfo {
name: string
packageName: string
// Other app-specific information
}
```
### Constructor
```typescript
constructor(packageName: string)
```
Creates a new signer instance that will communicate with the specified native app.
- `packageName`: The package identifier of the native signing app
### ISigner implementation
The `Nip55Signer` class implements the [`ISigner`](/signer/) interface
```typescript
class Nip55Signer implements ISigner {
// Constructor
constructor(private secret: string)
// ISigner implementation
sign: (event: StampedEvent) => Promise<SignedEvent>
getPubkey: () => Promise<string>
nip04: { encrypt, decrypt }
nip44: { encrypt, decrypt }
}
```
## Complete Example
```typescript
import { Nip55Signer, getNip55 } from '@welshman/signer'
import { createEvent, NOTE } from '@welshman/util'
async function example() {
try {
// Get available signing apps
const apps = await getNip55()
if (apps.length === 0) {
throw new Error('No native signing apps available')
}
// Create signer with first available app
const signer = new Nip55Signer(apps[0].packageName)
// Get public key
const pubkey = await signer.getPubkey()
console.log('Public key:', pubkey)
// Sign an event
const event = createEvent(NOTE, {
content: "Hello from native app!",
tags: [["t", "test"]]
})
const signedEvent = await signer.sign(event)
console.log('Signed event:', signedEvent)
// Encrypt a message
const encrypted = await signer.nip44.encrypt(
recipientPubkey,
"Secret message"
)
console.log('Encrypted:', encrypted)
} catch (error) {
console.error('Native signer error:', error)
}
}
```
## Implementation Details
### Request Serialization
The signer implements a lock mechanism to prevent concurrent requests:
```typescript
class Nip55Signer implements ISigner {
#lock = Promise.resolve()
#plugin = NostrSignerPlugin
#packageName: string
#packageNameSet = false
#then = async <T>(f: (signer: typeof NostrSignerPlugin) => Promise<T>) => {
const promise = this.#lock.then(async () => {
if (!this.#packageNameSet) {
await this.#initialize()
}
return f(this.#plugin)
})
this.#lock = promise.then(() => Promise.resolve())
return promise
}
}
```
### Public Key Caching
The signer caches the public key to minimize native app interactions:
```typescript
class Nip55Signer {
#npub?: string
#publicKey?: string
getPubkey = async (): Promise<string> => {
return this.#then(async signer => {
if (!this.#publicKey || !this.#npub) {
const {npub} = await signer.getPublicKey()
this.#npub = npub
const {data} = decode(npub)
this.#publicKey = data as string
}
return this.#publicKey
})
}
}
```
## Platform Support
- iOS: Requires compatible signing app
- Android: Requires compatible signing app
- Operations availability depends on native app implementation
- Some features might be platform-specific
+151
View File
@@ -0,0 +1,151 @@
# NIP-59 (Gift Wrap) Implementation
The `Nip59` class provides utilities for implementing the Gift Wrap protocol (NIP-59), allowing secure event wrapping and unwrapping. This implementation works with any signer that supports encryption, making it versatile for different authentication methods.
## Key Features
- Event wrapping (encryption) for specific recipients
- Event unwrapping (decryption) of received wrapped events
- Automatic ephemeral wrapper generation
- Caching of previously unwrapped events
- Compatible with all signer implementations
## Basic Usage
```typescript
import { Nip59 } from '@welshman/signer'
import { createEvent, DIRECT_MESSAGE } from '@welshman/util'
// Create a NIP-59 instance from any signer
const nip59 = Nip59.fromSigner(mySigner)
// Wrap an event
const rumor = await nip59.wrap(
recipientPubkey,
createEvent(DIRECT_MESSAGE, {
content: "Secret message",
tags: [["p", recipientPubkey]]
})
)
// The wrapped event to publish
const wrappedEvent = rumor.wrap
// Unwrap a received event
const unwrapped = await nip59.unwrap(receivedWrappedEvent)
```
### Wrapping Process
The wrapping process involves multiple steps:
1. Create the rumor (original event)
2. Create the seal (encrypted rumor)
3. Create the wrap (encrypted seal)
```typescript
export const wrap = async (
signer: ISigner,
wrapper: ISigner,
pubkey: string,
template: StampedEvent,
tags: string[][] = []
) => {
const rumor = await getRumor(signer, template)
const seal = await getSeal(signer, pubkey, rumor)
const wrap = await getWrap(wrapper, pubkey, seal, tags)
return Object.assign(rumor, {wrap})
}
```
## API Reference
### Constructor & Factory Methods
```typescript
class Nip59 {
// Constructor
constructor(signer: ISigner, wrapper?: ISigner)
// Factory Methods
static fromSigner(signer: ISigner): Nip59
static fromSecret(secret: string): Nip59
// Instance Methods
/**
* Wraps an event for a specific recipient
* @param pubkey Recipient's public key
* @param template The event to wrap
* @param tags Additional tags for the wrap event (optional)
* @returns Promise<UnwrappedEvent> Original event and its wrapped version
*/
wrap(
pubkey: string,
template: StampedEvent,
tags?: string[][]
): Promise<UnwrappedEvent>
/**
* Unwraps a received wrapped event
* @param event The wrapped event to decrypt
* @returns Promise<UnwrappedEvent> The original unwrapped event
*/
unwrap(event: SignedEvent): Promise<UnwrappedEvent>
/**
* Creates a new instance with a specific wrapper signer
* @param wrapper Signer to use for wrapping events
* @returns Nip59 New instance with the specified wrapper
*/
withWrapper(wrapper: ISigner): Nip59
}
```
## Detailed Examples
### Basic Wrapping & Unwrapping
```typescript
import { Nip59, Nip01Signer } from '@welshman/signer'
import { createEvent, DIRECT_MESSAGE } from '@welshman/util'
async function example() {
// Create NIP-59 instance
const signer = new Nip01Signer(mySecret)
const nip59 = Nip59.fromSigner(signer)
// Create and wrap an event
const event = createEvent(DIRECT_MESSAGE, {
content: "Secret message",
tags: [["p", recipientPubkey]]
})
const rumor = await nip59.wrap(recipientPubkey, event)
// rumor contains:
// - The original event (rumor)
// - The wrapped version to publish (rumor.wrap)
// Later, unwrap a received event
const unwrapped = await nip59.unwrap(receivedEvent)
}
```
### Custom Wrapper Signer
```typescript
import { Nip59, Nip01Signer } from '@welshman/signer'
// Create with specific wrapper
const nip59 = new Nip59(
mainSigner,
Nip01Signer.ephemeral() // Custom wrapper
)
// Or add wrapper to existing instance
const nip59WithWrapper = nip59.withWrapper(
Nip01Signer.ephemeral()
)
```
+60
View File
@@ -0,0 +1,60 @@
# Basic Utilities
## synced
Creates a writable store that automatically synchronizes its value with localStorage.
```typescript
const myStore = synced('storage-key', 'default value');
```
## getter
Creates a function that returns the current value of a store without subscribing to it.
```typescript
const myStore = writable('value');
const getValue = getter(myStore);
```
## withGetter
Enhances a store by adding a getter method to access its current value.
```typescript
const myStore = withGetter(writable('value'));
console.log(myStore.get()); // 'value'
```
## throttled
Creates a store that limits how often subscribers receive updates.
```typescript
const throttledStore = throttled(1000, myStore); // Updates at most once per second
```
## custom
Creates a custom store with optional throttling and custom set behavior.
```typescript
const customStore = custom(
set => {
// Setup logic
return () => {
// Cleanup logic
};
},
{ throttle: 1000 }
);
```
## adapter
Creates a derived store that can transform values between two types while maintaining two-way binding.
```typescript
const adaptedStore = adapter({
store: originalStore,
forward: (source) => /* transform to target */,
backward: (target) => /* transform back to source */
});
```
This is particularly useful when you need to transform data structures while maintaining the ability to update the original store.
+122
View File
@@ -0,0 +1,122 @@
# Event-Based Stores
## deriveEventsMapped
Creates a store that maintains a mapped collection of events from a repository.
Useful when you want to transform events into a different data structure while maintaining reactivity.
```typescript
import {Repository, NAMED_PEOPLE, type TrustedEvent} from '@welshman/util'
import {deriveEventsMapped} from '@welshman/store'
interface UserProfile {
name: string;
about: string;
pubkey: string;
}
const repository = new Repository()
const profiles = deriveEventsMapped<UserProfile>(repository, {
filters: [{kinds: [PROFILE]}],
eventToItem: (event: TrustedEvent) => ({
name: event.content.name,
about: event.content.about,
pubkey: event.pubkey,
}),
itemToEvent: (profile: UserProfile) => ({
// Convert profile back to event format
kind: PROFILE,
pubkey: profile.pubkey,
content: {
name: profile.name,
about: profile.about,
}
}),
throttle: 1000, // Optional: throttle updates
includeDeleted: false // Optional: exclude deleted events
})
```
## deriveEvents
Creates a store that maintains a collection of raw events from a repository.
Useful when you want to work directly with events without transformation.
```typescript
import {Repository} from '@welshman/util'
import {deriveEvents} from '@welshman/store'
const repository = new Repository()
const textNotes = deriveEvents(repository, {
filters: [{kinds: [NOTE], // kind 1 = text note
authors: ['pubkey1', 'pubkey2']}],
throttle: 500,
includeDeleted: false
})
// Subscribe to changes
textNotes.subscribe(events => {
console.log('New text notes:', events)
})
```
## deriveEvent
Creates a store that tracks a single event by its ID or address.
Returns a derived store containing the event or undefined.
```typescript
import {Repository} from '@welshman/util'
import {deriveEvent} from '@welshman/store'
const repository = new Repository()
const specificEvent = deriveEvent(repository, 'event_id_or_address')
// Subscribe to changes of the specific event
specificEvent.subscribe(event => {
if (event) {
console.log('Event updated:', event)
} else {
console.log('Event not found')
}
})
```
## deriveIsDeleted
Creates a store that tracks whether an event has been deleted. Returns a boolean store.
```typescript
import {Repository} from '@welshman/util'
import {deriveIsDeleted} from '@welshman/store'
const repository = new Repository()
const event = /* your event */
const isDeleted = deriveIsDeleted(repository, event)
// Subscribe to deletion status changes
isDeleted.subscribe(deleted => {
console.log('Event deleted status:', deleted)
})
```
## deriveIsDeletedByAddress
Creates a store that tracks whether an event has been deleted by address.
Similar to deriveIsDeleted but checks deletion by address instead of event ID.
```typescript
import {Repository} from '@welshman/util'
import {deriveIsDeletedByAddress} from '@welshman/store'
const repository = new Repository()
const event = /* your event */
const isDeletedByAddress = deriveIsDeletedByAddress(repository, event)
// Subscribe to address-based deletion status changes
isDeletedByAddress.subscribe(deleted => {
if (deleted) {
console.log('Event has been deleted by address')
}
})
```
+18
View File
@@ -0,0 +1,18 @@
# @welshman/store
A utility package designed specifically for Svelte applications, providing enhanced store functionality and utilities for managing state. While it's primarily built for use with Svelte's store system, the concepts could be valuable for developers familiar with reactive programming patterns like RxJS.
## What's Included
- **Basic Utilities** - Enhanced stores with persistence, throttling, and getter methods
- **Event-Based Stores** - Specialized stores for working with nostr events and repositories
- **Custom Adapters** - Two-way data transformation with maintained reactivity
- **Persistence Layer** - Automatic localStorage synchronization
- **Performance Optimizations** - Throttled updates and efficient subscription management
## Installation
```bash
npm install @welshman/store
```
+110
View File
@@ -0,0 +1,110 @@
# Nostr Address
The Address module provides utilities for working with Nostr Addresses (NIP-19 naddr format) and handles the conversion between different address formats.
## Address Class
```typescript
class Address {
constructor(
readonly kind: number, // Event kind
readonly pubkey: string, // Author's public key
readonly identifier: string, // Unique identifier (d-tag)
readonly relays?: string[], // Optional relay hints
)
}
```
## Creating Addresses
### From Components
```typescript
const address = new Address(
30023, // kind (e.g., long-form article)
'ab82...123', // pubkey
'my-article-title', // identifier
['wss://relay.example.com'] // relays
)
```
### From String Format
```typescript
// Parse "kind:pubkey:identifier" format
const address = Address.from('30023:ab82...123:my-article-title')
// With optional relays
const address = Address.from(
'30023:ab82...123:my-article-title',
['wss://relay.example.com']
)
```
### From Naddr
```typescript
// Parse naddr format
const address = Address.fromNaddr('naddr1...')
```
### From Event
```typescript
const address = Address.fromEvent(event, relays)
```
## Converting Addresses
### To String
```typescript
const address = new Address(kind, pubkey, identifier)
address.toString() // => "kind:pubkey:identifier"
```
### To Naddr
```typescript
const address = new Address(kind, pubkey, identifier, relays)
address.toNaddr() // => "naddr1..."
```
## Utility Functions
### Check Address Format
```typescript
// Check if string is valid address format
Address.isAddress('30023:abc...123:title') // => true
Address.isAddress('not-an-address') // => false
```
### Get Address from Event
```typescript
import { getAddress } from '@welshman/util'
// Extract address from event
const address = getAddress(event)
```
## Examples
### Working with Long-form Content
```typescript
// Create address for article
const articleAddress = new Address(
30023, // Long-form content kind
authorPubkey,
'my-article-slug',
['wss://relay.example.com']
)
// Convert to string format for storage
const addressString = articleAddress.toString()
// Convert to naddr for sharing
const shareableAddress = articleAddress.toNaddr()
```
### Handling Replaceable Events
```typescript
// Create address from replaceable event
const address = Address.fromEvent(event)
// Store latest version using address as key
storage.set(address.toString(), event)
```
+162
View File
@@ -0,0 +1,162 @@
# Encryptable
The Encryptable module provides a system for handling encrypted Nostr events, particularly useful for private content like muted lists, bookmarks, or other encrypted user data.
## Core Types
### Encrypt Function
```typescript
type Encrypt = (x: string) => Promise<string>
```
### Encryptable Updates
```typescript
type EncryptableUpdates = {
content?: string
tags?: string[][]
}
```
### Decrypted Event
```typescript
type DecryptedEvent = TrustedEvent & {
plaintext: EncryptableUpdates
}
```
## Encryptable Class
```typescript
class Encryptable<T extends EventTemplate> {
constructor(
readonly event: Partial<T>, // Base event template
readonly updates: EncryptableUpdates // Plaintext updates
)
}
```
## Usage
### Basic Encryption
```typescript
// Create encryptable event
const encryptable = new Encryptable(
{ kind: 10000 }, // Base event
{ content: "secret content" } // Plaintext updates
)
// Encrypt and get final event
const event = await encryptable.reconcile(encryptFn)
```
### Private Lists
```typescript
// Create private mute list
const muteList = new Encryptable(
{
kind: 10000, // Mute list kind
tags: [] // Public tags
},
{
content: JSON.stringify(['pubkey1', 'pubkey2']), // Private content
tags: [['p', 'pubkey1'], ['p', 'pubkey2']] // Private tags
}
)
// Encrypt for publishing
const encrypted = await muteList.reconcile(async (content) => {
return await nip04.encrypt(pubkey, content)
})
```
### Updating Encrypted Content
```typescript
// Create encryptable from existing event
const existing = {
kind: 10000,
content: encryptedContent,
tags: publicTags
}
// Add new encrypted content
const updated = new Encryptable(
existing,
{
content: JSON.stringify(newContent),
tags: newPrivateTags
}
)
const final = await updated.reconcile(encrypt)
```
## Helper Functions
### Create Decrypted Event
```typescript
import { asDecryptedEvent } from '@welshman/util'
// Add plaintext content to event
const decrypted = asDecryptedEvent(
event,
{
content: decryptedContent,
tags: decryptedTags
}
)
```
## Examples
### Private Bookmarks
```typescript
// Create private bookmark list
const bookmarks = new Encryptable(
{
kind: 10003,
tags: [['d', 'bookmarks']] // Public identifier
},
{
content: JSON.stringify([
{ id: 'note1', title: 'Secret Note' }
])
}
)
// Encrypt for publishing
const event = await bookmarks.reconcile(async (content) => {
return await myEncryptionFunction(content)
})
```
### Encrypted Group Membership
```typescript
// Create private group member list
const members = new Encryptable(
{
kind: 30000,
tags: [['d', 'group-members']]
},
{
tags: members.map(m => ['p', m.pubkey, m.role])
}
)
const encrypted = await members.reconcile(encrypt)
```
### Updating Private Content
```typescript
function updatePrivateList(event: DecryptedEvent, newItems: string[]) {
return new Encryptable(
event,
{
content: JSON.stringify(newItems)
}
)
}
// Usage
const updated = updatePrivateList(existingEvent, newItems)
const final = await updated.reconcile(encrypt)
```
+185
View File
@@ -0,0 +1,185 @@
# Nostr Events
The Events module provides comprehensive type definitions and utilities for working with Nostr events, including helper functions for event creation, validation, and manipulation.
## Event Types Hierarchy
```typescript
// Base event with content and tags
interface EventContent {
tags: string[][]
content: string
}
// Base event with kind
interface EventTemplate extends EventContent {
kind: number
}
// Event with timestamp
interface StampedEvent extends EventTemplate {
created_at: number
}
// Event with author
interface OwnedEvent extends StampedEvent {
pubkey: string
}
// Event with ID
interface HashedEvent extends OwnedEvent {
id: string
}
// Event with signature
interface SignedEvent extends HashedEvent {
sig: string
[verifiedSymbol]?: boolean
}
// Event with wrapped content
interface UnwrappedEvent extends HashedEvent {
wrap: SignedEvent
}
// Event that can be either signed or wrapped
type TrustedEvent = HashedEvent & {
sig?: string
wrap?: SignedEvent
[verifiedSymbol]?: boolean
}
```
## Event Creation
### Create Basic Event
```typescript
import { createEvent } from '@welshman/util'
const event = createEvent(
1, // kind
{
content: "Hello Nostr!",
tags: [["t", "nostr"]],
created_at: now() // Optional, defaults to current time
}
)
```
## Type Guards
```typescript
// Check event types
isEventTemplate(event): boolean
isStampedEvent(event): boolean
isOwnedEvent(event): boolean
isHashedEvent(event): boolean
isSignedEvent(event): boolean
isUnwrappedEvent(event): boolean
isTrustedEvent(event): boolean
```
## Event Type Conversion
```typescript
// Convert to specific event types
asEventTemplate(event): EventTemplate
asStampedEvent(event): StampedEvent
asOwnedEvent(event): OwnedEvent
asHashedEvent(event): HashedEvent
asSignedEvent(event): SignedEvent
asUnwrappedEvent(event): UnwrappedEvent
asTrustedEvent(event): TrustedEvent
```
## Event Utilities
### Event Validation
```typescript
// Check if event has valid signature
hasValidSignature(event: SignedEvent): boolean
// Get event identifier (d tag)
getIdentifier(event: EventTemplate): string | undefined
```
### Event References
```typescript
// Get event ID or address
getIdOrAddress(event: HashedEvent): string
// Get both ID and address (if replaceable)
getIdAndAddress(event: HashedEvent): string[]
```
### Event Type Checking
```typescript
// Check event properties
isEphemeral(event: EventTemplate): boolean
isReplaceable(event: EventTemplate): boolean
isPlainReplaceable(event: EventTemplate): boolean
isParameterizedReplaceable(event: EventTemplate): boolean
```
### Thread & Reply Handling
```typescript
// Get thread information
getAncestors(event: EventTemplate): { roots: string[], replies: string[] }
// Get parent references
getParentIdsAndAddrs(event: EventTemplate): string[]
getParentIdOrAddr(event: EventTemplate): string | undefined
getParentId(event: EventTemplate): string | undefined
getParentAddr(event: EventTemplate): string | undefined
// Check reply relationship
isChildOf(child: EventTemplate, parent: HashedEvent): boolean
```
## Examples
### Creating and Processing Events
```typescript
// Create new event
const event = createEvent(1, {
content: "Hello world!",
tags: [["t", "greeting"]]
})
// Process based on type
if (isSignedEvent(event)) {
// Handle signed event
if (hasValidSignature(event)) {
processValidEvent(event)
}
} else if (isUnwrappedEvent(event)) {
// Handle wrapped event
processWrappedEvent(event)
}
```
### Working with Threads
```typescript
// Get thread context
const ancestors = getAncestors(event)
const rootId = ancestors.roots[0]
const replyTo = ancestors.replies[0]
// Check threading
if (isChildOf(event, parentEvent)) {
// Handle reply
}
```
### Type Conversion
```typescript
// Convert to needed type
const template = asEventTemplate(event)
const stamped = asStampedEvent(event)
const owned = asOwnedEvent(event)
const hashed = asHashedEvent(event)
const signed = asSignedEvent(event)
```
+165
View File
@@ -0,0 +1,165 @@
# Filters
The Filters module provides utilities for creating, manipulating, and matching Nostr event filters.
It includes support for filter operations, optimization, and time-based filtering.
## Core Types
```typescript
interface Filter {
ids?: string[] // Match specific event IDs
kinds?: number[] // Match event kinds
authors?: string[] // Match author pubkeys
since?: number // Match events since timestamp
until?: number // Match events until timestamp
limit?: number // Limit number of results
search?: string // Text search
[key: `#${string}`]: string[] // Tag filters
}
```
## Filter Operations
### Match Events
```typescript
// Match single filter
matchFilter(filter: Filter, event: HashedEvent): boolean
// Match multiple filters
matchFilters(filters: Filter[], event: HashedEvent): boolean
```
### Combine Filters
```typescript
// Combine filters with OR operation
unionFilters(filters: Filter[]): Filter[]
// Combine filters with AND operation
intersectFilters(groups: Filter[][]): Filter[]
```
### Filter Utilities
```typescript
// Get unique filter ID
getFilterId(filter: Filter): string
// Calculate filter group
calculateFilterGroup(filter: Filter): string
// Get filters for event IDs or addresses
getIdFilters(idsOrAddresses: string[]): Filter[]
// Get filters for reply events
getReplyFilters(events: TrustedEvent[], filter?: Filter): Filter[]
// Add repost filters
addRepostFilters(filters: Filter[]): Filter[]
```
## Time Constants
```typescript
// Unix epoch for Nostr (2021-01-01)
export const EPOCH = 1609459200
// One day in seconds
export const DAY = 86400
```
## Examples
### Basic Filtering
```typescript
// Create basic filter
const filter: Filter = {
kinds: [1], // Text notes
authors: ['pubkey1', 'pubkey2'],
since: now() - 24 * 60 * 60, // Last 24 hours
limit: 100
}
// Match event against filter
if (matchFilter(filter, event)) {
processEvent(event)
}
```
### Combining Filters
```typescript
// Union of filters (OR)
const combinedFilters = unionFilters([
{ kinds: [1], authors: ['pub1'] },
{ kinds: [1], authors: ['pub2'] }
])
// Intersection of filters (AND)
const intersectedFilters = intersectFilters([
[{ kinds: [1] }],
[{ authors: ['pub1'] }]
])
```
### Time-based Filtering
```typescript
// Filter events from specific time range
const timeFilter: Filter = {
since: now() - 7 * DAY, // Last week
until: now(),
limit: 100
}
// Guess appropriate time window
const delta = guessFilterDelta([timeFilter])
```
### Tag Filtering
```typescript
// Filter by tags
const tagFilter: Filter = {
'#t': ['nostr', 'bitcoin'], // Match hashtags
'#p': ['pubkey1'], // Match mentions
limit: 50
}
```
## Filter Optimization
### Trim Filters
```typescript
// Trim large filters to reasonable size
const trimmedFilter = trimFilter(filter)
const trimmedFilters = trimFilters(filters)
```
### Filter Analysis
```typescript
// Get filter generality score
const score = getFilterGenerality(filter)
// Get expected result count
const count = getFilterResultCardinality(filter)
```
## Advanced Usage
### Reply Chain Filters
```typescript
// Get filters for replies
const replyFilters = getReplyFilters(events, {
kinds: [1],
limit: 100
})
```
### Repost Handling
```typescript
// Add filters for reposts
const withReposts = addRepostFilters([
{ kinds: [1] } // Original filter
])
// Results in filters for kinds 1, 6, and 16
```
+133
View File
@@ -0,0 +1,133 @@
# Handlers (NIP-89)
The Handlers module provides functionality for working with handler recommendations and information (NIP-89).
Handlers are events that describe which kinds a given application can display.
This module provides utilities for transforming these events into structured handler objects that applications can easily process.
## Types
### Handler Definition
```typescript
type Handler = {
kind: number // Event kind this handler can process
name: string // Display name of the handler
about: string // Description
image: string // Icon or image URL
identifier: string // Unique identifier (d-tag)
event: TrustedEvent // Original handler event
website?: string // Optional website URL
lud16?: string // Optional Lightning address
nip05?: string // Optional NIP-05 identifier
}
```
## Core Functions
### Reading Handlers
```typescript
function readHandlers(event: TrustedEvent): Handler[]
// Example
const handlers = readHandlers(handlerEvent)
handlers.forEach(handler => {
console.log(`Handler for kind ${handler.kind}: ${handler.name}`)
})
```
### Handler Identification
```typescript
function getHandlerKey(handler: Handler): string
// Returns "kind:address" format
function getHandlerAddress(event: TrustedEvent): string | undefined
// Gets handler address from event tags
```
### Display Formatting
```typescript
function displayHandler(
handler?: Handler,
fallback = ""
): string
```
## Usage Examples
### Reading Handler Information
```typescript
const event = {
kind: 31990, // Handler Information kind
content: JSON.stringify({
name: "Note Viewer",
about: "Displays text notes with formatting",
image: "https://example.com/icon.png"
}),
tags: [
['k', '1'], // Handles kind 1 (text notes)
['d', 'note-viewer']
]
}
const handlers = readHandlers(event)
// Returns array of handlers defined in the event
```
### Working with Handlers
```typescript
// Get unique handler identifier
const key = getHandlerKey(handler)
// => "1:30023:note-viewer" (kind:pubkey:identifier)
// Display handler name
const name = displayHandler(handler, "Unknown Handler")
// => "Note Viewer" or fallback if handler undefined
// Get handler address
const address = getHandlerAddress(event)
// Returns address from tags with 'web' marker or first address
```
## Complete Example
```typescript
// Process handler information event
function processHandlerEvent(event: TrustedEvent) {
// Read all handlers from event
const handlers = readHandlers(event)
// Process each handler
handlers.forEach(handler => {
// Generate unique key
const key = getHandlerKey(handler)
// Store handler information
handlerRegistry.set(key, {
name: handler.name,
kind: handler.kind,
about: handler.about,
image: handler.image,
website: handler.website,
address: getHandlerAddress(handler.event)
})
})
}
// Find handler for event kind
function findHandler(kind: number): Handler | undefined {
return Array.from(handlerRegistry.values())
.find(h => h.kind === kind)
}
// Display handler information
function renderHandler(handler: Handler) {
return {
title: displayHandler(handler, "Unknown"),
description: handler.about,
icon: handler.image,
website: handler.website || null
}
}
```
+21
View File
@@ -0,0 +1,21 @@
# @welshman/util
A comprehensive utility package for Nostr application development, providing essential tools and types for working with Nostr events, addresses, profiles, and more.
## What's Included
- **Event Management**: Create, validate, and process Nostr events
- **Repository**: In-memory event storage with querying and indexing
- **Filters**: Advanced event filtering and subscription management
- **Profiles**: User profile handling and formatting
- **Lists**: Public and private list management
- **Zaps**: Lightning Network payment integration
- **Tags**: Comprehensive tag parsing and manipulation
- **Addresses**: NIP-19 address handling
- **Relays**: Relay URL handling, event dispatching and in-memory storage
## Installation
```
npm install @welshman/util
```
+71
View File
@@ -0,0 +1,71 @@
# Event Kinds
This module provides a comprehensive collection of Nostr event kind definitions and utilities.
It includes standard NIP event kinds as well as commonly used application-specific kinds.
## Kind Type Checkers
```typescript
// Check if kind is ephemeral (should not be stored)
export const isEphemeralKind = (kind: number): boolean
// Check if kind is replaceable (only latest event matters)
export const isReplaceableKind = (kind: number): boolean
// Check if kind is plain replaceable (no parameters)
export const isPlainReplaceableKind = (kind: number): boolean
// Check if kind is parameterized replaceable
export const isParameterizedReplaceableKind = (kind: number): boolean
```
## Usage Examples
### Checking Event Types
```typescript
import { isReplaceableKind, PROFILE, NOTE } from '@welshman/util'
// Profile events are replaceable
isReplaceableKind(PROFILE) // => true
// Notes are not replaceable
isReplaceableKind(NOTE) // => false
```
### Working with DVMs
```typescript
import {
DVM_REQUEST_TEXT_SUMMARY,
DVM_RESPONSE_TEXT_SUMMARY,
isDVMKind
} from '@welshman/util'
// Create DVM request
const request = {
kind: DVM_REQUEST_TEXT_SUMMARY,
content: "Text to summarize"
}
// Check for DVM events
isDVMKind(event.kind) // => true for kinds 5000-7000
```
### Handling Replaceable Events
```typescript
import {
isReplaceableKind,
PROFILE,
LONG_FORM
} from '@welshman/util'
function handleEvent(event) {
if (isReplaceableKind(event.kind)) {
// Only keep latest version
replaceExistingEvent(event)
} else {
// Keep all versions
storeNewEvent(event)
}
}
```
+26
View File
@@ -0,0 +1,26 @@
# Links
A small module for handling Nostr URI manipulation.
## Core Functions
### fromNostrURI
```typescript
function fromNostrURI(s: string): string
// Examples
fromNostrURI('nostr:npub1...') // => 'npub1...'
fromNostrURI('nostr://npub1...') // => 'npub1...'
fromNostrURI('note1...') // => 'note1...'
```
Removes the `nostr:` or `nostr://` protocol prefix from a Nostr URI.
### toNostrURI
```typescript
function toNostrURI(s: string): string
// Examples
toNostrURI('npub1...') // => 'nostr:npub1...'
toNostrURI('nostr:npub1...') // => 'nostr:npub1...' (unchanged)
```
Ensures a string has the `nostr:` protocol prefix.
+175
View File
@@ -0,0 +1,175 @@
# Lists
The Lists module provides utilities for working with Nostr lists, including both public and private lists (like bookmarks, mute lists, etc.). It handles list creation, encryption, and manipulation.
## Core Types
### List Parameters
```typescript
interface ListParams {
kind: number // List kind (e.g., 10000 for mutes)
}
```
### List Structure
```typescript
interface List extends ListParams {
publicTags: string[][] // Publicly visible tags
privateTags: string[][] // Encrypted tags
event?: DecryptedEvent // Original event if list exists
}
```
### Published List
```typescript
interface PublishedList extends List {
event: DecryptedEvent // Required event for published lists
}
```
## List Creation
### Create New List
```typescript
function makeList(list: ListParams & Partial<List>): List
// Example
const muteList = makeList({
kind: 10000,
publicTags: [['d', 'mutes']],
privateTags: [['p', 'pubkey1'], ['p', 'pubkey2']]
})
```
### Read Existing List
```typescript
function readList(event: DecryptedEvent): PublishedList
// Example
const list = readList(decryptedEvent)
```
## List Operations
### Get All Tags
```typescript
function getListTags(list: List | undefined): string[][]
// Example
const allTags = getListTags(list) // Combines public and private tags
```
### Remove Items
```typescript
// Remove by predicate
function removeFromListByPredicate(
list: List,
pred: (t: string[]) => boolean
): Encryptable
// Remove by value
function removeFromList(
list: List,
value: string
): Encryptable
```
### Add Items
```typescript
// Add public items
function addToListPublicly(
list: List,
...tags: string[][]
): Encryptable
// Add private items
function addToListPrivately(
list: List,
...tags: string[][]
): Encryptable
```
## Usage Examples
### Creating a Private List
```typescript
// Create new mute list
const muteList = makeList({
kind: 10000,
publicTags: [
['d', 'mutes'],
['name', 'My Mute List']
]
})
// Add items privately
const updated = addToListPrivately(
muteList,
['p', 'pubkey1'],
['p', 'pubkey2']
)
// Encrypt and publish
const encrypted = await updated.reconcile(encrypt)
```
### Reading and Updating Lists
```typescript
// Read existing list
const list = readList(decryptedEvent)
// Remove item
const removeItem = removeFromList(list, 'pubkey1')
// Add new items publicly
const addItems = addToListPublicly(
list,
['p', 'pubkey3'],
['p', 'pubkey4']
)
```
### Working with Tags
```typescript
// Get all list tags
const tags = getListTags(list)
// Remove by predicate
const noMentions = removeFromListByPredicate(
list,
tag => tag[0] === 'p'
)
```
## Common List Types
### Mute List
```typescript
const muteList = makeList({
kind: 10000,
publicTags: [['d', 'mutes']],
privateTags: [] // Keep muted users private
})
```
### Bookmark List
```typescript
const bookmarks = makeList({
kind: 10003,
privateTags: [
['e', 'id1'],
['e', 'id2']
]
})
```
### Relay List
```typescript
const relays = makeList({
kind: 10002,
publicTags: [[
['r', 'wss://relay1.com'],
['r', 'wss://relay2.com', 'write']
]
})
```
+115
View File
@@ -0,0 +1,115 @@
# Profile
The Profile module provides utilities for handling Nostr user profiles (kind 0 events), including profile creation, reading, and display formatting.
## Core Types
### Profile Structure
```typescript
interface Profile {
name?: string // Display name
nip05?: string // NIP-05 verification
lud06?: string // Legacy Lightning address
lud16?: string // Lightning address
lnurl?: string // Lightning URL
about?: string // Bio/description
banner?: string // Banner image URL
picture?: string // Profile picture URL
website?: string // Website URL
display_name?: string // Alternative display name
event?: TrustedEvent // Original profile event
}
```
### Published Profile
```typescript
interface PublishedProfile extends Omit<Profile, "event"> {
event: TrustedEvent // Required event for published profiles
}
```
## Core Functions
### Profile Creation & Reading
```typescript
// Create new profile
function makeProfile(profile: Partial<Profile>): Profile
// Read profile from event
function readProfile(event: TrustedEvent): PublishedProfile
// Create profile event
function createProfile(profile: Profile): EventTemplate
// Edit existing profile
function editProfile(profile: PublishedProfile): EventTemplate
```
### Display Formatting
```typescript
// Format pubkey for display
function displayPubkey(pubkey: string): string
// Format profile name for display
function displayProfile(
profile?: Profile,
fallback = ""
): string
// Check if profile has name
function profileHasName(profile?: Profile): boolean
```
## Usage Examples
### Creating New Profile
```typescript
// Create basic profile
const profile = makeProfile({
name: "Alice",
about: "Nostr user",
picture: "https://example.com/avatar.jpg",
lud16: "alice@getalby.com"
})
// Create profile event
const event = createProfile(profile)
```
### Reading Profile
```typescript
// Read profile from event
const profile = readProfile(profileEvent)
// Access profile data
console.log(profile.name)
console.log(profile.about)
console.log(profile.lnurl) // Auto-generated from lud16/lud06
```
### Displaying Profile
```typescript
// Display profile name
const name = displayProfile(profile, "Anonymous")
// Display pubkey
const shortPubkey = displayPubkey(profile.event.pubkey)
// => "npub1abc...xyz"
// Check for name
if (profileHasName(profile)) {
showName(profile)
} else {
showPubkey(profile)
}
```
### Updating Profile
```typescript
// Edit existing profile
const updated = editProfile({
...existingProfile,
name: "New Name",
about: "Updated bio"
})
```
+147
View File
@@ -0,0 +1,147 @@
# Relay
The `Relay` module provides utilities for working with Nostr relays, including a local in-memory relay implementation that integrates with [Repository](/util/repository) for event storage.
The Relay class extends EventEmitter to provide event-based communication.
## Core Components
### Relay Class
```typescript
class Relay<E extends HashedEvent = TrustedEvent> extends Emitter {
constructor(readonly repository: Repository<E>)
// Emit events: 'EVENT', 'EOSE', 'OK'
emit(type: string, ...args: any[]): boolean
// Handle relay messages
send(type: string, ...message: any[]): void
}
```
### Relay Profile
```typescript
interface RelayProfile {
url: string // Relay URL
name?: string // Display name
description?: string // Description
pubkey?: string // Operator's pubkey
contact?: string // Contact information
software?: string // Software name
version?: string // Software version
supported_nips?: number[] // Supported NIPs
limitation?: {
min_pow_difficulty?: number
payment_required?: boolean
auth_required?: boolean
}
}
```
### Finding Relay Information
```typescript
// Fetch relay information document
async function getRelayProfile(url: string): Promise<RelayProfile | null> {
try {
const normalized = normalizeRelayUrl(url)
// Convert ws/wss to http/https
const httpUrl = normalized.replace(/^ws(s)?:\/\//, 'http$1://')
// Fetch relay information document
const response = await fetch(`${httpUrl}`)
const info = await response.json()
return {
url: normalized,
name: info.name,
description: info.description,
pubkey: info.pubkey,
contact: info.contact,
software: info.software,
version: info.version,
supported_nips: info.supported_nips,
limitation: info.limitation
}
} catch (error) {
console.error(`Failed to fetch relay info for ${url}:`, error)
return null
}
}
```
## URL Utilities
### URL Validation
```typescript
// Check if URL is valid relay URL
isRelayUrl(url: string): boolean
// Check if URL is .onion address
isOnionUrl(url: string): boolean
// Check if URL is local
isLocalUrl(url: string): boolean
// Check if URL is IP address
isIPAddress(url: string): boolean
// Check if URL can be shared
isShareableRelayUrl(url: string): boolean
```
### URL Formatting
```typescript
// Normalize relay URL
normalizeRelayUrl(url: string): string
// Format URL for display
displayRelayUrl(url: string): string
// Format relay profile for display
displayRelayProfile(profile?: RelayProfile, fallback = ""): string
```
## Usage Examples
### URL Processing
```typescript
// Validate relay URL
if (isRelayUrl(url)) {
// Normalize for consistency
const normalized = normalizeRelayUrl(url)
// Check if shareable
if (isShareableRelayUrl(normalized)) {
// Format for display
const display = displayRelayUrl(normalized)
showRelay(display)
}
}
```
### Relay usage with Repository
```typescript
// Create storage and relay interface
const repository = new Repository()
const relay = new Relay(repository)
// Subscribe to events
relay.send("REQ", "sub_id", {
kinds: [1],
limit: 100
})
// Listen for events
relay.on("EVENT", (subId, event) => {
console.log(`Received event for ${subId}:`, event)
})
// Publish event
// Will be stored in repository and sent to matching subscribers
relay.send("EVENT", signedEvent)
// Close subscription
relay.send("CLOSE", "sub_id")
```
+115
View File
@@ -0,0 +1,115 @@
# Repository
The Repository module provides a robust in-memory event storage system with indexing, querying, and event replacement capabilities.
## Core Features
- Event storage and indexing
- Query support with multiple filters
- Event replacement and deletion tracking
- Event update notifications
- Optimized indexes for common queries
## Class Definition
```typescript
class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
// Storage indexes
eventsById = new Map<string, E>()
eventsByWrap = new Map<string, E>()
eventsByAddress = new Map<string, E>()
eventsByTag = new Map<string, E[]>()
eventsByDay = new Map<number, E[]>()
eventsByAuthor = new Map<string, E[]>()
eventsByKind = new Map<number, E[]>()
deletes = new Map<string, number>()
}
```
## Core Methods
### Event Management
```typescript
// Store or update event
publish(event: E, opts = { shouldNotify: true }): boolean
// Get event by ID or address
getEvent(idOrAddress: string): E | undefined
// Check if event exists
hasEvent(event: E): boolean
// Remove event
removeEvent(idOrAddress: string): void
// Check deletion status
isDeleted(event: E): boolean
isDeletedByAddress(event: E): boolean
isDeletedById(event: E): boolean
```
### Querying
```typescript
// Query events with filters
query(
filters: Filter[],
opts = {
includeDeleted: false,
shouldSort: true
}
): E[]
// Dump all events
dump(): E[]
// Load events in bulk
load(events: E[], chunkSize = 1000): void
```
## Usage Examples
### Basic Repository Operations
```typescript
// Create repository
const repo = new Repository<TrustedEvent>()
// Add events
repo.publish(event)
// Query events
const events = repo.query([
{ kinds: [1], limit: 100 }
])
// Check event status
if (!repo.isDeleted(event)) {
processEvent(event)
}
```
### Bulk Operations
```typescript
// Load multiple events
repo.load(events, 500) // Process in chunks of 500
// Get all events
const allEvents = repo.dump()
```
### Query Examples
```typescript
// Query with multiple filters
const events = repo.query([
// Recent events from specific authors
{
kinds: [1],
authors: ['pub1', 'pub2'],
since: now() - 24 * 60 * 60
},
// Events with specific tags
{
'#t': ['bitcoin', 'nostr'],
limit: 50
}
])
```
+149
View File
@@ -0,0 +1,149 @@
# Tags
The Tags module provides comprehensive utilities for working with Nostr event tags, including helpers for extracting, validating, and manipulating different types of tags.
## Core Functions
### Basic Tag Operations
```typescript
// Get tags by type(s)
getTags(types: string | string[], tags: string[][]): string[][]
// Get single tag by type(s)
getTag(types: string | string[], tags: string[][]): string[] | undefined
// Get tag values
getTagValues(types: string | string[], tags: string[][]): string[]
// Get single tag value
getTagValue(types: string | string[], tags: string[][]): string | undefined
```
## Tag Type Extractors
### Event References
```typescript
// Get 'e' tags (event references)
getEventTags(tags: string[][]): string[][]
getEventTagValues(tags: string[][]): string[]
// Get 'a' tags (event addresses)
getAddressTags(tags: string[][]): string[][]
getAddressTagValues(tags: string[][]): string[]
```
### Profile References
```typescript
// Get 'p' tags (pubkey references)
getPubkeyTags(tags: string[][]): string[][]
getPubkeyTagValues(tags: string[][]): string[]
```
### Topics and Relays
```typescript
// Get 't' tags (topics/hashtags)
getTopicTags(tags: string[][]): string[][]
getTopicTagValues(tags: string[][]): string[]
// Get 'r' and 'relay' tags
getRelayTags(tags: string[][]): string[][]
getRelayTagValues(tags: string[][]): string[]
```
### Groups and Kinds
```typescript
// Get group tags
getGroupTags(tags: string[][]): string[][]
getGroupTagValues(tags: string[][]): string[]
// Get 'k' tags (kind references)
getKindTags(tags: string[][]): string[][]
getKindTagValues(tags: string[][]): number[]
```
## Thread Management
### Comment Tags
```typescript
// Get root and reply references
getCommentTags(tags: string[][]): {
roots: string[][],
replies: string[][]
}
getCommentTagValues(tags: string[][]): {
roots: string[],
replies: string[]
}
```
### Reply Tags
```typescript
// Get detailed reply structure
getReplyTags(tags: string[][]): {
roots: string[][], // Thread roots
replies: string[][], // Direct replies
mentions: string[][] // Mentions
}
getReplyTagValues(tags: string[][]): {
roots: string[],
replies: string[],
mentions: string[]
}
```
## Utility Functions
```typescript
// Remove duplicate tags
uniqTags(tags: string[][]): string[][]
// Parse imeta tags into array of tag arrays
tagsFromIMeta(imeta: string[]): string[][]
```
## Usage Examples
### Basic Tag Handling
```typescript
// Get specific tag types
const pubkeys = getPubkeyTagValues(event.tags)
const topics = getTopicTagValues(event.tags)
const relays = getRelayTagValues(event.tags)
// Get multiple tag types
const refs = getTags(['p', 'e'], event.tags)
// Get single tag
const topic = getTagValue('t', event.tags)
```
### Thread Processing
```typescript
// Get thread context
const {roots, replies} = getReplyTags(event.tags)
// Process thread structure
function processThread(tags: string[][]) {
const thread = getReplyTags(tags)
return {
rootEvents: thread.roots.map(t => t[1]),
replyTo: thread.replies.map(t => t[1]),
mentions: thread.mentions.map(t => t[1])
}
}
```
### Tag Collection
```typescript
// Collect all references
function collectReferences(tags: string[][]) {
return {
events: getEventTagValues(tags),
profiles: getPubkeyTagValues(tags),
addresses: getAddressTagValues(tags)
}
}
```
+192
View File
@@ -0,0 +1,192 @@
# Zaps
The Zaps module provides utilities for working with Lightning Network payments (zaps) in Nostr, including LNURL handling, invoice amount parsing, and zap validation.
## Zapper Interface
The Zapper interface represents a Lightning Network payment provider that can process zaps:
```typescript
interface Zapper {
// LNURL for payment processing
lnurl: string
// User's pubkey on the payment service
pubkey?: string
// LNURL callback endpoint
callback?: string
// Minimum payment amount in millisatoshis
minSendable?: number
// Maximum payment amount in millisatoshis
maxSendable?: number
// Pubkey used to sign zap receipts
nostrPubkey?: string
// Whether provider supports Nostr zaps
allowsNostr?: boolean
}
```
### Finding Nostr Zappers
#### Getting Lightning Info
First, check the user's profile for Lightning addresses:
```typescript
function getLightningInfo(profile: Profile) {
// Check for Lightning Address (NIP-57)
if (profile.lud16) {
return {
type: 'lud16',
address: profile.lud16
}
}
// Check for LNURL
if (profile.lud06) {
return {
type: 'lud06',
url: profile.lud06
}
}
return null
}
```
#### Fetching LNURL Metadata
Once you have the Lightning address or LNURL, fetch the metadata:
```typescript
async function fetchZapper(address: string): Promise<Zapper | null> {
// Convert Lightning address to LNURL if needed
const lnurl = getLnUrl(address)
if (!lnurl) return null
try {
// Decode and fetch LNURL metadata
const url = new URL(bech32.decode(lnurl).data)
const response = await fetch(url.toString())
const metadata = await response.json()
// Extract zapper details
return {
lnurl,
callback: metadata.callback,
minSendable: metadata.minSendable,
maxSendable: metadata.maxSendable,
nostrPubkey: metadata.nostrPubkey,
allowsNostr: Boolean(metadata.allowsNostr),
}
} catch (error) {
console.error('Failed to fetch zapper:', error)
return null
}
}
```
```typescript
// Example Alby zapper configuration
const albyZapper: Zapper = {
lnurl: "lnurl1...",
pubkey: "alby_user_pubkey",
nostrPubkey: "alby_signing_key",
allowsNostr: true,
minSendable: 1000, // 1 sat minimum
maxSendable: 100000000 // 100k sats maximum
}
// Example LNbits zapper
const lnbitsZapper: Zapper = {
lnurl: "lnurl1...",
callback: "https://lnbits.com/callback",
nostrPubkey: "lnbits_signing_key",
allowsNostr: true
}
```
### Zap Structure
```typescript
interface Zap {
request: TrustedEvent // Zap request event kind 9734
response: TrustedEvent // Zap receipt/response event kind 9735 sent by the zapper
invoiceAmount: number // Amount in millisats
}
```
## Core Functions
### Lightning Address Handling
```typescript
// Convert address to LNURL
function getLnUrl(address: string): string | null
// Examples:
getLnUrl("user@domain.com") // => lnurl1...
getLnUrl("https://domain.com/.well-known/lnurlp/user") // => lnurl1...
getLnUrl("lnurl1...") // => returns unchanged
```
### Invoice Processing
```typescript
// Parse amount from BOLT11 invoice
function getInvoiceAmount(bolt11: string): number
// Convert human readable amount to millisats
function hrpToMillisat(hrpString: string): bigint
```
### Zap Validation
The `zapFromEvent` function validates a zap receipt event, against an expected zapper.
It returns a `Zap` object if the zap is valid, or `null` if not.
```typescript
function zapFromEvent(
response: TrustedEvent,
zapper: Zapper | undefined
): Zap | null
```
## Usage Examples
### Processing Lightning Addresses
```typescript
// Get LNURL from various formats
const lnurl1 = getLnUrl("user@getalby.com")
const lnurl2 = getLnUrl("https://getalby.com/.well-known/lnurlp/user")
const lnurl3 = getLnUrl("lnurl1...")
// Check if conversion was successful
if (lnurl1) {
// Process LNURL
processLnurl(lnurl1)
}
```
### Invoice Amount Handling
```typescript
// Get invoice amount in millisats
const amount = getInvoiceAmount(bolt11Invoice)
// Convert string amount to millisats
const millisats = hrpToMillisat("1000") // 1000 sats
const millisats = hrpToMillisat("1m") // 1 million sats
```
### Zap Validation
```typescript
// Validate zap event
const zap = zapFromEvent(zapResponse, albyZapper)
if (zap) {
// Process valid zap
processZap(zap)
}
```
+20
View File
@@ -0,0 +1,20 @@
---
outline: deep
---
# What is Welshman?
Welshman is a production-grade nostr toolkit powering [Coracle](https://coracle.social) and [Flotilla](https://flotilla.social).
Built as independent, opt-in packages, it lets you choose exactly what you need - from basic event handling to complete feed management.
Need just a content parser? Grab @welshman/content. Building a complex client? Start with @welshman/app and add more packages as you grow.
Each module is battle-tested in production, designed to work together but never dependent on each other.
Build your next Nostr client with the same tools that power today's leading Nostr applications.
<!-- ## Installation
```sh
npm install @welshman
``` -->
+1794 -134
View File
File diff suppressed because it is too large Load Diff
+12 -3
View File
@@ -9,7 +9,16 @@
},
"devDependencies": {
"gts": "^6.0.2",
"typedoc": "^0.27.4",
"typescript": "^5.6.3"
"typedoc": "^0.27.9",
"typedoc-plugin-markdown": "^4.4.2",
"typedoc-vitepress-theme": "^1.1.2",
"typescript": "^5.6.3",
"vitepress": "^1.6.3"
},
"scripts": {
"predocs": "typedoc",
"docs:dev": "vitepress dev docs",
"docs:build": "npx typedoc && vitepress build docs",
"docs:preview": "vitepress preview docs"
}
}
}
+3
View File
@@ -1,7 +1,10 @@
{
"name": "Welshman Docs",
"out": "./docs/reference",
"docsRoot": "./docs",
"entryPoints": ["packages/*"],
"entryPointStrategy": "packages",
"exclude": ["**/normalize-url/*"],
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
"hideGenerator": true
}