From 075cdcb97b603b258734bc8f6c3afcc6d89c949c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Fri, 3 Apr 2026 11:07:14 +0200 Subject: [PATCH] refactor: cleanup skills --- .claude/commands/sync-nips.md | 47 +- .claude/skills/applesauce-common/SKILL.md | 623 ---------- .claude/skills/applesauce-core/SKILL.md | 1056 ----------------- .claude/skills/applesauce-signers/SKILL.md | 757 ------------ .claude/skills/nostr-tools/SKILL.md | 767 ------------ .claude/skills/nostr/README.md | 162 --- .claude/skills/nostr/SKILL.md | 602 ++++------ .claude/skills/react/README.md | 119 -- .claude/skills/react/SKILL.md | 1026 ---------------- .../react/examples/practical-patterns.tsx | 878 -------------- .../react/references/hooks-quick-reference.md | 291 ----- .../skills/react/references/performance.md | 658 ---------- .../react/references/server-components.md | 656 ---------- .claude/skills/tailwind-v4.md | 401 ------- 14 files changed, 263 insertions(+), 7780 deletions(-) delete mode 100644 .claude/skills/applesauce-common/SKILL.md delete mode 100644 .claude/skills/applesauce-core/SKILL.md delete mode 100644 .claude/skills/applesauce-signers/SKILL.md delete mode 100644 .claude/skills/nostr-tools/SKILL.md delete mode 100644 .claude/skills/nostr/README.md delete mode 100644 .claude/skills/react/README.md delete mode 100644 .claude/skills/react/SKILL.md delete mode 100644 .claude/skills/react/examples/practical-patterns.tsx delete mode 100644 .claude/skills/react/references/hooks-quick-reference.md delete mode 100644 .claude/skills/react/references/performance.md delete mode 100644 .claude/skills/react/references/server-components.md delete mode 100644 .claude/skills/tailwind-v4.md diff --git a/.claude/commands/sync-nips.md b/.claude/commands/sync-nips.md index 139b9ee..75b2f76 100644 --- a/.claude/commands/sync-nips.md +++ b/.claude/commands/sync-nips.md @@ -81,7 +81,49 @@ Read the current file, then apply changes carefully. - Every NIP in `VALID_NIPS` should have a corresponding entry in `NIP_METADATA` (`src/lib/nip-icons.ts`) - Flag and fix any inconsistencies -## Step 6: Verify +## Step 6: Update the Nostr skill + +Using the upstream data fetched in Step 1, update the Nostr protocol skill files in `.claude/skills/nostr/`. + +### Update `references/event-kinds.md` + +Regenerate this file from the upstream Event Kinds table. For each kind: +- Kind number, name, NIP reference +- Replaceability behavior (based on kind range) +- Brief description of purpose and key tags + +Preserve the existing document structure: +- Section grouping: Core Events (0-999), Regular (1000-9999), Replaceable (10000-19999), Ephemeral (20000-29999), Parameterized Replaceable (30000-39999) +- The "Event Kind Ranges Summary" table +- The "Common Patterns" section with JSON examples for frequently-used kinds (kinds 0, 1, 7, 10002, 30023) +- The "Event Kind Selection Guide" section + +### Update `references/nips-overview.md` + +Update NIP entries to match upstream: +- Add new NIPs with a brief description of purpose and key details +- Update titles/descriptions that changed upstream +- Mark deprecated NIPs with their status +- Maintain the existing grouping structure (Core Protocol, Social Features, Advanced, etc.) +- Place new NIPs in the appropriate section based on their topic + +### Update the Key NIPs table in `SKILL.md` + +Update the "Key NIPs Reference" table in `.claude/skills/nostr/SKILL.md`: +- Keep it as a curated summary (15-20 most important NIPs), not an exhaustive list +- Add any newly-important NIPs (final status, widely implemented) +- Remove deprecated NIPs from the table +- Update descriptions if they changed upstream + +Also update the "Common kinds" one-liner in the "Event Kind Ranges" section if new widely-used kinds were added. + +### Do NOT change in `SKILL.md`: +- The protocol fundamentals sections (event structure, tags, filters, WebSocket messages) +- The nostr-tools code examples +- The "Common Patterns" code examples +- The "Security Essentials" section + +## Step 7: Verify ```bash npm run lint && npm run test:run && npm run build @@ -89,11 +131,12 @@ npm run lint && npm run test:run && npm run build Fix any lint/type/build issues before reporting. -## Step 7: Report +## Step 8: Report Summarize: - NIPs added / removed / title-updated - NIP icons added / updated in `nip-icons.ts` - Kinds added / updated (with icon choices explained for new ones) +- Nostr skill files updated (which reference files changed, what was added/removed) - Inconsistencies found and resolved - Verification results (lint/test/build) diff --git a/.claude/skills/applesauce-common/SKILL.md b/.claude/skills/applesauce-common/SKILL.md deleted file mode 100644 index 22af114..0000000 --- a/.claude/skills/applesauce-common/SKILL.md +++ /dev/null @@ -1,623 +0,0 @@ ---- -name: applesauce-common -description: This skill should be used when working with applesauce-common library for social/NIP-specific helpers, casting system, blueprints, and operations. New in applesauce v5 - contains helpers that moved from applesauce-core. ---- - -# applesauce-common Skill (v5) - -This skill provides comprehensive knowledge for working with applesauce-common, a new package in applesauce v5 that contains social/NIP-specific utilities, the casting system, blueprints, and operations. - -**Note**: applesauce-common was introduced in v5. Many helpers that were previously in `applesauce-core/helpers` have moved here. - -## When to Use This Skill - -Use this skill when: -- Working with article, highlight, threading, zap, or reaction helpers -- Using the casting system for typed event access -- Creating events with blueprints -- Modifying events with operations -- Working with NIP-specific social features - -## Package Structure - -``` -applesauce-common/ -├── helpers/ # Social/NIP-specific helpers -│ ├── article.js # NIP-23 article helpers -│ ├── highlight.js # NIP-84 highlight helpers -│ ├── threading.js # NIP-10 thread helpers -│ ├── comment.js # NIP-22 comment helpers -│ ├── zap.js # NIP-57 zap helpers -│ ├── reaction.js # NIP-25 reaction helpers -│ ├── lists.js # NIP-51 list helpers -│ └── ... -├── casts/ # Typed event classes -│ ├── Note.js -│ ├── User.js -│ ├── Profile.js -│ ├── Article.js -│ └── ... -├── blueprints/ # Event creation blueprints -└── operations/ # Event modification operations -``` - -## Helpers (Migrated from applesauce-core) - -### Article Helpers (NIP-23) - -```typescript -import { - getArticleTitle, - getArticleSummary, - getArticleImage, - getArticlePublished -} from 'applesauce-common/helpers/article'; - -// All helpers cache internally - no useMemo needed -const title = getArticleTitle(event); -const summary = getArticleSummary(event); -const image = getArticleImage(event); -const publishedAt = getArticlePublished(event); -``` - -### Highlight Helpers (NIP-84) - -```typescript -import { - getHighlightText, - getHighlightSourceUrl, - getHighlightSourceEventPointer, - getHighlightSourceAddressPointer, - getHighlightContext, - getHighlightComment -} from 'applesauce-common/helpers/highlight'; - -const text = getHighlightText(event); -const sourceUrl = getHighlightSourceUrl(event); -const eventPointer = getHighlightSourceEventPointer(event); -const addressPointer = getHighlightSourceAddressPointer(event); -const context = getHighlightContext(event); -const comment = getHighlightComment(event); -``` - -### Threading Helpers (NIP-10) - -```typescript -import { getNip10References } from 'applesauce-common/helpers/threading'; - -// Parse NIP-10 thread structure -const refs = getNip10References(event); - -if (refs.root) { - console.log('Root event:', refs.root.e); - console.log('Root address:', refs.root.a); -} - -if (refs.reply) { - console.log('Reply to:', refs.reply.e); -} -``` - -### Comment Helpers (NIP-22) - -```typescript -import { getCommentReplyPointer } from 'applesauce-common/helpers/comment'; - -const pointer = getCommentReplyPointer(event); -if (pointer) { - // Handle reply target -} -``` - -### Zap Helpers (NIP-57) - -```typescript -import { - getZapAmount, - getZapSender, - getZapRecipient, - getZapComment -} from 'applesauce-common/helpers/zap'; - -const amount = getZapAmount(event); // In millisats -const sender = getZapSender(event); // Pubkey -const recipient = getZapRecipient(event); -const comment = getZapComment(event); -``` - -### List Helpers (NIP-51) - -```typescript -import { getRelaysFromList } from 'applesauce-common/helpers/lists'; - -const relays = getRelaysFromList(event); -``` - -## Casting System - -The casting system transforms raw Nostr events into typed classes with both synchronous properties and reactive observables. - -### Basic Usage - -```typescript -import { castEvent, Note, User, Profile } from 'applesauce-common/casts'; - -// Cast an event to a typed class -const note = castEvent(event, Note, eventStore); - -// Access synchronous properties -console.log(note.id); -console.log(note.createdAt); -console.log(note.isReply); - -// Subscribe to reactive observables -note.author.profile$.subscribe(profile => { - console.log('Author name:', profile?.name); -}); -``` - -### Available Casts - -- **Note** - Kind 1 short text notes -- **User** - User with profile and social graph -- **Profile** - Kind 0 profile metadata -- **Article** - Kind 30023 long-form articles -- **Reaction** - Kind 7 reactions -- **Zap** - Kind 9735 zap receipts -- **Comment** - NIP-22 comments -- **Share** - Reposts/quotes -- **Bookmarks** - NIP-51 bookmarks -- **Mutes** - NIP-51 mute lists - -### With React - -```typescript -import { use$ } from 'applesauce-react/hooks'; -import { castEvent, Note } from 'applesauce-common/casts'; - -function NoteComponent({ event }) { - const note = castEvent(event, Note, eventStore); - - // Subscribe to author's profile - const profile = use$(note.author.profile$); - - // Subscribe to replies - const replies = use$(note.replies$); - - return ( -
- {profile?.name} -

{note.content}

- {replies?.length} replies -
- ); -} -``` - -## Blueprints - -Blueprints are factory functions that create Nostr events with automatic tag extraction and proper NIP compliance. They eliminate manual tag building and reduce boilerplate code significantly. - -### Key Benefits - -1. **Automatic Tag Extraction**: Blueprints automatically extract hashtags (#word), mentions (nostr:npub), and event quotes (nostr:note/nevent) from text content -2. **NIP Compliance**: Each blueprint follows the correct NIP specifications for its event type -3. **Less Code**: Replace 50-100 lines of manual tag building with 5-10 lines -4. **Type Safety**: Full TypeScript support with proper types for all options -5. **Maintainable**: Centralized event building logic that's easier to update - -### Using Blueprints - -```typescript -import { EventFactory } from 'applesauce-core/event-factory'; -import { NoteBlueprint } from 'applesauce-common/blueprints'; - -const factory = new EventFactory(); -factory.setSigner(signer); - -// Create event from blueprint -const draft = await factory.create(NoteBlueprint, content, options); -const event = await factory.sign(draft); -``` - -### NoteBlueprint (Kind 1) - -Creates short text notes with automatic hashtag, mention, and quote extraction. - -**What it handles automatically:** -- Extracts `#hashtags` from content → `t` tags -- Extracts `nostr:npub...` mentions → `p` tags -- Extracts `nostr:note...` and `nostr:nevent...` quotes → `q` tags (NIP-18) -- Adds custom emoji tags (NIP-30) - -```typescript -import { NoteBlueprint } from 'applesauce-common/blueprints'; - -// Simple note -const draft = await factory.create( - NoteBlueprint, - 'Hello #nostr! Check out nostr:npub1abc...', - {} -); - -// With custom emojis -const draft = await factory.create( - NoteBlueprint, - 'Hello :rocket:!', - { - emojis: [{ shortcode: 'rocket', url: 'https://example.com/rocket.png' }] - } -); - -// The blueprint automatically adds: -// - ["t", "nostr"] for #nostr -// - ["p", "decoded-pubkey"] for the npub mention -// - ["emoji", "rocket", "https://example.com/rocket.png"] for custom emoji -``` - -**Options:** -- `emojis?: Array<{ shortcode: string; url: string }>` - Custom emojis (NIP-30) -- `contentWarning?: boolean | string` - Content warning tag - -**Before/After Example:** -```typescript -// ❌ BEFORE: Manual tag building (~70 lines) -const hashtags = content.match(/#(\w+)/g)?.map(tag => tag.slice(1)) || []; -const mentionRegex = /nostr:(npub1[a-z0-9]+)/g; -const mentions = []; -let match; -while ((match = mentionRegex.exec(content)) !== null) { - try { - const { data } = nip19.decode(match[1]); - mentions.push(data); - } catch (e) { /* ignore */ } -} -// ... more extraction logic ... -draft.tags = [ - ...hashtags.map(t => ['t', t]), - ...mentions.map(p => ['p', p]), - // ... more tags ... -]; - -// ✅ AFTER: Blueprint handles everything -const draft = await factory.create(NoteBlueprint, content, { emojis }); -``` - -### NoteReplyBlueprint (Kind 1 Reply) - -Creates threaded note replies following NIP-10 conventions. - -**What it handles automatically:** -- Extracts root event from parent's tags (NIP-10) -- Adds proper `e` tags with markers (root, reply) -- Copies `p` tags from parent for notifications -- Extracts hashtags, mentions, and quotes from content -- Uses `q` tags for quotes instead of `e` tags (correct semantic) - -```typescript -import { NoteReplyBlueprint } from 'applesauce-common/blueprints'; - -// Reply to a note -const parentEvent = await eventStore.event(parentId).toPromise(); - -const draft = await factory.create( - NoteReplyBlueprint, - parentEvent, - 'Great point! #bitcoin', - { - emojis: [{ shortcode: 'fire', url: 'https://example.com/fire.png' }] - } -); - -// The blueprint automatically: -// 1. Finds root from parent's tags (if parent is also a reply) -// 2. Adds ["e", rootId, relay, "root"] -// 3. Adds ["e", parentId, relay, "reply"] -// 4. Copies all ["p", ...] tags from parent -// 5. Extracts #bitcoin → ["t", "bitcoin"] -// 6. Adds emoji tag -``` - -**Options:** -- `emojis?: Array<{ shortcode: string; url: string }>` - Custom emojis -- `contentWarning?: boolean | string` - Content warning - -**Before/After Example:** -```typescript -// ❌ BEFORE: Manual NIP-10 threading (~95 lines) -const parentRefs = getNip10References(parentEvent); -const rootId = parentRefs.root?.e || parentEvent.id; -const rootRelay = parentRefs.root?.relay || ''; - -draft.tags = [ - ['e', rootId, rootRelay, 'root'], - ['e', parentEvent.id, '', 'reply'], -]; - -// Copy p-tags from parent -const parentPTags = parentEvent.tags.filter(t => t[0] === 'p'); -draft.tags.push(...parentPTags); -if (!parentPTags.some(t => t[1] === parentEvent.pubkey)) { - draft.tags.push(['p', parentEvent.pubkey]); -} -// ... hashtag extraction ... -// ... mention extraction ... - -// ✅ AFTER: Blueprint handles NIP-10 threading -const draft = await factory.create( - NoteReplyBlueprint, - parentEvent, - content, - { emojis } -); -``` - -### ReactionBlueprint (Kind 7) - -Creates reactions to events (likes, custom emoji reactions). - -**What it handles automatically:** -- Adds `e` tag pointing to reacted event -- Adds `k` tag for event kind -- Adds `p` tag for event author -- Handles custom emoji reactions (`:shortcode:` format) -- Supports both string emoji and Emoji objects - -```typescript -import { ReactionBlueprint } from 'applesauce-common/blueprints'; - -// Simple like (+ emoji) -const draft = await factory.create(ReactionBlueprint, messageEvent, '+'); - -// Custom emoji reaction -const draft = await factory.create( - ReactionBlueprint, - messageEvent, - { - shortcode: 'rocket', - url: 'https://example.com/rocket.png' - } -); - -// String emoji -const draft = await factory.create(ReactionBlueprint, messageEvent, '🚀'); - -// The blueprint automatically adds: -// - ["e", messageEvent.id] -// - ["k", messageEvent.kind.toString()] -// - ["p", messageEvent.pubkey] -// For custom emoji: ["emoji", "rocket", "url"] -``` - -**Options:** -- Second parameter: `emoji?: string | { shortcode: string; url: string }` - -**Before/After Example:** -```typescript -// ❌ BEFORE: Manual reaction building (~15 lines per adapter) -draft.kind = 7; -draft.content = typeof emoji === 'string' ? emoji : `:${emoji.shortcode}:`; -draft.tags = [ - ['e', messageEvent.id], - ['k', messageEvent.kind.toString()], - ['p', messageEvent.pubkey], -]; -if (typeof emoji === 'object') { - draft.tags.push(['emoji', emoji.shortcode, emoji.url]); -} - -// ✅ AFTER: Blueprint handles reactions -const draft = await factory.create(ReactionBlueprint, messageEvent, emoji); -``` - -### GroupMessageBlueprint (Kind 9 - NIP-29) - -Creates NIP-29 group chat messages. - -**What it handles automatically:** -- Adds `h` tag with group ID -- Extracts hashtags, mentions, and quotes from content -- Adds custom emoji tags -- Handles message threading with `previous` field - -```typescript -import { GroupMessageBlueprint } from 'applesauce-common/blueprints'; - -// Send message to NIP-29 group -const draft = await factory.create( - GroupMessageBlueprint, - { id: groupId, relay: relayUrl }, - 'Hello group! #welcome', - { - previous: [], // Array of previous message events for threading - emojis: [{ shortcode: 'wave', url: 'https://example.com/wave.png' }] - } -); - -// The blueprint automatically adds: -// - ["h", groupId] -// - ["t", "welcome"] for #welcome hashtag -// - ["emoji", "wave", "url"] for custom emoji -``` - -**Options:** -- `previous?: NostrEvent[]` - Previous messages for threading (required, use `[]` if no threading) -- `emojis?: Array<{ shortcode: string; url: string }>` - Custom emojis - -**Note:** The `previous` field is required by the type, but can be an empty array if you don't need threading. - -### DeleteBlueprint (Kind 5 - NIP-09) - -Creates event deletion requests. - -**What it handles automatically:** -- Adds `e` tags for each event to delete -- Sets proper kind and content format -- Adds optional reason in content - -```typescript -import { DeleteBlueprint } from 'applesauce-common/blueprints'; - -// Delete single event -const draft = await factory.create( - DeleteBlueprint, - [eventToDelete], - 'Accidental post' -); - -// Delete multiple events -const draft = await factory.create( - DeleteBlueprint, - [event1, event2, event3], - 'Cleaning up old posts' -); - -// Without reason -const draft = await factory.create(DeleteBlueprint, [event], ''); - -// The blueprint automatically: -// - Sets kind to 5 -// - Adds ["e", eventId] for each event -// - Sets content to reason (or empty) -``` - -**Parameters:** -- `events: (string | NostrEvent)[]` - Events to delete (IDs or full events) -- `reason?: string` - Optional deletion reason - -### Adding Custom Tags - -Blueprints handle common tags automatically, but you can add custom tags afterward: - -```typescript -// Create with blueprint -const draft = await factory.create(NoteBlueprint, content, { emojis }); - -// Add custom tags not handled by blueprint -draft.tags.push(['client', 'grimoire', '31990:...']); -draft.tags.push(['a', `${kind}:${pubkey}:${identifier}`]); - -// Add NIP-92 imeta tags for blob attachments -for (const blob of blobAttachments) { - draft.tags.push(['imeta', `url ${blob.url}`, `x ${blob.sha256}`, ...]); -} - -// Sign the modified draft -const event = await factory.sign(draft); -``` - -### Protocol-Specific Tag Additions - -Some protocols require additional tags beyond what blueprints provide: - -```typescript -// NIP-29: Add q-tag for replies (not in blueprint yet) -const draft = await factory.create(GroupMessageBlueprint, group, content, options); -if (replyToId) { - draft.tags.push(['q', replyToId]); -} - -// NIP-53: Add a-tag for live activity context -const draft = await factory.create(ReactionBlueprint, messageEvent, emoji); -draft.tags.push(['a', liveActivityATag, relay]); -``` - -### Available Blueprints - -All blueprints from `applesauce-common/blueprints`: - -- **NoteBlueprint** - Kind 1 short text notes -- **NoteReplyBlueprint** - Kind 1 threaded replies (NIP-10) -- **ReactionBlueprint** - Kind 7 reactions (NIP-25) -- **GroupMessageBlueprint** - Kind 9 group messages (NIP-29) -- **DeleteBlueprint** - Kind 5 deletion requests (NIP-09) -- **MetadataBlueprint** - Kind 0 profile metadata -- **ContactsBlueprint** - Kind 3 contact lists -- **ArticleBlueprint** - Kind 30023 long-form articles (NIP-23) -- **HighlightBlueprint** - Kind 9802 highlights (NIP-84) -- **ZapRequestBlueprint** - Kind 9734 zap requests (NIP-57) -- And more - check `node_modules/applesauce-common/dist/blueprints/` - -### Best Practices - -1. **Always use blueprints** when creating standard event types - they handle NIPs correctly -2. **Add custom tags after** blueprint creation for app-specific metadata -3. **Don't extract tags manually** - let blueprints handle hashtags, mentions, quotes -4. **Use proper emoji format** - blueprints expect `{ shortcode, url }` objects -5. **Check blueprint source** - when in doubt, read the blueprint code for exact behavior - -## Operations - -Operations modify existing events. - -```typescript -import { addTag, removeTag } from 'applesauce-common/operations'; - -// Add a tag to an event -const modified = addTag(event, ['t', 'bitcoin']); - -// Remove a tag -const updated = removeTag(event, 'client'); -``` - -## Migration from v4 - -### Helper Import Changes - -```typescript -// ❌ Old (v4) -import { getArticleTitle } from 'applesauce-core/helpers'; -import { getNip10References } from 'applesauce-core/helpers/threading'; -import { getZapAmount } from 'applesauce-core/helpers/zap'; - -// ✅ New (v5) -import { getArticleTitle } from 'applesauce-common/helpers/article'; -import { getNip10References } from 'applesauce-common/helpers/threading'; -import { getZapAmount } from 'applesauce-common/helpers/zap'; -``` - -### Helpers that stayed in applesauce-core - -These protocol-level helpers remain in `applesauce-core/helpers`: -- `getTagValue`, `hasNameValueTag` -- `getProfileContent` -- `parseCoordinate`, `getEventPointerFromETag`, `getAddressPointerFromATag` -- `isFilterEqual`, `matchFilter`, `mergeFilters` -- `getSeenRelays`, `mergeRelaySets` -- `getInboxes`, `getOutboxes` -- `normalizeURL` - -## Best Practices - -### Helper Caching - -All helpers in applesauce-common cache internally using symbols: - -```typescript -// ❌ Don't memoize helper calls -const title = useMemo(() => getArticleTitle(event), [event]); - -// ✅ Call helpers directly -const title = getArticleTitle(event); -``` - -### Casting vs Helpers - -Use **helpers** when you need specific fields: -```typescript -const title = getArticleTitle(event); -const amount = getZapAmount(event); -``` - -Use **casts** when you need reactive data or multiple related properties: -```typescript -const note = castEvent(event, Note, eventStore); -const profile$ = note.author.profile$; -const replies$ = note.replies$; -``` - -## Related Skills - -- **applesauce-core** - Protocol-level helpers and event store -- **applesauce-signers** - Event signing abstractions -- **nostr** - Nostr protocol fundamentals diff --git a/.claude/skills/applesauce-core/SKILL.md b/.claude/skills/applesauce-core/SKILL.md deleted file mode 100644 index d599dda..0000000 --- a/.claude/skills/applesauce-core/SKILL.md +++ /dev/null @@ -1,1056 +0,0 @@ ---- -name: applesauce-core -description: This skill should be used when working with applesauce-core library for Nostr client development, including event stores, queries, observables, and client utilities. Provides comprehensive knowledge of applesauce patterns for building reactive Nostr applications. ---- - -# applesauce-core Skill (v5) - -This skill provides comprehensive knowledge and patterns for working with applesauce-core v5, a library that provides reactive utilities and patterns for building Nostr clients. - -**Note**: applesauce v5 introduced package reorganization: -- Protocol-level code stays in `applesauce-core` -- Social/NIP-specific helpers moved to `applesauce-common` -- `EventFactory` moved from `applesauce-factory` to `applesauce-core/event-factory` -- `ActionHub` renamed to `ActionRunner` in `applesauce-actions` - -## When to Use This Skill - -Use this skill when: -- Building reactive Nostr applications -- Managing event stores and caches -- Working with observable patterns for Nostr -- Implementing real-time updates -- Building timeline and feed views -- Managing replaceable events -- Working with profiles and metadata -- Creating efficient Nostr queries - -## Core Concepts - -### applesauce-core Overview - -applesauce-core provides: -- **Event stores** - Reactive event caching and management -- **Queries** - Declarative event querying patterns -- **Observables** - RxJS-based reactive patterns -- **Profile helpers** - Profile metadata management -- **Timeline utilities** - Feed and timeline building -- **NIP helpers** - NIP-specific utilities - -### Installation - -```bash -npm install applesauce-core -``` - -### Basic Architecture - -applesauce-core is built on reactive principles: -- Events are stored in reactive stores -- Queries return observables that update when new events arrive -- Components subscribe to observables for real-time updates - -## Event Store - -### Creating an Event Store - -```javascript -import { EventStore } from 'applesauce-core'; - -// Create event store -const eventStore = new EventStore(); - -// Add events -eventStore.add(event1); -eventStore.add(event2); - -// Add multiple events -eventStore.addMany([event1, event2, event3]); - -// Check if event exists -const exists = eventStore.has(eventId); - -// Get event by ID -const event = eventStore.get(eventId); - -// Remove event -eventStore.remove(eventId); - -// Clear all events -eventStore.clear(); -``` - -### Event Store Queries - -```javascript -// Get all events -const allEvents = eventStore.getAll(); - -// Get events by filter -const filtered = eventStore.filter({ - kinds: [1], - authors: [pubkey] -}); - -// Get events by author -const authorEvents = eventStore.getByAuthor(pubkey); - -// Get events by kind -const textNotes = eventStore.getByKind(1); -``` - -### Replaceable Events - -applesauce-core handles replaceable events automatically: - -```javascript -// For kind 0 (profile), only latest is kept -eventStore.add(profileEvent1); // stored -eventStore.add(profileEvent2); // replaces if newer - -// For parameterized replaceable (30000-39999) -eventStore.add(articleEvent); // keyed by author + kind + d-tag - -// Get replaceable event -const profile = eventStore.getReplaceable(0, pubkey); -const article = eventStore.getReplaceable(30023, pubkey, 'article-slug'); -``` - -## Queries - -### Query Patterns - -```javascript -import { createQuery } from 'applesauce-core'; - -// Create a query -const query = createQuery(eventStore, { - kinds: [1], - limit: 50 -}); - -// Subscribe to query results -query.subscribe(events => { - console.log('Current events:', events); -}); - -// Query updates automatically when new events added -eventStore.add(newEvent); // Subscribers notified -``` - -### Timeline Query - -```javascript -import { TimelineQuery } from 'applesauce-core'; - -// Create timeline for user's notes -const timeline = new TimelineQuery(eventStore, { - kinds: [1], - authors: [userPubkey] -}); - -// Get observable of timeline -const timeline$ = timeline.events$; - -// Subscribe -timeline$.subscribe(events => { - // Events sorted by created_at, newest first - renderTimeline(events); -}); -``` - -### Profile Query - -```javascript -import { ProfileQuery } from 'applesauce-core'; - -// Query profile metadata -const profileQuery = new ProfileQuery(eventStore, pubkey); - -// Get observable -const profile$ = profileQuery.profile$; - -profile$.subscribe(profile => { - if (profile) { - console.log('Name:', profile.name); - console.log('Picture:', profile.picture); - } -}); -``` - -## Observables - -### Working with RxJS - -applesauce-core uses RxJS observables: - -```javascript -import { map, filter, distinctUntilChanged } from 'rxjs/operators'; - -// Transform query results -const names$ = profileQuery.profile$.pipe( - filter(profile => profile !== null), - map(profile => profile.name), - distinctUntilChanged() -); - -// Combine multiple observables -import { combineLatest } from 'rxjs'; - -const combined$ = combineLatest([ - timeline$, - profile$ -]).pipe( - map(([events, profile]) => ({ - events, - authorName: profile?.name - })) -); -``` - -### Creating Custom Observables - -```javascript -import { Observable } from 'rxjs'; - -function createEventObservable(store, filter) { - return new Observable(subscriber => { - // Initial emit - subscriber.next(store.filter(filter)); - - // Subscribe to store changes - const unsubscribe = store.onChange(() => { - subscriber.next(store.filter(filter)); - }); - - // Cleanup - return () => unsubscribe(); - }); -} -``` - -## Profile Helpers - -### Profile Metadata - -```javascript -import { parseProfile, ProfileContent } from 'applesauce-core'; - -// Parse kind 0 content -const profileEvent = await getProfileEvent(pubkey); -const profile = parseProfile(profileEvent); - -// Profile fields -console.log(profile.name); // Display name -console.log(profile.about); // Bio -console.log(profile.picture); // Avatar URL -console.log(profile.banner); // Banner image URL -console.log(profile.nip05); // NIP-05 identifier -console.log(profile.lud16); // Lightning address -console.log(profile.website); // Website URL -``` - -### Profile Store - -```javascript -import { ProfileStore } from 'applesauce-core'; - -const profileStore = new ProfileStore(eventStore); - -// Get profile observable -const profile$ = profileStore.getProfile(pubkey); - -// Get multiple profiles -const profiles$ = profileStore.getProfiles([pubkey1, pubkey2]); - -// Request profile load (triggers fetch if not cached) -profileStore.requestProfile(pubkey); -``` - -## Timeline Utilities - -### Building Feeds - -```javascript -import { Timeline } from 'applesauce-core'; - -// Create timeline -const timeline = new Timeline(eventStore); - -// Add filter -timeline.setFilter({ - kinds: [1, 6], - authors: followedPubkeys -}); - -// Get events observable -const events$ = timeline.events$; - -// Load more (pagination) -timeline.loadMore(50); - -// Refresh (get latest) -timeline.refresh(); -``` - -### Thread Building - -```javascript -import { ThreadBuilder } from 'applesauce-core'; - -// Build thread from root event -const thread = new ThreadBuilder(eventStore, rootEventId); - -// Get thread observable -const thread$ = thread.thread$; - -thread$.subscribe(threadData => { - console.log('Root:', threadData.root); - console.log('Replies:', threadData.replies); - console.log('Reply count:', threadData.replyCount); -}); -``` - -### Reactions and Zaps - -```javascript -import { ReactionStore, ZapStore } from 'applesauce-core'; - -// Reactions -const reactionStore = new ReactionStore(eventStore); -const reactions$ = reactionStore.getReactions(eventId); - -reactions$.subscribe(reactions => { - console.log('Likes:', reactions.likes); - console.log('Custom:', reactions.custom); -}); - -// Zaps -const zapStore = new ZapStore(eventStore); -const zaps$ = zapStore.getZaps(eventId); - -zaps$.subscribe(zaps => { - console.log('Total sats:', zaps.totalAmount); - console.log('Zap count:', zaps.count); -}); -``` - -## Helper Functions & Caching - -### Applesauce Helper System - -applesauce-core provides **60+ helper functions** for extracting data from Nostr events. **Critical**: These helpers cache their results internally using symbols, so **you don't need `useMemo` when calling them**. - -```javascript -// ❌ WRONG - Unnecessary memoization -const title = useMemo(() => getArticleTitle(event), [event]); -const text = useMemo(() => getHighlightText(event), [event]); - -// ✅ CORRECT - Helpers cache internally -const title = getArticleTitle(event); -const text = getHighlightText(event); -``` - -### How Helper Caching Works - -```javascript -import { getOrComputeCachedValue } from 'applesauce-core/helpers'; - -// Helpers use symbol-based caching -const symbol = Symbol('ArticleTitle'); - -export function getArticleTitle(event) { - return getOrComputeCachedValue(event, symbol, () => { - // This expensive computation only runs once - return getTagValue(event, 'title') || 'Untitled'; - }); -} - -// First call - computes and caches -const title1 = getArticleTitle(event); // Computation happens - -// Second call - returns cached value -const title2 = getArticleTitle(event); // Instant, from cache - -// Same reference -console.log(title1 === title2); // true -``` - -### Tag Helpers - -```javascript -import { getTagValue, hasNameValueTag } from 'applesauce-core/helpers'; - -// Get single tag value (searches hidden tags first) -const dTag = getTagValue(event, 'd'); -const title = getTagValue(event, 'title'); -const url = getTagValue(event, 'r'); - -// Check if tag exists with value -const hasTag = hasNameValueTag(event, 'client', 'grimoire'); -``` - -**Note**: applesauce only provides `getTagValue` (singular). For multiple values, implement your own: - -```javascript -function getTagValues(event, tagName) { - return event.tags - .filter(tag => tag[0] === tagName && tag[1]) - .map(tag => tag[1]); -} -``` - -### Article Helpers (NIP-23) - -**Note**: Article helpers moved to `applesauce-common` in v5. - -```javascript -import { - getArticleTitle, - getArticleSummary, - getArticleImage, - getArticlePublished, - isValidArticle -} from 'applesauce-common/helpers/article'; - -// All cached automatically -const title = getArticleTitle(event); -const summary = getArticleSummary(event); -const image = getArticleImage(event); -const publishedAt = getArticlePublished(event); - -// Validation -if (isValidArticle(event)) { - console.log('Valid article event'); -} -``` - -### Highlight Helpers (NIP-84) - -**Note**: Highlight helpers moved to `applesauce-common` in v5. - -```javascript -import { - getHighlightText, - getHighlightSourceUrl, - getHighlightSourceEventPointer, - getHighlightSourceAddressPointer, - getHighlightContext, - getHighlightComment, - getHighlightAttributions -} from 'applesauce-common/helpers/highlight'; - -// All cached - no useMemo needed -const text = getHighlightText(event); -const url = getHighlightSourceUrl(event); -const eventPointer = getHighlightSourceEventPointer(event); -const addressPointer = getHighlightSourceAddressPointer(event); -const context = getHighlightContext(event); -const comment = getHighlightComment(event); -const attributions = getHighlightAttributions(event); -``` - -### Profile Helpers - -```javascript -import { - getProfileContent, - getDisplayName, - getProfilePicture, - isValidProfile -} from 'applesauce-core/helpers'; - -// Parse profile JSON (cached) -const profile = getProfileContent(profileEvent); - -// Get display name with fallback -const name = getDisplayName(profile, 'Anonymous'); - -// Get profile picture with fallback -const avatar = getProfilePicture(profile, '/default-avatar.png'); - -// Validation -if (isValidProfile(event)) { - const profile = getProfileContent(event); -} -``` - -### Pointer Helpers (NIP-19) - -```javascript -import { - parseCoordinate, - getEventPointerFromETag, - getEventPointerFromQTag, - getAddressPointerFromATag, - getProfilePointerFromPTag, - getEventPointerForEvent, - getAddressPointerForEvent -} from 'applesauce-core/helpers'; - -// Parse "a" tag coordinate (30023:pubkey:identifier) -const aTag = event.tags.find(t => t[0] === 'a')?.[1]; -const pointer = parseCoordinate(aTag); -console.log(pointer.kind, pointer.pubkey, pointer.identifier); - -// Extract pointers from tags -const eTag = event.tags.find(t => t[0] === 'e'); -const eventPointer = getEventPointerFromETag(eTag); - -const qTag = event.tags.find(t => t[0] === 'q'); -const quotePointer = getEventPointerFromQTag(qTag); - -// Create pointer from event -const pointer = getEventPointerForEvent(event, ['wss://relay.example.com']); -const address = getAddressPointerForEvent(replaceableEvent); -``` - -### Reaction Helpers - -```javascript -import { - getReactionEventPointer, - getReactionAddressPointer -} from 'applesauce-core/helpers'; - -// Get what the reaction is reacting to -const eventPointer = getReactionEventPointer(reactionEvent); -const addressPointer = getReactionAddressPointer(reactionEvent); - -if (eventPointer) { - console.log('Reacted to event:', eventPointer.id); -} -``` - -### Threading Helpers (NIP-10) - -**Note**: Threading helpers moved to `applesauce-common` in v5. - -```javascript -import { getNip10References } from 'applesauce-common/helpers/threading'; - -// Parse NIP-10 thread structure (cached) -const refs = getNip10References(event); - -if (refs.root) { - console.log('Root event:', refs.root.e); - console.log('Root address:', refs.root.a); -} - -if (refs.reply) { - console.log('Reply to event:', refs.reply.e); - console.log('Reply to address:', refs.reply.a); -} -``` - -### Filter Helpers - -```javascript -import { - isFilterEqual, - matchFilter, - matchFilters, - mergeFilters -} from 'applesauce-core/helpers'; - -// Compare filters (better than JSON.stringify) -const areEqual = isFilterEqual(filter1, filter2); - -// Check if event matches filter -const matches = matchFilter({ kinds: [1], authors: [pubkey] }, event); - -// Check against multiple filters -const matchesAny = matchFilters([filter1, filter2], event); - -// Merge filters -const combined = mergeFilters(filter1, filter2); -``` - -### Available Helper Categories - -applesauce-core provides helpers for: - -- **Tags**: getTagValue, hasNameValueTag, tag type checks -- **Articles**: getArticleTitle, getArticleSummary, getArticleImage -- **Highlights**: 7+ helpers for highlight extraction -- **Profiles**: getProfileContent, getDisplayName, getProfilePicture -- **Pointers**: parseCoordinate, pointer extraction/creation -- **Reactions**: getReactionEventPointer, getReactionAddressPointer -- **Threading**: getNip10References for NIP-10 threads -- **Comments**: getCommentReplyPointer for NIP-22 -- **Filters**: isFilterEqual, matchFilter, mergeFilters -- **Bookmarks**: bookmark list parsing -- **Emoji**: custom emoji extraction -- **Zaps**: zap parsing and validation -- **Calendars**: calendar event helpers -- **Encryption**: NIP-04, NIP-44 helpers -- **And 40+ more** - explore `node_modules/applesauce-core/dist/helpers/` - -### When NOT to Use useMemo with Helpers - -```javascript -// ❌ DON'T memoize applesauce helper calls -const title = useMemo(() => getArticleTitle(event), [event]); -const summary = useMemo(() => getArticleSummary(event), [event]); - -// ✅ DO call helpers directly -const title = getArticleTitle(event); -const summary = getArticleSummary(event); - -// ❌ DON'T memoize helpers that wrap other helpers -function getRepoName(event) { - return getTagValue(event, 'name'); -} -const name = useMemo(() => getRepoName(event), [event]); - -// ✅ DO call directly (caching propagates) -const name = getRepoName(event); - -// ✅ DO use useMemo for expensive transformations -const sorted = useMemo( - () => events.sort((a, b) => b.created_at - a.created_at), - [events] -); - -// ✅ DO use useMemo for object creation -const options = useMemo( - () => ({ fallbackRelays, timeout: 1000 }), - [fallbackRelays] -); -``` - -## EventFactory - -### Overview - -The `EventFactory` class (moved to `applesauce-core/event-factory` in v5) provides a unified API for creating and signing Nostr events using blueprints. - -```typescript -import { EventFactory } from 'applesauce-core/event-factory'; - -// Create factory -const factory = new EventFactory(); - -// Set signer (required for signing) -factory.setSigner(signer); - -// Optional context -factory.setClient({ name: 'grimoire', address: { pubkey, identifier } }); -``` - -### Creating Events with Blueprints - -Blueprints are templates for event creation. See the **applesauce-common** skill for detailed blueprint documentation. - -```typescript -import { NoteBlueprint, NoteReplyBlueprint } from 'applesauce-common/blueprints'; - -// Create a note -const draft = await factory.create( - NoteBlueprint, - 'Hello #nostr!', - { emojis: [{ shortcode: 'wave', url: 'https://...' }] } -); - -// Sign the draft -const event = await factory.sign(draft); - -// Or combine: create and sign -const event = await factory.sign( - await factory.create(NoteBlueprint, content, options) -); -``` - -### Factory Methods - -```typescript -// Create from blueprint -const draft = await factory.create(Blueprint, ...args); - -// Sign event template -const event = await factory.sign(draft); - -// Stamp (add pubkey without signing) -const unsigned = await factory.stamp(draft); - -// Build with operations -const draft = await factory.build(template, ...operations); - -// Modify existing event -const modified = await factory.modify(event, ...operations); -``` - -### Using with Actions - -EventFactory integrates seamlessly with the action system: - -```typescript -import accountManager from '@/services/accounts'; -import { EventFactory } from 'applesauce-core/event-factory'; -import { NoteBlueprint } from 'applesauce-common/blueprints'; - -const account = accountManager.active; -const signer = account.signer; - -const factory = new EventFactory(); -factory.setSigner(signer); - -// Create and sign -const draft = await factory.create(NoteBlueprint, content, options); -const event = await factory.sign(draft); - -// Publish -await pool.publish(relays, event); -``` - -### Common Patterns - -**Creating with custom tags:** -```typescript -// Let blueprint handle automatic extraction -const draft = await factory.create(NoteBlueprint, content, { emojis }); - -// Add custom tags afterward -draft.tags.push(['client', 'grimoire', '31990:...']); -draft.tags.push(['imeta', `url ${blob.url}`, `x ${blob.sha256}`]); - -// Sign -const event = await factory.sign(draft); -``` - -**Error handling:** -```typescript -try { - const draft = await factory.create(NoteBlueprint, content, options); - const event = await factory.sign(draft); - await pool.publish(relays, event); -} catch (error) { - if (error.message.includes('User rejected')) { - // Handle rejection - } else { - // Handle other errors - } -} -``` - -**Optimistic updates:** -```typescript -// Create and sign -const event = await factory.sign( - await factory.create(NoteBlueprint, content, options) -); - -// Add to local store immediately -eventStore.add(event); - -// Publish in background -pool.publish(relays, event).catch(err => { - // Remove from store on failure - eventStore.remove(event.id); -}); -``` - -## NIP Helpers - -### NIP-05 Verification - -```javascript -import { verifyNip05 } from 'applesauce-core'; - -// Verify NIP-05 -const result = await verifyNip05('alice@example.com', expectedPubkey); - -if (result.valid) { - console.log('NIP-05 verified'); -} else { - console.log('Verification failed:', result.error); -} -``` - -### NIP-10 Reply Parsing - -```javascript -import { parseReplyTags } from 'applesauce-core'; - -// Parse reply structure -const parsed = parseReplyTags(event); - -console.log('Root event:', parsed.root); -console.log('Reply to:', parsed.reply); -console.log('Mentions:', parsed.mentions); -``` - -### NIP-65 Relay Lists - -```javascript -import { parseRelayList } from 'applesauce-core'; - -// Parse relay list event (kind 10002) -const relays = parseRelayList(relayListEvent); - -console.log('Read relays:', relays.read); -console.log('Write relays:', relays.write); -``` - -## Integration with nostr-tools - -### Using with SimplePool - -```javascript -import { SimplePool } from 'nostr-tools'; -import { EventStore } from 'applesauce-core'; - -const pool = new SimplePool(); -const eventStore = new EventStore(); - -// Load events into store -pool.subscribeMany(relays, [filter], { - onevent(event) { - eventStore.add(event); - } -}); - -// Query store reactively -const timeline$ = createTimelineQuery(eventStore, filter); -``` - -### Publishing Events - -```javascript -import { finalizeEvent } from 'nostr-tools'; - -// Create event -const event = finalizeEvent({ - kind: 1, - content: 'Hello!', - created_at: Math.floor(Date.now() / 1000), - tags: [] -}, secretKey); - -// Add to local store immediately (optimistic update) -eventStore.add(event); - -// Publish to relays -await pool.publish(relays, event); -``` - -## Svelte Integration - -### Using in Svelte Components - -```svelte - - -{#each events as event} -
- {event.content} -
-{/each} -``` - -### Svelte Store Adapter - -```javascript -import { readable } from 'svelte/store'; - -// Convert RxJS observable to Svelte store -function fromObservable(observable, initialValue) { - return readable(initialValue, set => { - const subscription = observable.subscribe(set); - return () => subscription.unsubscribe(); - }); -} - -// Usage -const events$ = timeline.events$; -const eventsStore = fromObservable(events$, []); -``` - -```svelte - - -{#each $eventsStore as event} -
{event.content}
-{/each} -``` - -## Best Practices - -### Store Management - -1. **Single store instance** - Use one EventStore per app -2. **Clear stale data** - Implement cache limits -3. **Handle replaceable events** - Let store manage deduplication -4. **Unsubscribe** - Clean up subscriptions on component destroy - -### Query Optimization - -1. **Use specific filters** - Narrow queries perform better -2. **Limit results** - Use limit for initial loads -3. **Cache queries** - Reuse query instances -4. **Debounce updates** - Throttle rapid changes - -### Memory Management - -1. **Limit store size** - Implement LRU or time-based eviction -2. **Clean up observables** - Unsubscribe when done -3. **Use weak references** - For profile caches -4. **Paginate large feeds** - Don't load everything at once - -### Reactive Patterns - -1. **Prefer observables** - Over imperative queries -2. **Use operators** - Transform data with RxJS -3. **Combine streams** - For complex views -4. **Handle loading states** - Show placeholders - -## Common Patterns - -### Event Deduplication - -```javascript -// EventStore handles deduplication automatically -eventStore.add(event1); -eventStore.add(event1); // No duplicate - -// For manual deduplication -const seen = new Set(); -events.filter(e => { - if (seen.has(e.id)) return false; - seen.add(e.id); - return true; -}); -``` - -### Optimistic Updates - -```javascript -async function publishNote(content) { - // Create event - const event = await createEvent(content); - - // Add to store immediately (optimistic) - eventStore.add(event); - - try { - // Publish to relays - await pool.publish(relays, event); - } catch (error) { - // Remove on failure - eventStore.remove(event.id); - throw error; - } -} -``` - -### Loading States - -```javascript -import { BehaviorSubject, combineLatest } from 'rxjs'; - -const loading$ = new BehaviorSubject(true); -const events$ = timeline.events$; - -const state$ = combineLatest([loading$, events$]).pipe( - map(([loading, events]) => ({ - loading, - events, - empty: !loading && events.length === 0 - })) -); - -// Start loading -loading$.next(true); -await loadEvents(); -loading$.next(false); -``` - -### Infinite Scroll - -```javascript -function createInfiniteScroll(timeline, pageSize = 50) { - let loading = false; - - async function loadMore() { - if (loading) return; - - loading = true; - await timeline.loadMore(pageSize); - loading = false; - } - - function onScroll(event) { - const { scrollTop, scrollHeight, clientHeight } = event.target; - if (scrollHeight - scrollTop <= clientHeight * 1.5) { - loadMore(); - } - } - - return { loadMore, onScroll }; -} -``` - -## Troubleshooting - -### Common Issues - -**Events not updating:** -- Check subscription is active -- Verify events are being added to store -- Ensure filter matches events - -**Memory growing:** -- Implement store size limits -- Clean up subscriptions -- Use weak references where appropriate - -**Slow queries:** -- Add indexes for common queries -- Use more specific filters -- Implement pagination - -**Stale data:** -- Implement refresh mechanisms -- Set up real-time subscriptions -- Handle replaceable event updates - -## References - -- **applesauce GitHub**: https://github.com/hzrd149/applesauce -- **RxJS Documentation**: https://rxjs.dev -- **nostr-tools**: https://github.com/nbd-wtf/nostr-tools -- **Nostr Protocol**: https://github.com/nostr-protocol/nostr - -## Related Skills - -- **nostr-tools** - Lower-level Nostr operations -- **applesauce-common** - Social/NIP-specific helpers (article, highlight, threading, zap, etc.) -- **applesauce-signers** - Event signing abstractions -- **svelte** - Building reactive UIs -- **nostr** - Nostr protocol fundamentals diff --git a/.claude/skills/applesauce-signers/SKILL.md b/.claude/skills/applesauce-signers/SKILL.md deleted file mode 100644 index 40d07e6..0000000 --- a/.claude/skills/applesauce-signers/SKILL.md +++ /dev/null @@ -1,757 +0,0 @@ ---- -name: applesauce-signers -description: This skill should be used when working with applesauce-signers library for Nostr event signing, including NIP-07 browser extensions, NIP-46 remote signing, and custom signer implementations. Provides comprehensive knowledge of signing patterns and signer abstractions. ---- - -# applesauce-signers Skill - -This skill provides comprehensive knowledge and patterns for working with applesauce-signers, a library that provides signing abstractions for Nostr applications. - -## When to Use This Skill - -Use this skill when: -- Implementing event signing in Nostr applications -- Integrating with NIP-07 browser extensions -- Working with NIP-46 remote signers -- Building custom signer implementations -- Managing signing sessions -- Handling signing requests and permissions -- Implementing multi-signer support - -## Core Concepts - -### applesauce-signers Overview - -applesauce-signers provides: -- **Signer abstraction** - Unified interface for different signers -- **NIP-07 integration** - Browser extension support -- **NIP-46 support** - Remote signing (Nostr Connect) -- **Simple signers** - Direct key signing -- **Permission handling** - Manage signing requests -- **Observable patterns** - Reactive signing states - -### Installation - -```bash -npm install applesauce-signers -``` - -### Signer Interface - -All signers implement a common interface: - -```typescript -interface Signer { - // Get public key - getPublicKey(): Promise; - - // Sign event - signEvent(event: UnsignedEvent): Promise; - - // Encrypt (NIP-04) - nip04Encrypt?(pubkey: string, plaintext: string): Promise; - nip04Decrypt?(pubkey: string, ciphertext: string): Promise; - - // Encrypt (NIP-44) - nip44Encrypt?(pubkey: string, plaintext: string): Promise; - nip44Decrypt?(pubkey: string, ciphertext: string): Promise; -} -``` - -## Simple Signer - -### Using Secret Key - -```javascript -import { SimpleSigner } from 'applesauce-signers'; -import { generateSecretKey } from 'nostr-tools'; - -// Create signer with existing key -const signer = new SimpleSigner(secretKey); - -// Or generate new key -const newSecretKey = generateSecretKey(); -const newSigner = new SimpleSigner(newSecretKey); - -// Get public key -const pubkey = await signer.getPublicKey(); - -// Sign event -const unsignedEvent = { - kind: 1, - content: 'Hello Nostr!', - created_at: Math.floor(Date.now() / 1000), - tags: [] -}; - -const signedEvent = await signer.signEvent(unsignedEvent); -``` - -### NIP-04 Encryption - -```javascript -// Encrypt message -const ciphertext = await signer.nip04Encrypt( - recipientPubkey, - 'Secret message' -); - -// Decrypt message -const plaintext = await signer.nip04Decrypt( - senderPubkey, - ciphertext -); -``` - -### NIP-44 Encryption - -```javascript -// Encrypt with NIP-44 (preferred) -const ciphertext = await signer.nip44Encrypt( - recipientPubkey, - 'Secret message' -); - -// Decrypt -const plaintext = await signer.nip44Decrypt( - senderPubkey, - ciphertext -); -``` - -## NIP-07 Signer - -### Browser Extension Integration - -```javascript -import { Nip07Signer } from 'applesauce-signers'; - -// Check if extension is available -if (window.nostr) { - const signer = new Nip07Signer(); - - // Get public key (may prompt user) - const pubkey = await signer.getPublicKey(); - - // Sign event (prompts user) - const signedEvent = await signer.signEvent(unsignedEvent); -} -``` - -### Handling Extension Availability - -```javascript -function getAvailableSigner() { - if (typeof window !== 'undefined' && window.nostr) { - return new Nip07Signer(); - } - return null; -} - -// Wait for extension to load -async function waitForExtension(timeout = 3000) { - const start = Date.now(); - - while (Date.now() - start < timeout) { - if (window.nostr) { - return new Nip07Signer(); - } - await new Promise(r => setTimeout(r, 100)); - } - - return null; -} -``` - -### Extension Permissions - -```javascript -// Some extensions support granular permissions -const signer = new Nip07Signer(); - -// Request specific permissions -try { - // This varies by extension - await window.nostr.enable(); -} catch (error) { - console.log('User denied permission'); -} -``` - -## NIP-46 Remote Signer - -### Nostr Connect - -```javascript -import { Nip46Signer } from 'applesauce-signers'; - -// Create remote signer -const signer = new Nip46Signer({ - // Remote signer's pubkey - remotePubkey: signerPubkey, - - // Relays for communication - relays: ['wss://relay.example.com'], - - // Local secret key for encryption - localSecretKey: localSecretKey, - - // Optional: custom client name - clientName: 'My Nostr App' -}); - -// Connect to remote signer -await signer.connect(); - -// Get public key -const pubkey = await signer.getPublicKey(); - -// Sign event -const signedEvent = await signer.signEvent(unsignedEvent); - -// Disconnect when done -signer.disconnect(); -``` - -### Connection URL - -```javascript -// Parse nostrconnect:// URL -function parseNostrConnectUrl(url) { - const parsed = new URL(url); - - return { - pubkey: parsed.pathname.replace('//', ''), - relay: parsed.searchParams.get('relay'), - secret: parsed.searchParams.get('secret') - }; -} - -// Create signer from URL -const { pubkey, relay, secret } = parseNostrConnectUrl(connectUrl); - -const signer = new Nip46Signer({ - remotePubkey: pubkey, - relays: [relay], - localSecretKey: generateSecretKey(), - secret: secret -}); -``` - -### Bunker URL - -```javascript -// Parse bunker:// URL (NIP-46) -function parseBunkerUrl(url) { - const parsed = new URL(url); - - return { - pubkey: parsed.pathname.replace('//', ''), - relays: parsed.searchParams.getAll('relay'), - secret: parsed.searchParams.get('secret') - }; -} - -const { pubkey, relays, secret } = parseBunkerUrl(bunkerUrl); -``` - -## Signer Management - -### Signer Store - -```javascript -import { SignerStore } from 'applesauce-signers'; - -const signerStore = new SignerStore(); - -// Set active signer -signerStore.setSigner(signer); - -// Get active signer -const activeSigner = signerStore.getSigner(); - -// Clear signer (logout) -signerStore.clearSigner(); - -// Observable for signer changes -signerStore.signer$.subscribe(signer => { - if (signer) { - console.log('Logged in'); - } else { - console.log('Logged out'); - } -}); -``` - -### Multi-Account Support - -```javascript -class AccountManager { - constructor() { - this.accounts = new Map(); - this.activeAccount = null; - } - - addAccount(pubkey, signer) { - this.accounts.set(pubkey, signer); - } - - removeAccount(pubkey) { - this.accounts.delete(pubkey); - if (this.activeAccount === pubkey) { - this.activeAccount = null; - } - } - - switchAccount(pubkey) { - if (this.accounts.has(pubkey)) { - this.activeAccount = pubkey; - return this.accounts.get(pubkey); - } - return null; - } - - getActiveSigner() { - return this.activeAccount - ? this.accounts.get(this.activeAccount) - : null; - } -} -``` - -## Custom Signers - -### Implementing a Custom Signer - -```javascript -class CustomSigner { - constructor(options) { - this.options = options; - } - - async getPublicKey() { - // Return public key - return this.options.pubkey; - } - - async signEvent(event) { - // Implement signing logic - // Could call external API, hardware wallet, etc. - - const signedEvent = await this.externalSign(event); - return signedEvent; - } - - async nip04Encrypt(pubkey, plaintext) { - // Implement NIP-04 encryption - throw new Error('NIP-04 not supported'); - } - - async nip04Decrypt(pubkey, ciphertext) { - throw new Error('NIP-04 not supported'); - } - - async nip44Encrypt(pubkey, plaintext) { - // Implement NIP-44 encryption - throw new Error('NIP-44 not supported'); - } - - async nip44Decrypt(pubkey, ciphertext) { - throw new Error('NIP-44 not supported'); - } -} -``` - -### Hardware Wallet Signer - -```javascript -class HardwareWalletSigner { - constructor(devicePath) { - this.devicePath = devicePath; - } - - async connect() { - // Connect to hardware device - this.device = await connectToDevice(this.devicePath); - } - - async getPublicKey() { - // Get public key from device - return await this.device.getNostrPubkey(); - } - - async signEvent(event) { - // Sign on device (user confirms on device) - const signature = await this.device.signNostrEvent(event); - - return { - ...event, - pubkey: await this.getPublicKey(), - id: getEventHash(event), - sig: signature - }; - } -} -``` - -### Read-Only Signer - -```javascript -class ReadOnlySigner { - constructor(pubkey) { - this.pubkey = pubkey; - } - - async getPublicKey() { - return this.pubkey; - } - - async signEvent(event) { - throw new Error('Read-only mode: cannot sign events'); - } - - async nip04Encrypt(pubkey, plaintext) { - throw new Error('Read-only mode: cannot encrypt'); - } - - async nip04Decrypt(pubkey, ciphertext) { - throw new Error('Read-only mode: cannot decrypt'); - } -} -``` - -## Signing Utilities - -### Event Creation Helper - -```javascript -async function createAndSignEvent(signer, template) { - const pubkey = await signer.getPublicKey(); - - const event = { - ...template, - pubkey, - created_at: template.created_at || Math.floor(Date.now() / 1000) - }; - - return await signer.signEvent(event); -} - -// Usage -const signedNote = await createAndSignEvent(signer, { - kind: 1, - content: 'Hello!', - tags: [] -}); -``` - -### Batch Signing - -```javascript -async function signEvents(signer, events) { - const signed = []; - - for (const event of events) { - const signedEvent = await signer.signEvent(event); - signed.push(signedEvent); - } - - return signed; -} - -// With parallelization (if signer supports) -async function signEventsParallel(signer, events) { - return Promise.all( - events.map(event => signer.signEvent(event)) - ); -} -``` - -## Svelte Integration - -### Signer Context - -```svelte - - - - -``` - -```svelte - - -``` - -### Login Component - -```svelte - - -{#if $signer} - -{:else} - - -
- - -
-{/if} -``` - -## Best Practices - -### Security - -1. **Never store secret keys in plain text** - Use secure storage -2. **Prefer NIP-07** - Let extensions manage keys -3. **Clear keys on logout** - Don't leave in memory -4. **Validate before signing** - Check event content - -### User Experience - -1. **Show signing status** - Loading states -2. **Handle rejections gracefully** - User may cancel -3. **Provide fallbacks** - Multiple login options -4. **Remember preferences** - Store signer type - -### Error Handling - -```javascript -async function safeSign(signer, event) { - try { - return await signer.signEvent(event); - } catch (error) { - if (error.message.includes('rejected')) { - console.log('User rejected signing'); - return null; - } - if (error.message.includes('timeout')) { - console.log('Signing timed out'); - return null; - } - throw error; - } -} -``` - -### Permission Checking - -```javascript -function hasEncryptionSupport(signer) { - return typeof signer.nip04Encrypt === 'function' || - typeof signer.nip44Encrypt === 'function'; -} - -function getEncryptionMethod(signer) { - // Prefer NIP-44 - if (typeof signer.nip44Encrypt === 'function') { - return 'nip44'; - } - if (typeof signer.nip04Encrypt === 'function') { - return 'nip04'; - } - return null; -} -``` - -## Common Patterns - -### Signer Detection - -```javascript -async function detectSigners() { - const available = []; - - // Check NIP-07 - if (typeof window !== 'undefined' && window.nostr) { - available.push({ - type: 'nip07', - name: 'Browser Extension', - create: () => new Nip07Signer() - }); - } - - // Check stored credentials - const storedKey = localStorage.getItem('nsec'); - if (storedKey) { - available.push({ - type: 'stored', - name: 'Saved Key', - create: () => new SimpleSigner(storedKey) - }); - } - - return available; -} -``` - -### Auto-Reconnect for NIP-46 - -```javascript -class ReconnectingNip46Signer { - constructor(options) { - this.options = options; - this.signer = null; - } - - async connect() { - this.signer = new Nip46Signer(this.options); - await this.signer.connect(); - } - - async signEvent(event) { - try { - return await this.signer.signEvent(event); - } catch (error) { - if (error.message.includes('disconnected')) { - await this.connect(); - return await this.signer.signEvent(event); - } - throw error; - } - } -} -``` - -### Signer Type Persistence - -```javascript -const SIGNER_KEY = 'nostr_signer_type'; - -function saveSigner(type, data) { - localStorage.setItem(SIGNER_KEY, JSON.stringify({ type, data })); -} - -async function restoreSigner() { - const saved = localStorage.getItem(SIGNER_KEY); - if (!saved) return null; - - const { type, data } = JSON.parse(saved); - - switch (type) { - case 'nip07': - if (window.nostr) { - return new Nip07Signer(); - } - break; - case 'simple': - // Don't store secret keys! - break; - case 'nip46': - const signer = new Nip46Signer(data); - await signer.connect(); - return signer; - } - - return null; -} -``` - -## Troubleshooting - -### Common Issues - -**Extension not detected:** -- Wait for page load -- Check window.nostr exists -- Verify extension is enabled - -**Signing rejected:** -- User cancelled in extension -- Handle gracefully with error message - -**NIP-46 connection fails:** -- Check relay is accessible -- Verify remote signer is online -- Check secret matches - -**Encryption not supported:** -- Check signer has encrypt methods -- Fall back to alternative method -- Show user appropriate error - -## References - -- **applesauce GitHub**: https://github.com/hzrd149/applesauce -- **NIP-07 Specification**: https://github.com/nostr-protocol/nips/blob/master/07.md -- **NIP-46 Specification**: https://github.com/nostr-protocol/nips/blob/master/46.md -- **nostr-tools**: https://github.com/nbd-wtf/nostr-tools - -## Related Skills - -- **nostr-tools** - Event creation and signing utilities -- **applesauce-core** - Event stores and queries -- **nostr** - Nostr protocol fundamentals -- **svelte** - Building Nostr UIs diff --git a/.claude/skills/nostr-tools/SKILL.md b/.claude/skills/nostr-tools/SKILL.md deleted file mode 100644 index 905d41c..0000000 --- a/.claude/skills/nostr-tools/SKILL.md +++ /dev/null @@ -1,767 +0,0 @@ ---- -name: nostr-tools -description: This skill should be used when working with nostr-tools library for Nostr protocol operations, including event creation, signing, filtering, relay communication, and NIP implementations. Provides comprehensive knowledge of nostr-tools APIs and patterns. ---- - -# nostr-tools Skill - -This skill provides comprehensive knowledge and patterns for working with nostr-tools, the most popular JavaScript/TypeScript library for Nostr protocol development. - -## When to Use This Skill - -Use this skill when: -- Building Nostr clients or applications -- Creating and signing Nostr events -- Connecting to Nostr relays -- Implementing NIP features -- Working with Nostr keys and cryptography -- Filtering and querying events -- Building relay pools or connections -- Implementing NIP-44/NIP-04 encryption - -## Core Concepts - -### nostr-tools Overview - -nostr-tools provides: -- **Event handling** - Create, sign, verify events -- **Key management** - Generate, convert, encode keys -- **Relay communication** - Connect, subscribe, publish -- **NIP implementations** - NIP-04, NIP-05, NIP-19, NIP-44, etc. -- **Cryptographic operations** - Schnorr signatures, encryption -- **Filter building** - Query events by various criteria - -### Installation - -```bash -npm install nostr-tools -``` - -### Basic Imports - -```javascript -// Core functionality -import { - SimplePool, - generateSecretKey, - getPublicKey, - finalizeEvent, - verifyEvent -} from 'nostr-tools'; - -// NIP-specific imports -import { nip04, nip05, nip19, nip44 } from 'nostr-tools'; - -// Relay operations -import { Relay } from 'nostr-tools/relay'; -``` - -## Key Management - -### Generating Keys - -```javascript -import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'; - -// Generate new secret key (Uint8Array) -const secretKey = generateSecretKey(); - -// Derive public key -const publicKey = getPublicKey(secretKey); - -console.log('Secret key:', bytesToHex(secretKey)); -console.log('Public key:', publicKey); // hex string -``` - -### Key Encoding (NIP-19) - -```javascript -import { nip19 } from 'nostr-tools'; - -// Encode to bech32 -const nsec = nip19.nsecEncode(secretKey); -const npub = nip19.npubEncode(publicKey); -const note = nip19.noteEncode(eventId); - -console.log(nsec); // nsec1... -console.log(npub); // npub1... -console.log(note); // note1... - -// Decode from bech32 -const { type, data } = nip19.decode(npub); -// type: 'npub', data: publicKey (hex) - -// Encode profile reference (nprofile) -const nprofile = nip19.nprofileEncode({ - pubkey: publicKey, - relays: ['wss://relay.example.com'] -}); - -// Encode event reference (nevent) -const nevent = nip19.neventEncode({ - id: eventId, - relays: ['wss://relay.example.com'], - author: publicKey, - kind: 1 -}); - -// Encode address (naddr) for replaceable events -const naddr = nip19.naddrEncode({ - identifier: 'my-article', - pubkey: publicKey, - kind: 30023, - relays: ['wss://relay.example.com'] -}); -``` - -## Event Operations - -### Event Structure - -```javascript -// Unsigned event template -const eventTemplate = { - kind: 1, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: 'Hello Nostr!' -}; - -// Signed event (after finalizeEvent) -const signedEvent = { - id: '...', // 32-byte sha256 hash as hex - pubkey: '...', // 32-byte public key as hex - created_at: 1234567890, - kind: 1, - tags: [], - content: 'Hello Nostr!', - sig: '...' // 64-byte Schnorr signature as hex -}; -``` - -### Creating and Signing Events - -```javascript -import { finalizeEvent, verifyEvent } from 'nostr-tools/pure'; - -// Create event template -const eventTemplate = { - kind: 1, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['p', publicKey], // Mention - ['e', eventId, '', 'reply'], // Reply - ['t', 'nostr'] // Hashtag - ], - content: 'Hello Nostr!' -}; - -// Sign event -const signedEvent = finalizeEvent(eventTemplate, secretKey); - -// Verify event -const isValid = verifyEvent(signedEvent); -console.log('Event valid:', isValid); -``` - -### Event Kinds - -```javascript -// Common event kinds -const KINDS = { - Metadata: 0, // Profile metadata (NIP-01) - Text: 1, // Short text note (NIP-01) - RecommendRelay: 2, // Relay recommendation - Contacts: 3, // Contact list (NIP-02) - EncryptedDM: 4, // Encrypted DM (NIP-04) - EventDeletion: 5, // Delete events (NIP-09) - Repost: 6, // Repost (NIP-18) - Reaction: 7, // Reaction (NIP-25) - ChannelCreation: 40, // Channel (NIP-28) - ChannelMessage: 42, // Channel message - Zap: 9735, // Zap receipt (NIP-57) - Report: 1984, // Report (NIP-56) - RelayList: 10002, // Relay list (NIP-65) - Article: 30023, // Long-form content (NIP-23) -}; -``` - -### Creating Specific Events - -```javascript -// Profile metadata (kind 0) -const profileEvent = finalizeEvent({ - kind: 0, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: JSON.stringify({ - name: 'Alice', - about: 'Nostr enthusiast', - picture: 'https://example.com/avatar.jpg', - nip05: 'alice@example.com', - lud16: 'alice@getalby.com' - }) -}, secretKey); - -// Contact list (kind 3) -const contactsEvent = finalizeEvent({ - kind: 3, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['p', pubkey1, 'wss://relay1.com', 'alice'], - ['p', pubkey2, 'wss://relay2.com', 'bob'], - ['p', pubkey3, '', 'carol'] - ], - content: '' // Or JSON relay preferences -}, secretKey); - -// Reply to an event -const replyEvent = finalizeEvent({ - kind: 1, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['e', rootEventId, '', 'root'], - ['e', parentEventId, '', 'reply'], - ['p', parentEventPubkey] - ], - content: 'This is a reply' -}, secretKey); - -// Reaction (kind 7) -const reactionEvent = finalizeEvent({ - kind: 7, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['e', eventId], - ['p', eventPubkey] - ], - content: '+' // or '-' or emoji -}, secretKey); - -// Delete event (kind 5) -const deleteEvent = finalizeEvent({ - kind: 5, - created_at: Math.floor(Date.now() / 1000), - tags: [ - ['e', eventIdToDelete], - ['e', anotherEventIdToDelete] - ], - content: 'Deletion reason' -}, secretKey); -``` - -## Relay Communication - -### Using SimplePool - -SimplePool is the recommended way to interact with multiple relays: - -```javascript -import { SimplePool } from 'nostr-tools/pool'; - -const pool = new SimplePool(); -const relays = [ - 'wss://relay.damus.io', - 'wss://nos.lol', - 'wss://relay.nostr.band' -]; - -// Subscribe to events -const subscription = pool.subscribeMany( - relays, - [ - { - kinds: [1], - authors: [publicKey], - limit: 10 - } - ], - { - onevent(event) { - console.log('Received event:', event); - }, - oneose() { - console.log('End of stored events'); - } - } -); - -// Close subscription when done -subscription.close(); - -// Publish event to all relays -const results = await Promise.allSettled( - pool.publish(relays, signedEvent) -); - -// Query events (returns Promise) -const events = await pool.querySync(relays, { - kinds: [0], - authors: [publicKey] -}); - -// Get single event -const event = await pool.get(relays, { - ids: [eventId] -}); - -// Close pool when done -pool.close(relays); -``` - -### Direct Relay Connection - -```javascript -import { Relay } from 'nostr-tools/relay'; - -const relay = await Relay.connect('wss://relay.damus.io'); - -console.log(`Connected to ${relay.url}`); - -// Subscribe -const sub = relay.subscribe([ - { - kinds: [1], - limit: 100 - } -], { - onevent(event) { - console.log('Event:', event); - }, - oneose() { - console.log('EOSE'); - sub.close(); - } -}); - -// Publish -await relay.publish(signedEvent); - -// Close -relay.close(); -``` - -### Handling Connection States - -```javascript -import { Relay } from 'nostr-tools/relay'; - -const relay = await Relay.connect('wss://relay.example.com'); - -// Listen for disconnect -relay.onclose = () => { - console.log('Relay disconnected'); -}; - -// Check connection status -console.log('Connected:', relay.connected); -``` - -## Filters - -### Filter Structure - -```javascript -const filter = { - // Event IDs - ids: ['abc123...'], - - // Authors (pubkeys) - authors: ['pubkey1', 'pubkey2'], - - // Event kinds - kinds: [1, 6, 7], - - // Tags (single-letter keys) - '#e': ['eventId1', 'eventId2'], - '#p': ['pubkey1'], - '#t': ['nostr', 'bitcoin'], - '#d': ['article-identifier'], - - // Time range - since: 1704067200, // Unix timestamp - until: 1704153600, - - // Limit results - limit: 100, - - // Search (NIP-50, if relay supports) - search: 'nostr protocol' -}; -``` - -### Common Filter Patterns - -```javascript -// User's recent posts -const userPosts = { - kinds: [1], - authors: [userPubkey], - limit: 50 -}; - -// User's profile -const userProfile = { - kinds: [0], - authors: [userPubkey] -}; - -// User's contacts -const userContacts = { - kinds: [3], - authors: [userPubkey] -}; - -// Replies to an event -const replies = { - kinds: [1], - '#e': [eventId] -}; - -// Reactions to an event -const reactions = { - kinds: [7], - '#e': [eventId] -}; - -// Feed from followed users -const feed = { - kinds: [1, 6], - authors: followedPubkeys, - limit: 100 -}; - -// Events mentioning user -const mentions = { - kinds: [1], - '#p': [userPubkey], - limit: 50 -}; - -// Hashtag search -const hashtagEvents = { - kinds: [1], - '#t': ['bitcoin'], - limit: 100 -}; - -// Replaceable event by d-tag -const replaceableEvent = { - kinds: [30023], - authors: [authorPubkey], - '#d': ['article-slug'] -}; -``` - -### Multiple Filters - -```javascript -// Subscribe with multiple filters (OR logic) -const filters = [ - { kinds: [1], authors: [userPubkey], limit: 20 }, - { kinds: [1], '#p': [userPubkey], limit: 20 } -]; - -pool.subscribeMany(relays, filters, { - onevent(event) { - // Receives events matching ANY filter - } -}); -``` - -## Encryption - -### NIP-04 (Legacy DMs) - -```javascript -import { nip04 } from 'nostr-tools'; - -// Encrypt message -const ciphertext = await nip04.encrypt( - secretKey, - recipientPubkey, - 'Hello, this is secret!' -); - -// Create encrypted DM event -const dmEvent = finalizeEvent({ - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', recipientPubkey]], - content: ciphertext -}, secretKey); - -// Decrypt message -const plaintext = await nip04.decrypt( - secretKey, - senderPubkey, - ciphertext -); -``` - -### NIP-44 (Modern Encryption) - -```javascript -import { nip44 } from 'nostr-tools'; - -// Get conversation key (cache this for multiple messages) -const conversationKey = nip44.getConversationKey( - secretKey, - recipientPubkey -); - -// Encrypt -const ciphertext = nip44.encrypt( - 'Hello with NIP-44!', - conversationKey -); - -// Decrypt -const plaintext = nip44.decrypt( - ciphertext, - conversationKey -); -``` - -## NIP Implementations - -### NIP-05 (DNS Identifier) - -```javascript -import { nip05 } from 'nostr-tools'; - -// Query NIP-05 identifier -const profile = await nip05.queryProfile('alice@example.com'); - -if (profile) { - console.log('Pubkey:', profile.pubkey); - console.log('Relays:', profile.relays); -} - -// Verify NIP-05 for a pubkey -const isValid = await nip05.queryProfile('alice@example.com') - .then(p => p?.pubkey === expectedPubkey); -``` - -### NIP-10 (Reply Threading) - -```javascript -import { nip10 } from 'nostr-tools'; - -// Parse reply tags -const parsed = nip10.parse(event); - -console.log('Root:', parsed.root); // Original event -console.log('Reply:', parsed.reply); // Direct parent -console.log('Mentions:', parsed.mentions); // Other mentions -console.log('Profiles:', parsed.profiles); // Mentioned pubkeys -``` - -### NIP-21 (nostr: URIs) - -```javascript -// Parse nostr: URIs -const uri = 'nostr:npub1...'; -const { type, data } = nip19.decode(uri.replace('nostr:', '')); -``` - -### NIP-27 (Content References) - -```javascript -// Parse nostr:npub and nostr:note references in content -const content = 'Check out nostr:npub1abc... and nostr:note1xyz...'; - -const references = content.match(/nostr:(n[a-z]+1[a-z0-9]+)/g); -references?.forEach(ref => { - const decoded = nip19.decode(ref.replace('nostr:', '')); - console.log(decoded.type, decoded.data); -}); -``` - -### NIP-57 (Zaps) - -```javascript -import { nip57 } from 'nostr-tools'; - -// Validate zap receipt -const zapReceipt = await pool.get(relays, { - kinds: [9735], - '#e': [eventId] -}); - -const validatedZap = await nip57.validateZapRequest(zapReceipt); -``` - -## Utilities - -### Hex and Bytes Conversion - -```javascript -import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; - -// Convert secret key to hex -const secretKeyHex = bytesToHex(secretKey); - -// Convert hex back to bytes -const secretKeyBytes = hexToBytes(secretKeyHex); -``` - -### Event ID Calculation - -```javascript -import { getEventHash } from 'nostr-tools/pure'; - -// Calculate event ID without signing -const eventId = getEventHash(unsignedEvent); -``` - -### Signature Operations - -```javascript -import { - getSignature, - verifyEvent -} from 'nostr-tools/pure'; - -// Sign event data -const signature = getSignature(unsignedEvent, secretKey); - -// Verify complete event -const isValid = verifyEvent(signedEvent); -``` - -## Best Practices - -### Connection Management - -1. **Use SimplePool** - Manages connections efficiently -2. **Limit concurrent connections** - Don't connect to too many relays -3. **Handle disconnections** - Implement reconnection logic -4. **Close subscriptions** - Always close when done - -### Event Handling - -1. **Verify events** - Always verify signatures -2. **Deduplicate** - Events may come from multiple relays -3. **Handle replaceable events** - Latest by created_at wins -4. **Validate content** - Don't trust event content blindly - -### Key Security - -1. **Never expose secret keys** - Keep in secure storage -2. **Use NIP-07 in browsers** - Let extensions handle signing -3. **Validate input** - Check key formats before use - -### Performance - -1. **Cache events** - Avoid re-fetching -2. **Use filters wisely** - Be specific, use limits -3. **Batch operations** - Combine related queries -4. **Close idle connections** - Free up resources - -## Common Patterns - -### Building a Feed - -```javascript -const pool = new SimplePool(); -const relays = ['wss://relay.damus.io', 'wss://nos.lol']; - -async function loadFeed(followedPubkeys) { - const events = await pool.querySync(relays, { - kinds: [1, 6], - authors: followedPubkeys, - limit: 100 - }); - - // Sort by timestamp - return events.sort((a, b) => b.created_at - a.created_at); -} -``` - -### Real-time Updates - -```javascript -function subscribeToFeed(followedPubkeys, onEvent) { - return pool.subscribeMany( - relays, - [{ kinds: [1, 6], authors: followedPubkeys }], - { - onevent: onEvent, - oneose() { - console.log('Caught up with stored events'); - } - } - ); -} -``` - -### Profile Loading - -```javascript -async function loadProfile(pubkey) { - const [metadata] = await pool.querySync(relays, { - kinds: [0], - authors: [pubkey], - limit: 1 - }); - - if (metadata) { - return JSON.parse(metadata.content); - } - return null; -} -``` - -### Event Deduplication - -```javascript -const seenEvents = new Set(); - -function handleEvent(event) { - if (seenEvents.has(event.id)) { - return; // Skip duplicate - } - seenEvents.add(event.id); - - // Process event... -} -``` - -## Troubleshooting - -### Common Issues - -**Events not publishing:** -- Check relay is writable -- Verify event is properly signed -- Check relay's accepted kinds - -**Subscription not receiving events:** -- Verify filter syntax -- Check relay has matching events -- Ensure subscription isn't closed - -**Signature verification fails:** -- Check event structure is correct -- Verify keys are in correct format -- Ensure event hasn't been modified - -**NIP-05 lookup fails:** -- Check CORS headers on server -- Verify .well-known path is correct -- Handle network timeouts - -## References - -- **nostr-tools GitHub**: https://github.com/nbd-wtf/nostr-tools -- **Nostr Protocol**: https://github.com/nostr-protocol/nostr -- **NIPs Repository**: https://github.com/nostr-protocol/nips -- **NIP-01 (Basic Protocol)**: https://github.com/nostr-protocol/nips/blob/master/01.md - -## Related Skills - -- **nostr** - Nostr protocol fundamentals -- **svelte** - Building Nostr UIs with Svelte -- **applesauce-core** - Higher-level Nostr client utilities -- **applesauce-signers** - Nostr signing abstractions diff --git a/.claude/skills/nostr/README.md b/.claude/skills/nostr/README.md deleted file mode 100644 index 6806b77..0000000 --- a/.claude/skills/nostr/README.md +++ /dev/null @@ -1,162 +0,0 @@ -# Nostr Protocol Skill - -A comprehensive Claude skill for working with the Nostr protocol and implementing Nostr clients and relays. - -## Overview - -This skill provides expert-level knowledge of the Nostr protocol, including: -- Complete NIP (Nostr Implementation Possibilities) reference -- Event structure and cryptographic operations -- Client-relay WebSocket communication -- Event kinds and their behaviors -- Best practices and common pitfalls - -## Contents - -### SKILL.md -The main skill file containing: -- Core protocol concepts -- Event structure and signing -- WebSocket communication patterns -- Cryptographic operations -- Common implementation patterns -- Quick reference guides - -### Reference Files - -#### references/nips-overview.md -Comprehensive documentation of all standard NIPs including: -- Core protocol NIPs (NIP-01, NIP-02, etc.) -- Social features (reactions, reposts, channels) -- Identity and discovery (NIP-05, NIP-65) -- Security and privacy (NIP-44, NIP-42) -- Lightning integration (NIP-47, NIP-57) -- Advanced features - -#### references/event-kinds.md -Complete reference for all Nostr event kinds: -- Core events (0-999) -- Regular events (1000-9999) -- Replaceable events (10000-19999) -- Ephemeral events (20000-29999) -- Parameterized replaceable events (30000-39999) -- Event lifecycle behaviors -- Common patterns and examples - -#### references/common-mistakes.md -Detailed guide on implementation pitfalls: -- Event creation and signing errors -- WebSocket communication issues -- Filter query problems -- Threading mistakes -- Relay management errors -- Security vulnerabilities -- UX considerations -- Testing strategies - -## When to Use - -Use this skill when: -- Implementing Nostr clients or relays -- Working with Nostr events and messages -- Handling cryptographic signatures and keys -- Implementing any NIP -- Building social features on Nostr -- Debugging Nostr applications -- Discussing Nostr protocol architecture - -## Key Features - -### Complete NIP Coverage -All standard NIPs documented with: -- Purpose and status -- Implementation details -- Code examples -- Usage patterns -- Interoperability notes - -### Cryptographic Operations -Detailed guidance on: -- Event signing with Schnorr signatures -- Event ID calculation -- Signature verification -- Key management (BIP-39, NIP-06) -- Encryption (NIP-04, NIP-44) - -### WebSocket Protocol -Complete reference for: -- Message types (EVENT, REQ, CLOSE, OK, EOSE, etc.) -- Filter queries and optimization -- Subscription management -- Connection handling -- Error handling - -### Event Lifecycle -Understanding of: -- Regular events (immutable) -- Replaceable events (latest only) -- Ephemeral events (real-time only) -- Parameterized replaceable events (by identifier) - -### Best Practices -Comprehensive guidance on: -- Multi-relay architecture -- NIP-65 relay lists -- Event caching -- Optimistic UI -- Security considerations -- Performance optimization - -## Quick Start Examples - -### Publishing a Note -```javascript -const event = { - pubkey: userPublicKey, - created_at: Math.floor(Date.now() / 1000), - kind: 1, - tags: [], - content: "Hello Nostr!" -} -event.id = calculateId(event) -event.sig = signEvent(event, privateKey) -ws.send(JSON.stringify(["EVENT", event])) -``` - -### Subscribing to Events -```javascript -const filter = { - kinds: [1], - authors: [followedPubkey], - limit: 50 -} -ws.send(JSON.stringify(["REQ", "sub-id", filter])) -``` - -### Replying to a Note -```javascript -const reply = { - kind: 1, - tags: [ - ["e", originalEventId, "", "root"], - ["p", originalAuthorPubkey] - ], - content: "Great post!" -} -``` - -## Official Resources - -- **NIPs Repository**: https://github.com/nostr-protocol/nips -- **Nostr Website**: https://nostr.com -- **Nostr Documentation**: https://nostr.how -- **NIP Status**: https://nostr-nips.com - -## Skill Maintenance - -This skill is based on the official Nostr NIPs repository. As new NIPs are proposed and implemented, this skill should be updated to reflect the latest standards and best practices. - -## License - -Based on public Nostr protocol specifications (MIT License). - diff --git a/.claude/skills/nostr/SKILL.md b/.claude/skills/nostr/SKILL.md index 6499097..f3e71e6 100644 --- a/.claude/skills/nostr/SKILL.md +++ b/.claude/skills/nostr/SKILL.md @@ -1,449 +1,283 @@ --- name: nostr -description: This skill should be used when working with the Nostr protocol, implementing Nostr clients or relays, handling Nostr events, or discussing Nostr Implementation Possibilities (NIPs). Provides comprehensive knowledge of Nostr's decentralized protocol, event structure, cryptographic operations, and all standard NIPs. +description: Use this skill whenever working with the Nostr protocol — event creation, signing, filtering, relay communication, NIP implementations, key encoding (NIP-19), encryption (NIP-04/NIP-44), or nostr-tools library APIs. Activate for any code importing nostr-tools, handling Nostr events, or discussing NIPs. --- -# Nostr Protocol Expert +# Nostr Protocol & nostr-tools -## Purpose +Comprehensive reference for the Nostr protocol and the nostr-tools JavaScript library. -This skill provides expert-level assistance with the Nostr protocol, a simple, open protocol for global, decentralized, and censorship-resistant social networks. The protocol is built on relays and cryptographic keys, enabling direct peer-to-peer communication without central servers. +## Protocol Fundamentals -## When to Use +### Events -Activate this skill when: -- Implementing Nostr clients or relays -- Working with Nostr events and messages -- Handling cryptographic signatures and keys (schnorr signatures on secp256k1) -- Implementing any Nostr Implementation Possibility (NIP) -- Building social networking features on Nostr -- Querying or filtering Nostr events -- Discussing Nostr protocol architecture -- Implementing WebSocket communication with relays - -## Core Concepts - -### The Protocol Foundation - -Nostr operates on two main components: -1. **Clients** - Applications users run to read/write data -2. **Relays** - Servers that store and forward messages - -Key principles: -- Everyone runs a client -- Anyone can run a relay -- Users identified by public keys -- Messages signed with private keys -- No central authority or trusted servers - -### Events Structure - -All data in Nostr is represented as events. An event is a JSON object with this structure: +All data in Nostr is an event — a signed JSON object: ```json { - "id": "<32-bytes lowercase hex-encoded sha256 of the serialized event data>", - "pubkey": "<32-bytes lowercase hex-encoded public key of the event creator>", + "id": "<32-byte hex SHA256 of serialized event>", + "pubkey": "<32-byte hex public key>", "created_at": "", - "kind": "", - "tags": [ - ["", "", "", "..."] - ], - "content": "", - "sig": "<64-bytes lowercase hex of the schnorr signature of the sha256 hash of the serialized event data>" + "kind": "", + "tags": [["", "", "..."]], + "content": "", + "sig": "<64-byte hex schnorr signature of id>" } ``` -### Event Kinds +**ID calculation**: `SHA256(JSON.stringify([0, pubkey, created_at, kind, tags, content]))` — compact JSON, no spaces. -Standard event kinds (from various NIPs): -- `0` - Metadata (user profile) -- `1` - Text note (short post) -- `2` - Recommend relay -- `3` - Contacts (following list) -- `4` - Encrypted direct messages -- `5` - Event deletion -- `6` - Repost -- `7` - Reaction (like, emoji reaction) -- `40` - Channel creation -- `41` - Channel metadata -- `42` - Channel message -- `43` - Channel hide message -- `44` - Channel mute user -- `1000-9999` - Regular events -- `10000-19999` - Replaceable events -- `20000-29999` - Ephemeral events -- `30000-39999` - Parameterized replaceable events +**Signature**: Schnorr (BIP-340) on secp256k1, signing the 32-byte event ID. + +### Event Kind Ranges + +| Range | Type | Behavior | +|-------|------|----------| +| 0-999 | Core | Varies per kind | +| 1000-9999 | Regular | Immutable, all kept | +| 10000-19999 | Replaceable | Latest per pubkey+kind wins | +| 20000-29999 | Ephemeral | Not stored, forwarded only | +| 30000-39999 | Parameterized Replaceable | Latest per pubkey+kind+d-tag wins | + +Common kinds: `0` metadata, `1` text note, `3` contacts, `5` deletion, `6` repost, `7` reaction, `9` group message (NIP-29), `9735` zap receipt, `10002` relay list, `30023` article. + +See **references/event-kinds.md** for the full list. ### Tags -Common tag types: -- `["e", "", "", ""]` - Reference to an event -- `["p", "", ""]` - Reference to a user -- `["a", "::", ""]` - Reference to a replaceable event -- `["d", ""]` - Identifier for parameterized replaceable events -- `["r", ""]` - Reference/link to a web resource -- `["t", ""]` - Hashtag -- `["g", ""]` - Geolocation -- `["nonce", "", ""]` - Proof of work -- `["subject", ""]` - Subject/title -- `["client", ""]` - Client application used - -## Key NIPs Reference - -For detailed specifications, refer to **references/nips-overview.md**. - -### Core Protocol NIPs - -#### NIP-01: Basic Protocol Flow -The foundation of Nostr. Defines: -- Event structure and validation -- Event ID calculation (SHA256 of serialized event) -- Signature verification (schnorr signatures) -- Client-relay communication via WebSocket -- Message types: EVENT, REQ, CLOSE, EOSE, OK, NOTICE - -#### NIP-02: Contact List and Petnames -Event kind `3` for following lists: -- Each `p` tag represents a followed user -- Optional relay URL and petname in tag -- Replaceable event (latest overwrites) - -#### NIP-04: Encrypted Direct Messages -Event kind `4` for private messages: -- Content encrypted with shared secret (ECDH) -- `p` tag for recipient pubkey -- Deprecated in favor of NIP-44 - -#### NIP-05: Mapping Nostr Keys to DNS -Internet identifier format: `name@domain.com` -- `.well-known/nostr.json` endpoint -- Maps names to pubkeys -- Optional relay list - -#### NIP-09: Event Deletion -Event kind `5` to request deletion: -- Contains `e` tags for events to delete -- Relays should delete referenced events -- Only works for own events - -#### NIP-10: Text Note References (Threads) -Conventions for `e` and `p` tags in replies: -- Root event reference -- Reply event reference -- Mentions -- Marker types: "root", "reply", "mention" - -#### NIP-11: Relay Information Document -HTTP endpoint for relay metadata: -- GET request to relay URL -- Returns JSON with relay information -- Supported NIPs, software, limitations - -### Social Features NIPs - -#### NIP-25: Reactions -Event kind `7` for reactions: -- Content usually "+" (like) or emoji -- `e` tag for reacted event -- `p` tag for event author - -#### NIP-42: Authentication -Client authentication to relays: -- AUTH message from relay -- Client responds with event kind `22242` -- Proves key ownership - -#### NIP-50: Search -Query filter extension for full-text search: -- `search` field in REQ filters -- Implementation-defined behavior - -### Advanced NIPs - -#### NIP-19: bech32-encoded Entities -Human-readable identifiers: -- `npub`: public key -- `nsec`: private key (sensitive!) -- `note`: note/event ID -- `nprofile`: profile with relay hints -- `nevent`: event with relay hints -- `naddr`: replaceable event coordinate - -#### NIP-44: Encrypted Payloads -Improved encryption for direct messages: -- Versioned encryption scheme -- Better security than NIP-04 -- ChaCha20-Poly1305 AEAD - -#### NIP-65: Relay List Metadata -Event kind `10002` for relay lists: -- Read/write relay preferences -- Optimizes relay discovery -- Replaceable event - -## Client-Relay Communication - -### WebSocket Messages - -#### From Client to Relay - -**EVENT** - Publish an event: -```json -["EVENT", ] +``` +["e", "", "", ""] — event reference +["p", "", ""] — profile reference +["a", "::", ""] — replaceable event ref +["d", ""] — parameterized replaceable ID +["t", ""] — hashtag +["r", ""] — web resource / relay ``` -**REQ** - Request events (subscription): -```json -["REQ", , , , ...] -``` +NIP-10 markers for threading: `"root"`, `"reply"`, `"mention"`. -**CLOSE** - Stop a subscription: -```json -["CLOSE", ] -``` +### Client-Relay Communication (WebSocket) -**AUTH** - Respond to auth challenge: -```json -["AUTH", ] -``` +**Client sends**: `["EVENT", ]`, `["REQ", , , ...]`, `["CLOSE", ]`, `["AUTH", ]` -#### From Relay to Client +**Relay sends**: `["EVENT", , ]`, `["OK", , , ]`, `["EOSE", ]`, `["CLOSED", , ]`, `["NOTICE", ]`, `["AUTH", ]` -**EVENT** - Send event to client: -```json -["EVENT", , ] -``` - -**OK** - Acceptance/rejection notice: -```json -["OK", , , ] -``` - -**EOSE** - End of stored events: -```json -["EOSE", ] -``` - -**CLOSED** - Subscription closed: -```json -["CLOSED", , ] -``` - -**NOTICE** - Human-readable message: -```json -["NOTICE", ] -``` - -**AUTH** - Authentication challenge: -```json -["AUTH", ] -``` - -### Filter Objects - -Filters select events in REQ messages: +### Filters ```json { - "ids": ["", ...], - "authors": ["", ...], - "kinds": [, ...], - "#e": ["", ...], - "#p": ["", ...], - "#a": ["", ...], - "#t": ["", ...], - "since": , - "until": , - "limit": + "ids": [""], + "authors": [""], + "kinds": [1, 7], + "#e": [""], + "#p": [""], + "#t": [""], + "since": 1704067200, + "until": 1704153600, + "limit": 50, + "search": "query" } ``` -Filtering rules: -- Arrays are ORed together -- Different fields are ANDed -- Tag filters: `#` matches tag values -- Prefix matching allowed for `ids` and `authors` +- Array fields are OR'd; different fields are AND'd +- `ids` and `authors` support prefix matching +- Always set reasonable `limit` (50-500) -## Cryptographic Operations +## nostr-tools Library -### Key Management +### Key Management & Encoding (NIP-19) -- **Private Key**: 32-byte random value, keep secure -- **Public Key**: Derived via secp256k1 -- **Encoding**: Hex (lowercase) or bech32 +```typescript +import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'; +import { nip19 } from 'nostr-tools'; -### Event Signing (schnorr) +const sk = generateSecretKey(); // Uint8Array +const pk = getPublicKey(sk); // hex string -Steps to create a signed event: -1. Set all fields except `id` and `sig` -2. Serialize event data to JSON (specific order) -3. Calculate SHA256 hash → `id` -4. Sign `id` with schnorr signature → `sig` +// Encode +const npub = nip19.npubEncode(pk); // npub1... +const nsec = nip19.nsecEncode(sk); // nsec1... +const note = nip19.noteEncode(eventId); // note1... -Serialization format for ID calculation: -```json -[ - 0, - , - , - , - , - -] +const nprofile = nip19.nprofileEncode({ pubkey: pk, relays: ['wss://...'] }); +const nevent = nip19.neventEncode({ id, relays, author, kind }); +const naddr = nip19.naddrEncode({ identifier, pubkey, kind, relays }); + +// Decode +const { type, data } = nip19.decode(npub); // type: 'npub', data: hex ``` -### Event Verification +### Creating & Signing Events -Steps to verify an event: -1. Verify ID matches SHA256 of serialized data -2. Verify signature is valid schnorr signature -3. Check created_at is reasonable (not far future) -4. Validate event structure and required fields +```typescript +import { finalizeEvent, verifyEvent } from 'nostr-tools/pure'; -## Implementation Best Practices +const event = finalizeEvent({ + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [['t', 'nostr']], + content: 'Hello Nostr!' +}, secretKey); -### For Clients +const valid = verifyEvent(event); // true/false +``` -1. **Connect to Multiple Relays**: Don't rely on single relay -2. **Cache Events**: Reduce redundant relay queries -3. **Verify Signatures**: Always verify event signatures -4. **Handle Replaceable Events**: Keep only latest version -5. **Respect User Privacy**: Careful with sensitive data -6. **Implement NIP-65**: Use user's preferred relays -7. **Proper Error Handling**: Handle relay disconnections -8. **Pagination**: Use `limit`, `since`, `until` for queries +### Relay Communication (SimplePool) -### For Relays +```typescript +import { SimplePool } from 'nostr-tools/pool'; -1. **Validate Events**: Check signatures, IDs, structure -2. **Rate Limiting**: Prevent spam and abuse -3. **Storage Management**: Ephemeral events, retention policies -4. **Implement NIP-11**: Provide relay information -5. **WebSocket Optimization**: Handle many connections -6. **Filter Optimization**: Efficient event querying -7. **Consider NIP-42**: Authentication for write access -8. **Performance**: Index by pubkey, kind, tags, timestamp +const pool = new SimplePool(); +const relays = ['wss://relay.damus.io', 'wss://nos.lol']; -### Security Considerations +// Subscribe +const sub = pool.subscribeMany(relays, [filter], { + onevent(event) { /* handle event */ }, + oneose() { /* end of stored events */ } +}); +sub.close(); // always close when done -1. **Never Expose Private Keys**: Handle nsec carefully -2. **Validate All Input**: Prevent injection attacks -3. **Use NIP-44**: For encrypted messages (not NIP-04) -4. **Check Event Timestamps**: Reject far-future events -5. **Implement Proof of Work**: NIP-13 for spam prevention -6. **Sanitize Content**: XSS prevention in displayed content -7. **Relay Trust**: Don't trust single relay for critical data +// Query (returns Promise) +const events = await pool.querySync(relays, filter); +const event = await pool.get(relays, { ids: [eventId] }); + +// Publish +await Promise.allSettled(pool.publish(relays, signedEvent)); + +// Cleanup +pool.close(relays); +``` + +### Direct Relay Connection + +```typescript +import { Relay } from 'nostr-tools/relay'; + +const relay = await Relay.connect('wss://relay.damus.io'); + +const sub = relay.subscribe([filter], { + onevent(event) { /* ... */ }, + oneose() { sub.close(); } +}); + +await relay.publish(signedEvent); +relay.close(); +``` + +### Encryption + +```typescript +// NIP-44 (preferred) +import { nip44 } from 'nostr-tools'; + +const conversationKey = nip44.getConversationKey(secretKey, recipientPubkey); +const ciphertext = nip44.encrypt('Hello!', conversationKey); +const plaintext = nip44.decrypt(ciphertext, conversationKey); + +// NIP-04 (legacy, avoid for new code) +import { nip04 } from 'nostr-tools'; + +const ct = await nip04.encrypt(secretKey, recipientPubkey, 'Hello!'); +const pt = await nip04.decrypt(secretKey, senderPubkey, ct); +``` + +### NIP-05 DNS Identifiers + +```typescript +import { nip05 } from 'nostr-tools'; + +const profile = await nip05.queryProfile('alice@example.com'); +// { pubkey: '...', relays: ['wss://...'] } +``` + +### NIP-10 Thread Parsing + +```typescript +import { nip10 } from 'nostr-tools'; + +const parsed = nip10.parse(event); +// { root, reply, mentions, profiles } +``` + +## Key NIPs Reference + +| NIP | Topic | Key Points | +|-----|-------|------------| +| 01 | Basic protocol | Event structure, WebSocket messages, filters | +| 02 | Contacts | Kind 3, `p` tags for following list | +| 05 | DNS identifiers | `name@domain` via `.well-known/nostr.json` | +| 09 | Deletion | Kind 5, `e` tags for events to delete | +| 10 | Threading | `e` tag markers: root, reply, mention | +| 11 | Relay info | HTTP GET relay URL for NIP-11 document | +| 19 | bech32 entities | npub, nsec, note, nprofile, nevent, naddr | +| 25 | Reactions | Kind 7, content "+" or emoji | +| 42 | Auth | Relay challenges, kind 22242 response | +| 44 | Encryption | ChaCha20-Poly1305 AEAD (replaces NIP-04) | +| 50 | Search | `search` field in filters | +| 57 | Zaps | Kind 9734 request, 9735 receipt | +| 65 | Relay list | Kind 10002, read/write relay preferences | + +See **references/nips-overview.md** for comprehensive NIP documentation. ## Common Patterns -### Publishing a Note +### Reply with NIP-10 threading -```javascript -const event = { - pubkey: userPublicKey, +```typescript +const reply = finalizeEvent({ + kind: 1, created_at: Math.floor(Date.now() / 1000), - kind: 1, - tags: [], - content: "Hello Nostr!", -} -// Calculate ID and sign -event.id = calculateId(event) -event.sig = signEvent(event, privateKey) -// Publish to relay -ws.send(JSON.stringify(["EVENT", event])) -``` - -### Subscribing to Notes - -```javascript -const filter = { - kinds: [1], - authors: [followedPubkey1, followedPubkey2], - limit: 50 -} -ws.send(JSON.stringify(["REQ", "my-sub", filter])) -``` - -### Replying to a Note - -```javascript -const reply = { - kind: 1, tags: [ - ["e", originalEventId, relayUrl, "root"], - ["p", originalAuthorPubkey] + ['e', rootEventId, '', 'root'], + ['e', parentEventId, '', 'reply'], + ['p', parentAuthorPubkey] ], - content: "Great post!", - // ... other fields + content: 'Great post!' +}, secretKey); +``` + +### NIP-65 relay list + +```typescript +// Fetch user's relay preferences +const [relayList] = await pool.querySync(relays, { + kinds: [10002], authors: [pubkey] +}); + +const readRelays = [], writeRelays = []; +for (const [tag, url, mode] of relayList.tags) { + if (tag !== 'r') continue; + if (!mode || mode === 'read') readRelays.push(url); + if (!mode || mode === 'write') writeRelays.push(url); } ``` -### Reacting to a Note +### Reconnection with backoff -```javascript -const reaction = { - kind: 7, - tags: [ - ["e", eventId], - ["p", eventAuthorPubkey] - ], - content: "+", // or emoji - // ... other fields -} +```typescript +let delay = 1000; +const connect = () => { + const ws = new WebSocket(relayUrl); + ws.onopen = () => { delay = 1000; resubscribe(); }; + ws.onclose = () => { + setTimeout(connect, delay); + delay = Math.min(delay * 2, 30000); + }; +}; ``` -## Development Resources +## Security Essentials -### Essential NIPs for Beginners +- **Never expose nsec** in code, logs, or network requests +- **Always verify signatures** before trusting event data +- **Use NIP-44** for encryption (NIP-04 is deprecated) +- **Sanitize content** before rendering (XSS prevention) +- **Prefer NIP-07** (browser extensions) over handling raw keys -Start with these NIPs in order: -1. **NIP-01** - Basic protocol (MUST read) -2. **NIP-19** - Bech32 identifiers -3. **NIP-02** - Following lists -4. **NIP-10** - Threaded conversations -5. **NIP-25** - Reactions -6. **NIP-65** - Relay lists - -### Testing and Development - -- **Relay Implementations**: nostream, strfry, relay.py -- **Test Relays**: wss://relay.damus.io, wss://nos.lol -- **Libraries**: nostr-tools (JS), rust-nostr (Rust), python-nostr (Python) -- **Development Tools**: NostrDebug, Nostr Army Knife, nostril -- **Reference Clients**: Damus (iOS), Amethyst (Android), Snort (Web) - -### Key Repositories - -- **NIPs Repository**: https://github.com/nostr-protocol/nips -- **Awesome Nostr**: https://github.com/aljazceru/awesome-nostr -- **Nostr Resources**: https://nostr.how +See **references/common-mistakes.md** for a comprehensive list of pitfalls. ## Reference Files -For comprehensive NIP details, see: -- **references/nips-overview.md** - Detailed descriptions of all standard NIPs -- **references/event-kinds.md** - Complete event kinds reference -- **references/common-mistakes.md** - Pitfalls and how to avoid them - -## Quick Checklist - -When implementing Nostr: -- [ ] Events have all required fields (id, pubkey, created_at, kind, tags, content, sig) -- [ ] Event IDs calculated correctly (SHA256 of serialization) -- [ ] Signatures verified (schnorr on secp256k1) -- [ ] WebSocket messages properly formatted -- [ ] Filter queries optimized with appropriate limits -- [ ] Handling replaceable events correctly -- [ ] Connected to multiple relays for redundancy -- [ ] Following relevant NIPs for features implemented -- [ ] Private keys never exposed or transmitted -- [ ] Event timestamps validated - -## Official Resources - -- **NIPs Repository**: https://github.com/nostr-protocol/nips -- **Nostr Website**: https://nostr.com -- **Nostr Documentation**: https://nostr.how -- **NIP Status**: https://nostr-nips.com - +- **references/nips-overview.md** — Detailed descriptions of all standard NIPs +- **references/event-kinds.md** — Complete event kinds reference with examples +- **references/common-mistakes.md** — 30 common implementation mistakes and fixes diff --git a/.claude/skills/react/README.md b/.claude/skills/react/README.md deleted file mode 100644 index 9144da8..0000000 --- a/.claude/skills/react/README.md +++ /dev/null @@ -1,119 +0,0 @@ -# React 19 Skill - -A comprehensive Claude skill for working with React 19, including hooks, components, server components, and modern React architecture. - -## Contents - -### Main Skill File -- **SKILL.md** - Main skill document with React 19 fundamentals, hooks, components, and best practices - -### References -- **hooks-quick-reference.md** - Quick reference for all React hooks with examples -- **server-components.md** - Complete guide to React Server Components and Server Functions -- **performance.md** - Performance optimization strategies and techniques - -### Examples -- **practical-patterns.tsx** - Real-world React patterns and solutions - -## What This Skill Covers - -### Core Topics -- React 19 features and improvements -- All built-in hooks (useState, useEffect, useTransition, useOptimistic, etc.) -- Component patterns and composition -- Server Components and Server Functions -- React Compiler and automatic optimization -- Performance optimization techniques -- Form handling and validation -- Error boundaries and error handling -- Context and global state management -- Code splitting and lazy loading - -### Best Practices -- Component design principles -- State management strategies -- Performance optimization -- Error handling patterns -- TypeScript integration -- Testing considerations -- Accessibility guidelines - -## When to Use This Skill - -Use this skill when: -- Building React 19 applications -- Working with React hooks -- Implementing server components -- Optimizing React performance -- Troubleshooting React-specific issues -- Understanding concurrent features -- Working with forms and user input -- Implementing complex UI patterns - -## Quick Start Examples - -### Basic Component -```typescript -interface ButtonProps { - label: string - onClick: () => void -} - -const Button = ({ label, onClick }: ButtonProps) => { - return -} -``` - -### Using Hooks -```typescript -const Counter = () => { - const [count, setCount] = useState(0) - - useEffect(() => { - console.log(`Count is: ${count}`) - }, [count]) - - return ( - - ) -} -``` - -### Server Component -```typescript -const Page = async () => { - const data = await fetchData() - return
{data}
-} -``` - -### Server Function -```typescript -'use server' - -export async function createUser(formData: FormData) { - const name = formData.get('name') - return await db.user.create({ data: { name } }) -} -``` - -## Related Skills - -- **typescript** - TypeScript patterns for React -- **ndk** - Nostr integration with React -- **skill-creator** - Creating reusable component libraries - -## Resources - -- [React Documentation](https://react.dev) -- [React API Reference](https://react.dev/reference/react) -- [React Hooks Reference](https://react.dev/reference/react/hooks) -- [React Server Components](https://react.dev/reference/rsc) -- [React Compiler](https://react.dev/reference/react-compiler) - -## Version - -This skill is based on React 19.2 and includes the latest features and APIs. - diff --git a/.claude/skills/react/SKILL.md b/.claude/skills/react/SKILL.md deleted file mode 100644 index abe826f..0000000 --- a/.claude/skills/react/SKILL.md +++ /dev/null @@ -1,1026 +0,0 @@ ---- -name: react -description: This skill should be used when working with React 19, including hooks, components, server components, concurrent features, and React DOM APIs. Provides comprehensive knowledge of React patterns, best practices, and modern React architecture. ---- - -# React 19 Skill - -This skill provides comprehensive knowledge and patterns for working with React 19 effectively in modern applications. - -## When to Use This Skill - -Use this skill when: -- Building React applications with React 19 features -- Working with React hooks and component patterns -- Implementing server components and server functions -- Using concurrent features and transitions -- Optimizing React application performance -- Troubleshooting React-specific issues -- Working with React DOM APIs and client/server rendering -- Using React Compiler features - -## Core Concepts - -### React 19 Overview - -React 19 introduces significant improvements: -- **Server Components** - Components that render on the server -- **Server Functions** - Functions that run on the server from client code -- **Concurrent Features** - Better performance with concurrent rendering -- **React Compiler** - Automatic memoization and optimization -- **Form Actions** - Built-in form handling with useActionState -- **Improved Hooks** - New hooks like useOptimistic, useActionState -- **Better Hydration** - Improved SSR and hydration performance - -### Component Fundamentals - -Use functional components with hooks: - -```typescript -// Functional component with props interface -interface ButtonProps { - label: string - onClick: () => void - variant?: 'primary' | 'secondary' -} - -const Button = ({ label, onClick, variant = 'primary' }: ButtonProps) => { - return ( - - ) -} -``` - -**Key Principles:** -- Use functional components over class components -- Define prop interfaces in TypeScript -- Use destructuring for props -- Provide default values for optional props -- Keep components focused and composable - -## React Hooks Reference - -### State Hooks - -#### useState -Manage local component state: - -```typescript -const [count, setCount] = useState(0) -const [user, setUser] = useState(null) - -// Named return variables pattern -const handleIncrement = () => { - setCount(prev => prev + 1) // Functional update -} - -// Update object state immutably -setUser(prev => prev ? { ...prev, name: 'New Name' } : null) -``` - -#### useReducer -Manage complex state with reducer pattern: - -```typescript -type State = { count: number; status: 'idle' | 'loading' } -type Action = - | { type: 'increment' } - | { type: 'decrement' } - | { type: 'setStatus'; status: State['status'] } - -const reducer = (state: State, action: Action): State => { - switch (action.type) { - case 'increment': - return { ...state, count: state.count + 1 } - case 'decrement': - return { ...state, count: state.count - 1 } - case 'setStatus': - return { ...state, status: action.status } - default: - return state - } -} - -const [state, dispatch] = useReducer(reducer, { count: 0, status: 'idle' }) -``` - -#### useActionState -Handle form actions with pending states (React 19): - -```typescript -const [state, formAction, isPending] = useActionState( - async (previousState: FormState, formData: FormData) => { - const name = formData.get('name') as string - - // Server action or async operation - const result = await saveUser({ name }) - - return { success: true, data: result } - }, - { success: false, data: null } -) - -return ( -
- - -
-) -``` - -### Effect Hooks - -#### useEffect -Run side effects after render: - -```typescript -// Named return variables preferred -useEffect(() => { - const controller = new AbortController() - - const fetchData = async () => { - const response = await fetch('/api/data', { - signal: controller.signal - }) - const data = await response.json() - setData(data) - } - - fetchData() - - // Cleanup function - return () => { - controller.abort() - } -}, [dependencies]) // Dependencies array -``` - -**Key Points:** -- Always return cleanup function for subscriptions -- Use dependency array correctly to avoid infinite loops -- Don't forget to handle race conditions with AbortController -- Effects run after paint, not during render - -#### useLayoutEffect -Run effects synchronously after DOM mutations but before paint: - -```typescript -useLayoutEffect(() => { - // Measure DOM nodes - const height = ref.current?.getBoundingClientRect().height - setHeight(height) -}, []) -``` - -Use when you need to: -- Measure DOM layout -- Synchronously re-render before browser paints -- Prevent visual flicker - -#### useInsertionEffect -Insert styles before any DOM reads (for CSS-in-JS libraries): - -```typescript -useInsertionEffect(() => { - const style = document.createElement('style') - style.textContent = '.my-class { color: red; }' - document.head.appendChild(style) - - return () => { - document.head.removeChild(style) - } -}, []) -``` - -### Performance Hooks - -#### useMemo -Memoize expensive calculations: - -```typescript -const expensiveValue = useMemo(() => { - return computeExpensiveValue(a, b) -}, [a, b]) -``` - -**When to use:** -- Expensive calculations that would slow down renders -- Creating stable object references for dependency arrays -- Optimizing child component re-renders - -**When NOT to use:** -- Simple calculations (overhead not worth it) -- Values that change frequently - -#### useCallback -Memoize callback functions: - -```typescript -const handleClick = useCallback(() => { - console.log('Clicked', value) -}, [value]) - -// Pass to child that uses memo - -``` - -**Use when:** -- Passing callbacks to optimized child components -- Function is a dependency in another hook -- Function is used in effect cleanup - -### Ref Hooks - -#### useRef -Store mutable values that don't trigger re-renders: - -```typescript -// DOM reference -const inputRef = useRef(null) - -useEffect(() => { - inputRef.current?.focus() -}, []) - -// Mutable value storage -const countRef = useRef(0) -countRef.current += 1 // Doesn't trigger re-render -``` - -#### useImperativeHandle -Customize ref handle for parent components: - -```typescript -interface InputHandle { - focus: () => void - clear: () => void -} - -const CustomInput = forwardRef((props, ref) => { - const inputRef = useRef(null) - - useImperativeHandle(ref, () => ({ - focus: () => { - inputRef.current?.focus() - }, - clear: () => { - if (inputRef.current) { - inputRef.current.value = '' - } - } - })) - - return -}) -``` - -### Context Hooks - -#### useContext -Access context values: - -```typescript -// Create context -interface ThemeContext { - theme: 'light' | 'dark' - toggleTheme: () => void -} - -const ThemeContext = createContext(null) - -// Provider -const ThemeProvider = ({ children }: { children: React.ReactNode }) => { - const [theme, setTheme] = useState<'light' | 'dark'>('light') - - const toggleTheme = useCallback(() => { - setTheme(prev => prev === 'light' ? 'dark' : 'light') - }, []) - - return ( - - {children} - - ) -} - -// Consumer -const ThemedButton = () => { - const context = useContext(ThemeContext) - if (!context) throw new Error('useTheme must be used within ThemeProvider') - - const { theme, toggleTheme } = context - - return ( - - ) -} -``` - -### Transition Hooks - -#### useTransition -Mark state updates as non-urgent: - -```typescript -const [isPending, startTransition] = useTransition() - -const handleTabChange = (newTab: string) => { - startTransition(() => { - setTab(newTab) // Non-urgent update - }) -} - -return ( - <> - - {isPending && } - - -) -``` - -**Use for:** -- Marking expensive updates as non-urgent -- Keeping UI responsive during state transitions -- Preventing loading states for quick updates - -#### useDeferredValue -Defer re-rendering for non-urgent updates: - -```typescript -const [query, setQuery] = useState('') -const deferredQuery = useDeferredValue(query) - -// Use deferred value for expensive rendering -const results = useMemo(() => { - return searchResults(deferredQuery) -}, [deferredQuery]) - -return ( - <> - setQuery(e.target.value)} /> - - -) -``` - -### Optimistic Updates - -#### useOptimistic -Show optimistic state while async operation completes (React 19): - -```typescript -const [optimisticMessages, addOptimisticMessage] = useOptimistic( - messages, - (state, newMessage: string) => [ - ...state, - { id: 'temp', text: newMessage, pending: true } - ] -) - -const handleSend = async (formData: FormData) => { - const message = formData.get('message') as string - - // Show optimistic update immediately - addOptimisticMessage(message) - - // Send to server - await sendMessage(message) -} - -return ( - <> - {optimisticMessages.map(msg => ( -
- {msg.text} -
- ))} -
- - -
- -) -``` - -### Other Hooks - -#### useId -Generate unique IDs for accessibility: - -```typescript -const id = useId() - -return ( - <> - - - -) -``` - -#### useSyncExternalStore -Subscribe to external stores: - -```typescript -const subscribe = (callback: () => void) => { - store.subscribe(callback) - return () => store.unsubscribe(callback) -} - -const getSnapshot = () => store.getState() -const getServerSnapshot = () => store.getInitialState() - -const state = useSyncExternalStore( - subscribe, - getSnapshot, - getServerSnapshot -) -``` - -#### useDebugValue -Display custom label in React DevTools: - -```typescript -const useCustomHook = (value: string) => { - useDebugValue(value ? `Active: ${value}` : 'Inactive') - return value -} -``` - -## React Components - -### Fragment -Group elements without extra DOM nodes: - -```typescript -// Short syntax -<> - - - - -// Full syntax (when you need key prop) - -
{item.term}
-
{item.description}
-
-``` - -### Suspense -Show fallback while loading: - -```typescript -}> - - - -// With error boundary -}> - }> - - - -``` - -### StrictMode -Enable additional checks in development: - -```typescript - - - -``` - -**StrictMode checks:** -- Warns about deprecated APIs -- Detects unexpected side effects -- Highlights potential problems -- Double-invokes functions to catch bugs - -### Profiler -Measure rendering performance: - -```typescript - - - - -const onRender = ( - id: string, - phase: 'mount' | 'update', - actualDuration: number, - baseDuration: number, - startTime: number, - commitTime: number -) => { - console.log(`${id} took ${actualDuration}ms`) -} -``` - -## React APIs - -### memo -Prevent unnecessary re-renders: - -```typescript -const ExpensiveComponent = memo(({ data }: Props) => { - return
{data}
-}, (prevProps, nextProps) => { - // Return true if props are equal (skip render) - return prevProps.data === nextProps.data -}) -``` - -### lazy -Code-split components: - -```typescript -const Dashboard = lazy(() => import('./Dashboard')) - -}> - - -``` - -### startTransition -Mark updates as transitions imperatively: - -```typescript -startTransition(() => { - setTab('profile') -}) -``` - -### cache (React Server Components) -Cache function results per request: - -```typescript -const getUser = cache(async (id: string) => { - return await db.user.findUnique({ where: { id } }) -}) -``` - -### use (React 19) -Read context or promises in render: - -```typescript -// Read context -const theme = use(ThemeContext) - -// Read promise (must be wrapped in Suspense) -const data = use(fetchDataPromise) -``` - -## Server Components & Server Functions - -### Server Components - -Components that run only on the server: - -```typescript -// app/page.tsx (Server Component by default) -const Page = async () => { - // Can fetch data directly - const posts = await db.post.findMany() - - return ( -
- {posts.map(post => ( - - ))} -
- ) -} - -export default Page -``` - -**Benefits:** -- Direct database access -- Zero bundle size for server-only code -- Automatic code splitting -- Better performance - -### Server Functions - -Functions that run on server, callable from client: - -```typescript -'use server' - -export async function createPost(formData: FormData) { - const title = formData.get('title') as string - const content = formData.get('content') as string - - const post = await db.post.create({ - data: { title, content } - }) - - revalidatePath('/posts') - return post -} -``` - -**Usage from client:** - -```typescript -'use client' - -import { createPost } from './actions' - -const PostForm = () => { - const [state, formAction] = useActionState(createPost, null) - - return ( -
- -