diff --git a/.gitignore b/.gitignore index 7b8ff453d..5af3936ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ **/node_modules temp +.idea **/build **/dist **/coverage @@ -21,32 +22,15 @@ package-lock.json *.tgz .DS_Store .turbo -.idea bun.lockb _local_ .svelte-kit/ .pnpm-store -docs/.vitepress/cache -docs/.vitepress/dist .ngit **/.repomix-output.txt cursor-tools.config.json coverage -# Generated docs directories -/docs/getting-started/ -/docs/internals/ -/docs/migration/ -/docs/tutorial/ -/docs/api-examples.md -/docs/index.md -/docs/snippets/ -/docs/cache/ -/docs/mobile/ -/docs/wallet/ -/docs/wrappers/ -/docs/hooks/ -**/docs/api/ cache-sqlite/example/cache.db # TENEX project files @@ -60,4 +44,3 @@ svelte/examples/basic-feed/public/ .test-sessions.json **/baseline-timing.ts **/current-timing.ts -.playwright-mcp diff --git a/biome.json b/biome.json index d2bfeb2ff..d41e5cf5c 100644 --- a/biome.json +++ b/biome.json @@ -29,7 +29,7 @@ } }, "files": { - "includes": ["**/*.{js,ts,jsx,tsx,json}"], + "includes": ["**/*.{js,ts,jsx,tsx,json}", "!/core/docs/snippets"], "ignoreUnknown": true }, "vcs": { diff --git a/docs/error-handling.md b/blossom/docs/error-handling.md similarity index 100% rename from docs/error-handling.md rename to blossom/docs/error-handling.md diff --git a/docs/getting-started.md b/blossom/docs/getting-started.md similarity index 100% rename from docs/getting-started.md rename to blossom/docs/getting-started.md diff --git a/docs/mirroring.md b/blossom/docs/mirroring.md similarity index 100% rename from docs/mirroring.md rename to blossom/docs/mirroring.md diff --git a/docs/optimization.md b/blossom/docs/optimization.md similarity index 100% rename from docs/optimization.md rename to blossom/docs/optimization.md diff --git a/cache-dexie/README.md b/cache-dexie/README.md index 7a1d45e61..b0af13f04 100644 --- a/cache-dexie/README.md +++ b/cache-dexie/README.md @@ -22,7 +22,3 @@ const ndk = new NDK({cacheAdapter: dexieAdapter, ...other config options}); ``` 🚨 Because Dexie only exists client-side, this cache adapter will not work in pure node.js environments. You'll need to make sure that you're using the right cache adapter in the right place (e.g. Redis on the backend, Dexie on the frontend). - -# License - -MIT diff --git a/cache-sqlite-wasm/docs/INDEX.md b/cache-sqlite-wasm/README.md similarity index 100% rename from cache-sqlite-wasm/docs/INDEX.md rename to cache-sqlite-wasm/README.md diff --git a/core/.prettierignore b/core/.prettierignore deleted file mode 100644 index 1d6407e51..000000000 --- a/core/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -dist -docs -lib -coverage -**/.changeset -**/.svelte-kit -docs-styles.css \ No newline at end of file diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index bcbf2cedf..17e2d5af9 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -465,6 +465,35 @@ ## 2.15.0 +I'm happy to announce the release of NDK 2.15. This is a very significant release that's been in the works for quite a +while. The main focus of this release has been in reducing the amount of footguns in NDK. Yes, mainly thinking about +making the vibecoding scene a lot more enjoyable: LLMs are great at churning out code at insane speed. Not so much at +debugging, optimizations, performant code. So this new release of NDK focuses a lot on making sure that the most obvious +solution is the right solution. + +ndk-hooks + +I spun out a new package called ndk-hooks. Someone else had built (and abandoned) an ndk-react package, and LLMs +constantly got confused using it and produced terrible code. ndk-hooks is an offshoot of ndk-mobile, and, if you're +vibecoding Next.js apps like most people are doing, ndk-hooks provides you with all the building blocks to make sure +your agents don't lose their minds. + +ndk-core + +A lot has changed under the hood to make space for performant access points, particularly synchronous access points. + +Worthy mentions: + +- The whole codebase has been migrated to use modern tooling: bun and vitest. +- Very useful test helpers, until now, writing tests on clients was a pain, NDK now ships with @nostr-dev-kit/ndk/test + which provides a `MockRelay` that provides access to very useful testing utilities. +- Race condition bugs are now gone, no more dreaded "Not enough relays received this event" when publishing before + connecting to relays. + +ndk-sqlite-wasm-cache-adapter + +A new cache adapter leveraging SQLite WASM is now available. This should be the preferred option to + ### Minor Changes - Implement dynamic subscription relay refresh for outbox model diff --git a/core/OUTBOX.md b/core/OUTBOX.md deleted file mode 100644 index b029e38df..000000000 --- a/core/OUTBOX.md +++ /dev/null @@ -1,7 +0,0 @@ -# Outbox model - -NDK defines a set of seeding relays, these are relays that will be exclusively used to request Outbox model events. These are kept in a separate pool. - -NDK automatically fetches gossip information for users when they are included in an `authors` filter enough times or when they are explicitly scored with the right value. - -When a filter users the `authors` field diff --git a/core/RELEASE-NOTES.md b/core/RELEASE-NOTES.md deleted file mode 100644 index df8fc84dd..000000000 --- a/core/RELEASE-NOTES.md +++ /dev/null @@ -1,21 +0,0 @@ -# NDK 2.15 - -I'm happy to announce the release of NDK 2.15. This is a very significant release that's been in the works for quite a while. The main focus of this release has been in reducing the amount of footguns in NDK. Yes, mainly thinking about making the vibecoding scene a lot more enjoyable: LLMs are great at churning out code at insane speed. Not so much at debugging, optimizations, performant code. So this new release of NDK focuses a lot on making sure that the most obvious solution is the right solution. - -ndk-hooks - -I spun out a new package called ndk-hooks. Someone else had built (and abandoned) an ndk-react package, and LLMs constantly got confused using it and produced terrible code. ndk-hooks is an offshoot of ndk-mobile, and, if you're vibecoding Next.js apps like most people are doing, ndk-hooks provides you with all the building blocks to make sure your agents don't lose their minds. - -ndk-core - -A lot has changed under the hood to make space for performant access points, particularly synchronous access points. - -Worthy mentions: - -- The whole codebase has been migrated to use modern tooling: bun and vitest. -- Very useful test helpers, until now, writing tests on clients was a pain, NDK now ships with @nostr-dev-kit/ndk/test which provides a `MockRelay` that provides access to very useful testing utilities. -- Race condition bugs are now gone, no more dreaded "Not enough relays received this event" when publishing before connecting to relays. - -ndk-sqlite-wasm-cache-adapter - -A new cache adapter leveraging SQLite WASM is now available. This should be the preferred option to diff --git a/core/SIG-SAMPLING.md b/core/SIG-SAMPLING.md deleted file mode 100644 index de83418c2..000000000 --- a/core/SIG-SAMPLING.md +++ /dev/null @@ -1,714 +0,0 @@ -# Signature Verification Sampling Implementation Plan - -## Overview - -This document outlines the implementation plan for adding signature verification sampling to NDK. The goal is to optimize performance by reducing the number of signature verifications while maintaining security. - -Currently, NDK verifies every signature it encounters. This is computationally expensive and unnecessary - we only need to verify a sample of signatures from each relay to determine trustworthiness. Once a relay sends an invalid signature, it can be marked as untrustworthy. - -Relays must always send valid signatures, a single failure means the relay is evil. - -All invalid-signature detections—whether synchronous or asynchronous—will delegate to a new `NDK.reportInvalidSignature(event, relay)` method. This centralizes emission of the `event:invalid-sig` (with relay context) and supports optional auto-blacklisting of malicious relays. - -## Current Architecture Analysis - -### Key Components - -1. **NDK Class** (`ndk-core/src/ndk/index.ts`): - - Contains configuration properties: - - `initialValidationRatio`: Starting validation ratio for new relays - - `lowestValidationRatio`: Minimum validation ratio for any relay - - `validationRatioFn`: Optional function to calculate validation ratio - - Emits `event:invalid-sig` events when invalid signatures are detected - -2. **NDKRelay Class** (`ndk-core/src/relay/index.ts`): - - Tracks validated and non-validated event counts - - Has methods to add validated/non-validated events - - Has `shouldValidateEvent` method (implementation needs to be enhanced) - -3. **Signature Verification** (`ndk-core/src/events/signature.ts`): - - Contains verification logic - - Maintains `verifiedSignatures` map to track already verified event IDs - -4. **NDKSubscription Class** (`ndk-core/src/subscription/index.ts`): - - Receives events from relays - - Calls verification methods on events - - Can check already verified signatures - -5. **Test Utilities** (`ndk-test-utils/src/index.ts`): - - Provides mocks and helpers for testing - - Includes `RelayMock`, `RelayPoolMock`, and `EventGenerator` - - Offers `TestFixture` and time control utilities - -## Implementation Plan - -### 1. Enhance NDKRelay Class - -#### 1.1. Add Validation Statistics Tracking - -```typescript -class NDKRelay { - // Existing properties - private validatedCount = 0; - private nonValidatedCount = 0; - private currentValidationRatio: number; - - constructor(url: string, authPolicy?: NDKAuthPolicy, ndk?: NDK) { - // Existing constructor code - this.currentValidationRatio = ndk?.initialValidationRatio || 1.0; - } - - public addValidatedEvent(): void { - this.validatedCount++; - this.updateValidationRatio(); - } - - public addNonValidatedEvent(): void { - this.nonValidatedCount++; - } - - private updateValidationRatio(): void { - if (!this.ndk) return; - - // Use custom function if provided - if (this.ndk.validationRatioFn) { - this.currentValidationRatio = this.ndk.validationRatioFn( - this, - this.validatedCount, - this.nonValidatedCount, - ); - return; - } - - // Default ratio calculation: - // Gradually decrease ratio based on number of validated events - // But never go below lowestValidationRatio - const newRatio = Math.max( - this.ndk.lowestValidationRatio, - this.ndk.initialValidationRatio * Math.exp(-0.01 * this.validatedCount), - ); - - this.currentValidationRatio = newRatio; - } - - public shouldValidateEvent(): boolean { - if (!this.ndk) return true; - - // Always validate if ratio is 1.0 - if (this.currentValidationRatio >= 1.0) return true; - - // Otherwise, randomly decide based on ratio - return Math.random() < this.currentValidationRatio; - } -} -``` - -### 2. Enhance Signature Verification Process - -#### 2.1. Update NDKSubscription's `eventReceived` Method - -Current method in `ndk-core/src/subscription/index.ts` needs modifications: - -```typescript -public eventReceived( - event: NDKEvent | NostrEvent, - relay: NDKRelay | undefined, - fromCache = false, - optimisticPublish = false, -) { - const eventId = event.id! as NDKEventId; - const eventAlreadySeen = this.eventFirstSeen.has(eventId); - let ndkEvent: NDKEvent; - - if (event instanceof NDKEvent) ndkEvent = event; - - if (!eventAlreadySeen) { - // generate the ndkEvent - ndkEvent ??= new NDKEvent(this.ndk, event); - ndkEvent.ndk = this.ndk; - ndkEvent.relay = relay; - - // Skip validation for cached or self-published events - if (!fromCache && !optimisticPublish) { - // Validate event structure - if (!this.skipValidation) { - if (!ndkEvent.isValid) { - this.debug("Event failed validation %s from relay %s", eventId, relay?.url); - return; - } - } - - // Verify signature with sampling - if (relay) { - // Check if we need to verify this event based on sampling - const shouldVerify = relay.shouldValidateEvent(); - - if (shouldVerify && !this.skipVerification) { - // Attempt verification - if (!ndkEvent.verifySignature(true) && !this.ndk.asyncSigVerification) { - this.debug("Event failed signature validation", event); - // Report the invalid signature with relay information through the centralized method - this.ndk.reportInvalidSignature(ndkEvent, relay); - return; - } - - // Track successful validation - relay.addValidatedEvent(); - } else { - // We skipped verification for this event - relay.addNonValidatedEvent(); - } - } - - // Cache the event if appropriate - if (this.ndk.cacheAdapter && !this.opts.dontSaveToCache) { - this.ndk.cacheAdapter.setEvent(ndkEvent, this.filters, relay); - } - } - - // Emit the event - if (!optimisticPublish || this.skipOptimisticPublishEvent !== true) { - this.emitEvent(this.opts?.wrap ?? false, ndkEvent, relay, fromCache, optimisticPublish); - // Mark as seen - this.eventFirstSeen.set(eventId, Date.now()); - } - } else { - // Handle duplicate events (existing code) - const timeSinceFirstSeen = Date.now() - (this.eventFirstSeen.get(eventId) || 0); - this.emit("event:dup", event, relay, timeSinceFirstSeen, this, fromCache, optimisticPublish); - - if (relay) { - // Check if we've already verified this event id's signature - const signature = verifiedSignatures.get(eventId); - if (signature && typeof signature === "string") { - // If signatures match, we count it as validated - if (event.sig === signature) { - relay.addValidatedEvent(); - } else { - // Signatures don't match - this is a malicious relay! - // One invalid signature means the relay is considered evil - this.ndk.reportInvalidSignature(ndkEvent || new NDKEvent(this.ndk, event), relay); - } - } - } - } - - this.lastEventReceivedAt = Date.now(); -} -``` - -### 3. Centralize Invalid Signature Reporting in NDK Class - -#### 3.1. Add Central Reporting Method - -In `ndk-core/src/ndk/index.ts`, add a centralized method for reporting invalid signatures: - -```typescript -export class NDK extends EventEmitter<{ - // Existing events - "signer:ready": (signer: NDKSigner) => void; - "signer:required": () => void; - - // Updated event to include the relay parameter - "event:invalid-sig": (event: NDKEvent, relay: NDKRelay) => void; - - "event:publish-failed": ( - event: NDKEvent, - error: NDKPublishError, - relays: WebSocket["url"][], - ) => void; -}> { - // Existing properties and methods - - /** - * Centralized method to report an invalid signature, identifying the relay that provided it. - * A single invalid signature means the relay is considered malicious. - * All invalid signature detections (synchronous or asynchronous) should delegate to this method. - * - * @param event The event with an invalid signature - * @param relay The relay that provided the invalid signature - */ - public reportInvalidSignature(event: NDKEvent, relay: NDKRelay): void { - this.debug(`Invalid signature detected from relay ${relay.url} for event ${event.id}`); - - // Emit event with relay information - this.emit("event:invalid-sig", event, relay); - - // If auto-blacklisting is enabled, add the relay to the blacklist - if (this.autoBlacklistInvalidRelays) { - this.blacklistRelay(relay.url); - } - } - - /** - * Add a relay URL to the blacklist as it has been identified as malicious - */ - public blacklistRelay(url: string): void { - if (!this.blacklistRelayUrls) { - this.blacklistRelayUrls = []; - } - - if (!this.blacklistRelayUrls.includes(url)) { - this.blacklistRelayUrls.push(url); - this.debug(`Added relay to blacklist: ${url}`); - - // Disconnect from this relay if connected - const relay = this.pool.getRelay(url, false, false); - if (relay) { - relay.disconnect(); - this.debug(`Disconnected from blacklisted relay: ${url}`); - } - } - } -} -``` - -### 4. Update Async Signature Verification - -In `ndk-core/src/events/signature.ts`, modify the worker message handler to use the centralized reporting: - -```typescript -function initSignatureVerification(worker: Worker) { - // ... existing code ... - - worker.onmessage = (e) => { - const { id, valid } = e.data; - const callback = callbacks.get(id); - - if (callback) { - callbacks.delete(id); - - // Get the stored event and relay information - const { event, relay, ndk } = eventContext.get(id) || {}; - eventContext.delete(id); - - if (valid) { - verifiedSignatures.set(event.id, event.sig); - callback(true); - relay?.addValidatedEvent(); - } else { - callback(false); - // If invalid, report through the centralized method - if (event && relay && ndk) { - ndk.reportInvalidSignature(event, relay); - } - } - } - }; -} -``` - -### 5. Default Validation Ratio Function - -#### 5.1. Implement a Default Algorithm - -This would be part of the NDK class constructor in `ndk-core/src/ndk/index.ts`: - -```typescript -public constructor(opts: NDKConstructorParams = {}) { - // Existing constructor code - - this.initialValidationRatio = opts.initialValidationRatio || 1.0; - this.lowestValidationRatio = opts.lowestValidationRatio || 0.1; - this.autoBlacklistInvalidRelays = opts.autoBlacklistInvalidRelays || false; - - // Set a default validation ratio function if none is provided - this.validationRatioFn = opts.validationRatioFn || this.defaultValidationRatioFn; -} - -/** - * Default function to calculate validation ratio based on historical validation results. - * The more events validated successfully, the lower the ratio goes (down to the minimum). - */ -private defaultValidationRatioFn(relay: NDKRelay, validatedCount: number, nonValidatedCount: number): number { - if (validatedCount < 10) return this.initialValidationRatio; - - // Calculate a logarithmically decreasing ratio that approaches the minimum - // as more events are validated - const totalEvents = validatedCount + nonValidatedCount; - const trustFactor = Math.min(validatedCount / 100, 1); // Caps at 100 validated events - - const calculatedRatio = this.initialValidationRatio * - (1 - trustFactor) + - this.lowestValidationRatio * trustFactor; - - return Math.max(calculatedRatio, this.lowestValidationRatio); -} -``` - -### 6. Integration Points - -#### 6.1. Update NDKConstructorParams Interface - -In `ndk-core/src/ndk/index.ts`, update: - -```typescript -export interface NDKConstructorParams { - // Existing parameters - - /** - * The signature verification validation ratio for new relays. - * A value of 1.0 means verify all signatures, 0.5 means verify half, etc. - * @default 1.0 - */ - initialValidationRatio?: number; - - /** - * The lowest validation ratio any single relay can have. - * Relays will have a sample of events verified based on this ratio. - * When using this, you MUST listen for event:invalid-sig events - * to handle invalid signatures and disconnect from evil relays. - * - * @default 0.1 - */ - lowestValidationRatio?: number; - - /** - * A function that is invoked to calculate the validation ratio for a relay. - * If not provided, a default algorithm will be used. - */ - validationRatioFn?: NDKValidationRatioFn; - - /** - * When true, automatically blacklist relays that provide events with invalid signatures. - * A single invalid signature is enough to mark a relay as malicious. - * @default false - */ - autoBlacklistInvalidRelays?: boolean; -} -``` - -### 7. Documentation Updates - -#### 7.1. Add Documentation to README.md and API Reference - -For example: - -````markdown -## Signature Verification Sampling - -NDK includes support for signature verification sampling to improve performance while maintaining security. - -### Security Model - -The security model is based on the principle that **all relays must always send valid signatures**. A single invalid signature is sufficient evidence that a relay is malicious and should be blacklisted. - -By using signature sampling, we can significantly reduce computational overhead while maintaining this security model. As a relay proves trustworthy by consistently providing valid signatures, we reduce the sampling rate, checking fewer signatures over time, down to a configurable minimum ratio. - -If at any point an invalid signature is detected, the relay is immediately reported through the centralized `reportInvalidSignature` method, which emits an `event:invalid-sig` event and optionally blacklists the relay. - -### Configuration - -```typescript -const ndk = new NDK({ - // Verify 100% of signatures from new relays - initialValidationRatio: 1.0, - - // Eventually drop to verifying only 10% of signatures from trusted relays - lowestValidationRatio: 0.1, - - // Optional custom function to determine validation ratio - validationRatioFn: (relay, validatedCount, nonValidatedCount) => { - // Custom logic to determine ratio - return Math.max(0.1, 1.0 - validatedCount / 1000); - }, - - // Automatically blacklist relays that send invalid signatures - autoBlacklistInvalidRelays: true, -}); - -// Listen for invalid signature events -ndk.on("event:invalid-sig", (event, relay) => { - console.log(`Relay ${relay.url} sent an event with invalid signature: ${event.id}`); - // Custom handling... -}); -``` -```` - -```` - -## Implementation Steps - -1. **First Phase: Core Implementation** - - Enhance NDKRelay to track validation statistics - - Implement `shouldValidateEvent()` method logic - - Add `updateValidationRatio()` method - - Implement default ratio calculation algorithm - -2. **Second Phase: Centralized Invalid Signature Handling** - - Implement the centralized `reportInvalidSignature` method in NDK - - Update `event:invalid-sig` event to include relay information - - Modify async signature verification to use centralized reporting - -3. **Third Phase: Integration with Existing Code** - - Update NDKSubscription's `eventReceived` method to use sampling - - Wire up the blacklisting functionality - - Update NDK constructor and interfaces - -4. **Fourth Phase: Testing** - - Create unit tests for ratio calculation - - Test integration with different ratio configurations - - Verify behavior with intentionally invalid signatures - -5. **Fifth Phase: Documentation and Examples** - - Update README and API documentation - - Create examples for different use cases - -## Testing Plan - -### Utilizing NDK Test Utilities - -The `ndk-test-utils` package provides several useful tools for testing our implementation: - -1. **RelayMock**: We'll use this to simulate relays sending both valid and invalid signatures -2. **EventGenerator**: Helps create test events with controlled properties -3. **TestFixture**: Provides a complete test environment with mock relays and events -4. **TimeController**: Useful for testing time-dependent behavior in our ratio calculation - -### Unit Tests - -1. **Validation Ratio Calculation** - ```typescript - import { TestFixture, EventGenerator } from "@nostr-dev-kit/ndk/test"; - - test('validation ratio decreases with successful validations', () => { - const fixture = new TestFixture(); - const ndk = fixture.ndk; - ndk.initialValidationRatio = 1.0; - ndk.lowestValidationRatio = 0.1; - - const relay = new NDKRelay('wss://example.com', undefined, ndk); - - // Initial ratio should be 1.0 - expect(relay.shouldValidateEvent()).toBe(true); - - // Add 100 validated events using EventGenerator - const eventGenerator = new EventGenerator(); - const events = eventGenerator.generateEvents(100); // Generate 100 valid events - - // Simulate validation - for (const event of events) { - relay.addValidatedEvent(); - } - - // Ratio should decrease but still be probabilistic - // Run multiple checks to verify the ratio is roughly as expected - let validationCount = 0; - for (let i = 0; i < 1000; i++) { - if (relay.shouldValidateEvent()) validationCount++; - } - - // With 100 validated events, we expect the ratio to be lower than initial - // but still above the minimum - expect(validationCount).toBeGreaterThan(100); // should be more than minimum - expect(validationCount).toBeLessThan(900); // should be less than initial - }); -```` - -2. **Custom Validation Function** - - ```typescript - import { TestFixture } from "@nostr-dev-kit/ndk/test"; - - test("custom validation function is applied", () => { - // Creating a custom function that always returns 0.5 - const customFn = () => 0.5; - - const fixture = new TestFixture({ - ndkOptions: { - initialValidationRatio: 1.0, - lowestValidationRatio: 0.1, - validationRatioFn: customFn, - }, - }); - - const relay = new NDKRelay("wss://example.com", undefined, fixture.ndk); - - // Validate multiple times to check probability is ~0.5 - let validationCount = 0; - for (let i = 0; i < 1000; i++) { - if (relay.shouldValidateEvent()) validationCount++; - } - - // Should be roughly 50% - expect(validationCount).toBeGreaterThan(400); - expect(validationCount).toBeLessThan(600); - }); - ``` - -### Integration Tests - -1. **Invalid Signature Detection** - - ```typescript - import { RelayMock, EventGenerator } from "@nostr-dev-kit/ndk/test"; - - test("detects and reports invalid signatures", async () => { - // Create NDK instance with test configuration - const ndk = new NDK({ initialValidationRatio: 1.0 }); - - // Create a mock relay - const mockRelay = new RelayMock(ndk, { url: "wss://mock.com" }); - - // Spy on reportInvalidSignature - const reportSpy = jest.spyOn(ndk, "reportInvalidSignature"); - - // Create event with invalid signature using EventGenerator - const eventGenerator = new EventGenerator(); - const eventData = eventGenerator.generateEvent(); - - // Modify signature to be invalid - eventData.sig = "invalid-signature"; - - // Create NDKEvent and subscription - const event = new NDKEvent(ndk, eventData); - const sub = new NDKSubscription(ndk, { kinds: [1] }); - - // Process the event as if received from the relay - sub.eventReceived(event, mockRelay); - - // Verify reportInvalidSignature was called with correct parameters - expect(reportSpy).toHaveBeenCalledWith(expect.any(NDKEvent), mockRelay); - - // Verify the relay is considered malicious after a single invalid signature - if (ndk.autoBlacklistInvalidRelays) { - expect(ndk.blacklistRelayUrls).toContain(mockRelay.url); - } - }); - ``` - -2. **Event Emitting and Blacklisting Test** - - ```typescript - import { RelayMock, EventGenerator } from "@nostr-dev-kit/ndk/test"; - - test("emits event:invalid-sig event with relay and can blacklist", async () => { - // Create NDK with auto blacklisting - const ndk = new NDK({ - autoBlacklistInvalidRelays: true, - }); - - // Create mock relay - const mockRelay = new RelayMock(ndk, { url: "wss://mock.com" }); - - // Create listener for the event - const listener = jest.fn(); - ndk.on("event:invalid-sig", listener); - - // Generate event - const eventGenerator = new EventGenerator(); - const event = new NDKEvent(ndk, eventGenerator.generateEvent()); - - // Trigger invalid signature report - ndk.reportInvalidSignature(event, mockRelay); - - // Verify listener was called with correct args - expect(listener).toHaveBeenCalledWith(event, mockRelay); - - // Verify relay was blacklisted - expect(ndk.blacklistRelayUrls).toContain(mockRelay.url); - }); - ``` - -3. **Testing with Time Control** - - ```typescript - import { TestFixture, withTimeControl } from "@nostr-dev-kit/ndk/test"; - - test( - "ratio calculation over time", - withTimeControl(async ({ advanceTime }) => { - const fixture = new TestFixture(); - const ndk = fixture.ndk; - ndk.initialValidationRatio = 1.0; - ndk.lowestValidationRatio = 0.1; - - const relay = new NDKRelay("wss://example.com", undefined, ndk); - - // Add validated events over simulated time - for (let i = 0; i < 5; i++) { - relay.addValidatedEvent(); - // Advance time by 1 hour - await advanceTime(60 * 60 * 1000); - } - - // Check ratio is still high with just a few validations - let validationCount = 0; - for (let i = 0; i < 100; i++) { - if (relay.shouldValidateEvent()) validationCount++; - } - - // Should still be high with just 5 events - expect(validationCount).toBeGreaterThan(80); - - // Add many more validated events - for (let i = 0; i < 95; i++) { - relay.addValidatedEvent(); - } - - // Advance time by 1 day - await advanceTime(24 * 60 * 60 * 1000); - - // Check ratio has decreased significantly - validationCount = 0; - for (let i = 0; i < 100; i++) { - if (relay.shouldValidateEvent()) validationCount++; - } - - // Should now be much lower after 100 total validated events - expect(validationCount).toBeLessThan(50); - expect(validationCount).toBeGreaterThan(10); // But not below minimum - }), - ); - ``` - -4. **Testing Async Signature Verification** - - ```typescript - import { RelayMock, EventGenerator } from "@nostr-dev-kit/ndk/test"; - - test("centralizes invalid signature reporting from async verification", async () => { - // Create NDK with async verification - const worker = new Worker("path/to/signature-worker.js"); - const ndk = new NDK({ - signatureVerificationWorker: worker, - autoBlacklistInvalidRelays: true, - }); - - // Create a mock relay - const mockRelay = new RelayMock(ndk, { url: "wss://mock.com" }); - - // Spy on reportInvalidSignature - const reportSpy = jest.spyOn(ndk, "reportInvalidSignature"); - - // Generate event with invalid signature - const eventGenerator = new EventGenerator(); - const event = eventGenerator.generateEvent(); - event.sig = "invalid-signature"; - - // Mock the worker verification process - // This would normally be handled by the worker messaging - const ndkEvent = new NDKEvent(ndk, event); - ndkEvent.relay = mockRelay; - - // Simulate worker message for invalid signature - // (In reality this would happen asynchronously) - ndk.reportInvalidSignature(ndkEvent, mockRelay); - - // Verify reportInvalidSignature was called with correct parameters - expect(reportSpy).toHaveBeenCalledWith(ndkEvent, mockRelay); - - // Verify relay was blacklisted - expect(ndk.blacklistRelayUrls).toContain(mockRelay.url); - }); - ``` - -## Conclusion - -This implementation plan provides a comprehensive approach to signature verification sampling in NDK. By validating only a sample of signatures from each relay, we can significantly improve performance while maintaining security. - -The centralized `reportInvalidSignature` method ensures consistent handling of invalid signatures, regardless of whether they are detected synchronously during event processing or asynchronously by a web worker. This maintains the security model where a single invalid signature is sufficient to identify a malicious relay. - -By extending the existing `event:invalid-sig` event to include relay information, we maintain backward compatibility while providing the necessary context to identify and potentially blacklist malicious relays. - -The implementation is flexible, allowing developers to configure the validation ratio parameters or provide their own custom ratio calculation function to suit their specific needs and threat models. diff --git a/core/docs-styles.css b/core/docs-styles.css deleted file mode 100644 index 8dfe07744..000000000 --- a/core/docs-styles.css +++ /dev/null @@ -1,37 +0,0 @@ -html, -body { - font-family: - ui-sans-serif, - system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - "Helvetica Neue", - Arial, - "Noto Sans", - sans-serif, - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji"; - background: #222; -} - -header.tsd-page-toolbar .tsd-toolbar-contents { - height: auto; - padding: 1rem; -} - -header.tsd-page-toolbar a.title { - background-image: url("https://raw.githubusercontent.com/nvk/ndk.fyi/master/ndk.svg"); - width: 40px; - height: 40px; - display: block; - background-size: contain; - color: transparent; -} - -header.tsd-page-toolbar .tsd-toolbar-links a { - font-weight: bold; -} diff --git a/core/docs/advanced/architecture_suggestions.md b/core/docs/advanced/architecture_suggestions.md new file mode 100644 index 000000000..869227baa --- /dev/null +++ b/core/docs/advanced/architecture_suggestions.md @@ -0,0 +1,8 @@ +## Architecture decisions & suggestions + +- Users of NDK should instantiate a single NDK instance. +- That instance tracks state with all relays connected, explicit and otherwise. +- All relays are tracked in a single pool that handles connection errors/reconnection logic. +- RelaySets are assembled ad-hoc as needed depending on the queries set, although some RelaySets might be long-lasting, + like the `explicitRelayUrls` specified by the user. +- RelaySets are always a subset of the pool of all available relays. \ No newline at end of file diff --git a/core/snippets/user/encrypted-keys-nip49.md b/core/docs/advanced/encrypted-keys-nip49.md similarity index 75% rename from core/snippets/user/encrypted-keys-nip49.md rename to core/docs/advanced/encrypted-keys-nip49.md index c6fa204a7..ef1c373ab 100644 --- a/core/snippets/user/encrypted-keys-nip49.md +++ b/core/docs/advanced/encrypted-keys-nip49.md @@ -4,27 +4,7 @@ This snippet demonstrates how to encrypt and decrypt private keys using NIP-49 ( ## Basic Encryption and Decryption -```typescript -import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; - -// Generate or load a signer -const signer = NDKPrivateKeySigner.generate(); - -// Encrypt the private key with a password -const password = "user-chosen-password"; -const ncryptsec = signer.encryptToNcryptsec(password); - -// Store the encrypted key (e.g., localStorage, database) -localStorage.setItem("encrypted_key", ncryptsec); - -// Later, restore the signer from the encrypted key -const storedKey = localStorage.getItem("encrypted_key"); -const restoredSigner = NDKPrivateKeySigner.fromNcryptsec(storedKey, password); - -console.log("Original pubkey:", signer.pubkey); -console.log("Restored pubkey:", restoredSigner.pubkey); -// Both will match! -``` +<<< @/core/docs/snippets/key_create_store.ts ## Security Parameters @@ -126,24 +106,7 @@ main().catch(console.error); For advanced use cases, you can access the raw NIP-49 functions: -```typescript -import { nip49 } from "@nostr-dev-kit/ndk"; -import { hexToBytes, bytesToHex } from "@noble/hashes/utils"; - -// Encrypt raw private key bytes -const privateKeyHex = "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a"; -const privateKeyBytes = hexToBytes(privateKeyHex); -const password = "my-password"; - -const ncryptsec = nip49.encrypt(privateKeyBytes, password, 16, 0x02); -console.log("Encrypted:", ncryptsec); - -// Decrypt to raw bytes -const decryptedBytes = nip49.decrypt(ncryptsec, password); -const decryptedHex = bytesToHex(decryptedBytes); -console.log("Decrypted:", decryptedHex); -// Will match original privateKeyHex -``` +<<< @/core/docs/snippets/nip-49-encrypting.ts ## Security Best Practices diff --git a/docs/event-class-registration.md b/core/docs/advanced/event-class-registration.md similarity index 100% rename from docs/event-class-registration.md rename to core/docs/advanced/event-class-registration.md diff --git a/core/docs/advanced/exclusive-relay.md b/core/docs/advanced/exclusive-relay.md new file mode 100644 index 000000000..93a27879e --- /dev/null +++ b/core/docs/advanced/exclusive-relay.md @@ -0,0 +1,208 @@ +# Exclusive Relay Subscriptions + +By default, [NDK subscriptions](/core/docs/fundamentals/subscribing.md) use cross-subscription matching: when an event +comes in from any relay, it's delivered to +all subscriptions whose filters match, regardless of which relays the subscription was targeting. + +The `exclusiveRelay` option allows you to create subscriptions that **only** accept events from their specified relays, +ignoring events that match the filter but come from other relays. + +## Basic Usage + +<<< @/core/docs/snippets/subscribe_relay_targetting.ts + +## Default Behavior (Cross-Subscription Matching) + +Without `exclusiveRelay`, subscriptions receive events from any relay: + +```typescript +// Default behavior - accepts events from ANY relay +const normalSub = ndk.subscribe( + {kinds: [1], authors: ['pubkey...']}, + { + relayUrls: ['wss://relay-a.com'], + exclusiveRelay: false, // or omit (default) + onEvent: (event) => { + // This fires for events from relay-a.com, relay-b.com, relay-c.com + // or any other relay, as long as the filter matches + } + } +); +``` + +## Use Cases + +### 1. Relay-Specific Data Fetching + +Fetch events exclusively from a specific relay: + +```typescript +// Only get events from a specific community relay +const communitySub = ndk.subscribe( + {kinds: [1], '#t': ['community']}, + { + relayUrls: ['wss://community-relay.example.com'], + exclusiveRelay: true + } +); +``` + +### 2. Relay Isolation Testing + +Test relay-specific behavior: + +```typescript +// Test what a specific relay returns +const testSub = ndk.subscribe( + {kinds: [1], limit: 10}, + { + relayUrls: ['wss://test-relay.com'], + exclusiveRelay: true, + closeOnEose: true, + onEose: () => { + console.log('Finished fetching from test-relay.com'); + } + } +); + +``` + +### 3. Relay-Based Routing + +Route events based on relay provenance: + +```typescript +const publicRelaySub = ndk.subscribe( + {kinds: [1]}, + { + relayUrls: ['wss://public-relay.com'], + exclusiveRelay: true, + onEvent: (event) => { + console.log('Public event:', event.content); + } + } +); + +const privateRelaySub = ndk.subscribe( + {kinds: [1]}, + { + relayUrls: ['wss://private-relay.com'], + exclusiveRelay: true, + onEvent: (event) => { + console.log('Private event:', event.content); + } + } +); +``` + +## Using NDKRelaySet + +You can also use `NDKRelaySet` with `exclusiveRelay`: + +```typescript +import {NDKRelaySet} from '@nostr-dev-kit/ndk'; + +const relaySet = NDKRelaySet.fromRelayUrls( + ['wss://relay-a.com', 'wss://relay-b.com'], + ndk +); + +const sub = ndk.subscribe( + {kinds: [1]}, + { + relaySet, + exclusiveRelay: true + } +); + +// Only receives events from relay-a.com or relay-b.com +``` + +## Edge Cases + +### Cached Events + +Cached events are checked against their known relay provenance. If a cached event was previously seen on a relay in your +exclusive relaySet, it will be delivered: + +```typescript +const sub = ndk.subscribe( + {kinds: [1]}, + { + relayUrls: ['wss://relay-a.com'], + exclusiveRelay: true, + cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST + } +); + +// Cached events that came from relay-a.com: ✅ Delivered +// Cached events from other relays: ❌ Rejected +``` + +### Optimistic Publishes + +Optimistic publishes (local events before relay confirmation) respect the `skipOptimisticPublishEvent` setting: + +```typescript +const sub = ndk.subscribe( + {kinds: [1]}, + { + relayUrls: ['wss://relay-a.com'], + exclusiveRelay: true, + skipOptimisticPublishEvent: false // Accept optimistic publishes + } +); + +// Optimistic publishes: ✅ Delivered (if skipOptimisticPublishEvent is false) +``` + +### No RelaySet Specified + +If `exclusiveRelay: true` but no `relaySet` or `relayUrls` is specified, the check is not applied: + +```typescript +const sub = ndk.subscribe( + {kinds: [1]}, + { + exclusiveRelay: true // Has no effect without relaySet/relayUrls + } +); + +// Behaves like a normal subscription - accepts events from any relay +``` + +## Combining Exclusive and Non-Exclusive Subscriptions + +You can mix exclusive and non-exclusive subscriptions in the same NDK instance: + +```typescript +// Exclusive subscription - only relay-a.com +const exclusiveSub = ndk.subscribe( + {kinds: [1], '#t': ['exclusive']}, + { + relayUrls: ['wss://relay-a.com'], + exclusiveRelay: true + } +); + +// Non-exclusive subscription - any relay +const globalSub = ndk.subscribe( + {kinds: [1], '#t': ['global']}, + { + exclusiveRelay: false + } +); + +// exclusiveSub: Only gets #t=exclusive events from relay-a.com +// globalSub: Gets #t=global events from any connected relay +``` + +## Performance Considerations + +The `exclusiveRelay` check happens after filter matching, so there's minimal performance impact. The check only applies +to subscriptions that have both: + +- `exclusiveRelay: true` +- A specified `relaySet` or `relayUrls` + +All other subscriptions skip the relay provenance check entirely. diff --git a/core/docs/tutorial/filter-validation.md b/core/docs/advanced/filter-validation.md similarity index 100% rename from core/docs/tutorial/filter-validation.md rename to core/docs/advanced/filter-validation.md diff --git a/core/docs/tutorial/local-first.md b/core/docs/advanced/local-first.md similarity index 100% rename from core/docs/tutorial/local-first.md rename to core/docs/advanced/local-first.md diff --git a/core/docs/tutorial/mute-filtering.md b/core/docs/advanced/mute-filtering.md similarity index 100% rename from core/docs/tutorial/mute-filtering.md rename to core/docs/advanced/mute-filtering.md diff --git a/core/docs/advanced/relay-metadata-caching.md b/core/docs/advanced/relay-metadata-caching.md new file mode 100644 index 000000000..e69de29bb diff --git a/core/docs/tutorial/signer-persistence.md b/core/docs/advanced/signer-persistence.md similarity index 96% rename from core/docs/tutorial/signer-persistence.md rename to core/docs/advanced/signer-persistence.md index 078adefa3..b8cd4f268 100644 --- a/core/docs/tutorial/signer-persistence.md +++ b/core/docs/advanced/signer-persistence.md @@ -12,9 +12,6 @@ function. Every NDK signer (`NDKPrivateKeySigner`, `NDKNip07Signer`, `NDKNip46Signer`, etc.) implements a `toPayload()` method. This method returns a JSON string containing the minimal information needed to reconstruct a functionally equivalent signer instance later. -Every NDK signer (`NDKPrivateKeySigner`, `NDKNip07Signer`, `NDKNip46Signer`, etc.) implements a `toPayload()` method. -This method returns a string containing the information needed to reconstruct a functionally equivalent signer instance -later. **Example Usage:** diff --git a/core/docs/tutorial/speed.md b/core/docs/advanced/speed.md similarity index 100% rename from core/docs/tutorial/speed.md rename to core/docs/advanced/speed.md diff --git a/core/docs/internals/subscriptions.md b/core/docs/advanced/subscription-internals.md similarity index 72% rename from core/docs/internals/subscriptions.md rename to core/docs/advanced/subscription-internals.md index b60872965..9c92ccca9 100644 --- a/core/docs/internals/subscriptions.md +++ b/core/docs/advanced/subscription-internals.md @@ -8,14 +8,11 @@ Say we want to see `kind:1` events from pubkeys `123`, `456`, and `678`. const subscription = ndk.subscribe({ kinds: [1], authors: ["123", "456", "678"] }); ``` -Since the application level didn't explicitly provide a relay-set, which is the most common use case, NDK will calculate -a relay set based on the outbox model plus a variety of some other factors. +Since the application level didn't explicitly provide a relay-set, which is the most common use case, NDK will calculate a relay set based on the outbox model plus a variety of some other factors. ## Relay Selection for Authors -When a subscription includes an `authors` filter, NDK uses the outbox model to determine which relays to query for each -author. By default, NDK will query **2 relays** for each author, but this can be customized using the -`relayGoalPerAuthor` option: +When a subscription includes an `authors` filter, NDK uses the outbox model to determine which relays to query for each author. By default, NDK will query **2 relays** for each author, but this can be customized using the `relayGoalPerAuthor` option: ```ts // Query 3 relays for each author instead of the default 2 @@ -31,20 +28,15 @@ const subscription = ndk.subscribe( ); ``` -Higher values improve redundancy and reduce the chance of missing events, but increase bandwidth usage and the number of -relay connections. Lower values reduce resource usage but may miss events if a relay is down or doesn't have the event. -Setting `relayGoalPerAuthor: Infinity` will query all available relays for each author. +Higher values improve redundancy and reduce the chance of missing events, but increase bandwidth usage and the number of relay connections. Lower values reduce resource usage but may miss events if a relay is down or doesn't have the event. Setting `relayGoalPerAuthor: Infinity` will query all available relays for each author. So the first thing we'll do before talking to relays is, decide to _which_ relays we should talk to. -The `calculateRelaySetsFromFilters` function will take care of this and provide us with a map of relay URLs and filters -for each relay. +The `calculateRelaySetsFromFilters` function will take care of this and provide us with a map of relay URLs and filters for each relay. -This means that the query, as specified by the client might be broken into distinct queries specialized for the -different relays. +This means that the query, as specified by the client might be broken into distinct queries specialized for the different relays. -For example, if we have 3 relays, and the query is for `kind:1` events from pubkeys `a` and `b`, the -`calculateRelaySetsFromFilters` function might return something like this: +For example, if we have 3 relays, and the query is for `kind:1` events from pubkeys `a` and `b`, the `calculateRelaySetsFromFilters` function might return something like this: ```ts { @@ -62,42 +54,27 @@ flowchart TD ## Subscription bundling -Once the subscription has been split into the filters each relay should receive, the filters are sent to the individual -`NDKRelay`'s `NDKRelaySubscriptionManager` instances. +Once the subscription has been split into the filters each relay should receive, the filters are sent to the individual `NDKRelay`'s `NDKRelaySubscriptionManager` instances. -`NDKRelaySubscriptionManager` is responsible for keeping track of the active and scheduled subscriptions that are -pending to be executed within an individual relay. +`NDKRelaySubscriptionManager` is responsible for keeping track of the active and scheduled subscriptions that are pending to be executed within an individual relay. This is an important aspect to consider: -> `NDKSubscription` have a different lifecycle than `NDKRelaySubscription`. For example, a subscription that is set to -> close after EOSE might still be active within the `NDKSubscription` lifecycle, but it might have been already been -> closed within the `NDKRelaySubscription` lifecycle, since NDK attempts to keep the minimum amount of open subscriptions -> at any given time. +> `NDKSubscription` have a different lifecycle than `NDKRelaySubscription`. For example, a subscription that is set to close after EOSE might still be active within the `NDKSubscription` lifecycle, but it might have been already been closed within the `NDKRelaySubscription` lifecycle, since NDK attempts to keep the minimum amount of open subscriptions at any given time. ## NDKRelaySubscription -Most NDK subscriptions (by default) are set to be executed with a grouping delay. Will cover what this looks like in -practice later, but for now, let's understand than when the `NDKRelaySubscriptionManager` receives an order, it might -not execute it right away. +Most NDK subscriptions (by default) are set to be executed with a grouping delay. Will cover what this looks like in practice later, but for now, let's understand than when the `NDKRelaySubscriptionManager` receives an order, it might not execute it right away. -The different filters that can be grouped together (thus executed as a single `REQ` within a relay) are grouped within -the same `NDKRelaySubscription` instance and the execution scheduler is computed respecting what each individual -`NDKSubscription` has requested. +The different filters that can be grouped together (thus executed as a single `REQ` within a relay) are grouped within the same `NDKRelaySubscription` instance and the execution scheduler is computed respecting what each individual `NDKSubscription` has requested. -(For example, if a subscription with a `groupingDelay` of `at-least` 500 millisecond has been grouped with another -subscription with a `groupingDelay` of `at-least` 1000 milliseconds, the `NDKRelaySubscriptionManager` will wait 1000 ms -before sending the `REQ` to this particular relay). +(For example, if a subscription with a `groupingDelay` of `at-least` 500 millisecond has been grouped with another subscription with a `groupingDelay` of `at-least` 1000 milliseconds, the `NDKRelaySubscriptionManager` will wait 1000 ms before sending the `REQ` to this particular relay). ### Execution -Once the filter is executed at the relay level, the `REQ` is submitted into that relay's `NDKRelayConnectivity` -instance, which will take care of monitoring for responses for this particular REQ and communicate them back into the -`NDKRelaySubscription` instance. +Once the filter is executed at the relay level, the `REQ` is submitted into that relay's `NDKRelayConnectivity` instance, which will take care of monitoring for responses for this particular REQ and communicate them back into the `NDKRelaySubscription` instance. -Each `EVENT` that comes back as a response to our `REQ` within this `NDKRelaySubscription` instance is sent to the -top-level `NDKSubscriptionManager`. This manager tracks ALL active subscriptions and when events come in dispatches the -event to all `NDKSubscription`s interested in this event. +Each `EVENT` that comes back as a response to our `REQ` within this `NDKRelaySubscription` instance is sent to the top-level `NDKSubscriptionManager`. This manager tracks ALL active subscriptions and when events come in dispatches the event to all `NDKSubscription`s interested in this event. # Example @@ -130,8 +107,7 @@ flowchart TD end ``` -Both subscriptions have their relayset calculated by NDK and, the resulting filters are sent into the -`NDKRelaySubscriptionManager`, which will decide what, and how filters can be grouped. +Both subscriptions have their relayset calculated by NDK and, the resulting filters are sent into the `NDKRelaySubscriptionManager`, which will decide what, and how filters can be grouped. ```mermaid flowchart TD @@ -152,8 +128,7 @@ flowchart TD end ``` -The `NDKRelaySubscriptionManager` will create `NDKRelaySubscription` instances, or add filters to them if -`NDKRelaySubscription` with the same filter fingerprint exists. +The `NDKRelaySubscriptionManager` will create `NDKRelaySubscription` instances, or add filters to them if `NDKRelaySubscription` with the same filter fingerprint exists. ```mermaid flowchart TD @@ -178,8 +153,7 @@ flowchart TD end ``` -Each individual `NDKRelaySubscription` computes the execution schedule of the filters it has received and sends them to -the `NDKRelayConnectivity` instance, which in turns sends the `REQ` to the relay. +Each individual `NDKRelaySubscription` computes the execution schedule of the filters it has received and sends them to the `NDKRelayConnectivity` instance, which in turns sends the `REQ` to the relay. ```mermaid flowchart TD @@ -214,9 +188,7 @@ flowchart TD end ``` -As the events come from the relays, `NDKRelayConnectivity` will send them back to the `NDKRelaySubscription` instance, -which will compare the event with the filters of the `NDKSubscription` instances that have been grouped together and -send the received event back to the correct `NDKSubscription` instance. +As the events come from the relays, `NDKRelayConnectivity` will send them back to the `NDKRelaySubscription` instance, which will compare the event with the filters of the `NDKSubscription` instances that have been grouped together and send the received event back to the correct `NDKSubscription` instance. ```mermaid flowchart TD @@ -263,8 +235,7 @@ flowchart TD ## Handling Subscription Events -When creating a subscription using `ndk.subscribe`, you can provide handlers for different stages of the subscription -lifecycle directly within the options or the `autoStart` parameter. +When creating a subscription using `ndk.subscribe`, you can provide handlers for different stages of the subscription lifecycle directly within the options or the `autoStart` parameter. ```typescript interface NDKSubscriptionEventHandlers { @@ -312,14 +283,10 @@ ndk.subscribe( ### Bulk Cache Event Handling (`onEvents`) -A key feature is the behavior when using the `onEvents` handler. If NDK has a cache adapter configured and finds events -matching the subscription filter synchronously in the cache upon starting the subscription: +A key feature is the behavior when using the `onEvents` handler. If NDK has a cache adapter configured and finds events matching the subscription filter synchronously in the cache upon starting the subscription: -1. The `onEvents` handler will be called exactly once with an array containing all these cached `NDKEvent` objects. -2. The regular `onEvent` handler will *not* be called for this initial batch of cached events. -3. After this initial batch, `onEvent` will be called for any subsequent events received from relays or asynchronous - cache updates. +1. The `onEvents` handler will be called exactly once with an array containing all these cached `NDKEvent` objects. +2. The regular `onEvent` handler will *not* be called for this initial batch of cached events. +3. After this initial batch, `onEvent` will be called for any subsequent events received from relays or asynchronous cache updates. -This allows applications to efficiently process the initial state from the cache in one go, which can be particularly -beneficial for UI frameworks to avoid multiple re-renders that might occur if `onEvent` were called for each cached item -individually. If `onEvents` is *not* provided, `onEvent` will be called for every event, including those from the cache. +This allows applications to efficiently process the initial state from the cache in one go, which can be particularly beneficial for UI frameworks to avoid multiple re-renders that might occur if `onEvent` were called for each cached item individually. If `onEvents` is *not* provided, `onEvent` will be called for every event, including those from the cache. diff --git a/core/docs/tutorial/subscription-management.md b/core/docs/advanced/subscription-management.md similarity index 100% rename from core/docs/tutorial/subscription-management.md rename to core/docs/advanced/subscription-management.md diff --git a/core/docs/fundamentals/connecting.md b/core/docs/fundamentals/connecting.md new file mode 100644 index 000000000..aeb7d1b60 --- /dev/null +++ b/core/docs/fundamentals/connecting.md @@ -0,0 +1,129 @@ +# Connecting + +This section will briefly explain the different mechanmisms through which NDK can connect to relays. + +## Connecting + +Connecting to relays in itself is super easy. + +```ts +// Import the package +import NDK from "@nostr-dev-kit/ndk"; + +// ... set up signer, specify relays, ... + +await ndk.connect(); // [!code focus] +``` + +On connection changes NDK will emit +[a number of connection events](/core/docs/fundamentals/connecting.html#connection-events). + +## Connection types + +::: tip +Because NOSTR is decentralized and comprised of thousands of relays, it's important to read up on the +advised ways of connecting to relays. We advise you to use +the "[Outbox Model](/core/fundamentals/connecting.html#outbox-model)" +in addition or as a replacement for specifying explicit relays. +::: + +### Specific Relays + +The simplest way to get NDK to connect to relays is to specify them: + +<<< @/core/docs/snippets/connect_explicit.ts + +Make sure to wait for the `connect()` promise to resolve before using NDK after which +you can start interacting with relays. + +Explicit relays can also be added using the `addExplicitRelay()` method. + +<<< @/core/docs/snippets/connect_explicit_alt.ts + +### User preferred relays + +A [signer](/core/getting-started/signers.html) is used to sign events and tells NDK about your pubkey and related +settings. Once you +link up a signer and you have `autoConnectUserRelays` enabled (on by default) NDK will fetch your `kind:10002` event +([NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md)) and add discovered relays specified to a 2nd pool +of connected relays. + +<<< @/core/docs/snippets/connect_nip07.ts + +### Outbox Model + +Outbox (previously known as [the Gossip Model](https://mikedilger.com/gossip-model/)) is a more elaborate way of +dynamically connecting to relays based on who you are interacting with. +More about [the outbox model](https://how-nostr-works.pages.dev/#/outbox). + +The outbox model works similarly to the web/RSS model: + +- **Outbox Relays**: You post your content to your own designated relays +- **Inbox Relays**: You designate relays where you want to receive messages +- **Dynamic Discovery**: Clients discover and connect to relays based on where users actually post + +The protocol is formalized in ([NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md)), which defines: + +- **`kind:10002` events**: Relay list metadata containing read/write relay preferences +- **Relay tags**: "r" tags with optional "read"/"write" markers +- **Fallback to Kind 3**: Contact list events can contain relay information in their content + +By enabling `enableOutboxModel` (off by default) NDK will add an extra `outboxPool` to the ndk pool AND (@TODO Explain) + +https://primal.net/e/nevent1qqs2txvkjpa6fdlhxdtqmyk2tzzchpaa4vrfgx7h20539u5k9lzgqwgfjnlen + +### Dev Write Relays + +During local development you might want to specify a list of relays to write to. THis can be done by using +`devWriteRelayUrls` which will + +<<< @/core/docs/snippets/connect_dev_relays.ts + +This will write new events to those relays only. Note that if you have provided relays in +`explicitRelayUrls` these will also be used to write events to. + +## Relay Sets + +Under the hood NDK uses different sets of relays to send and receive messages. You can tap into that pool logic by +using the `NDKPool` class. + +<<< @/core/docs/snippets/connect_pools.ts + +Note that if you have outbox enabled you will have an extra pool in the `ndk.pools` array reserved for user provided +relays. + +## Authentication + +([NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md)) defines that relays can request authentication from +clients. NDK uses an `NDKAuthPolicy` callback to provide a way to handle authentication requests. + +* Relays can have specific `NDKAuthPolicy` functions. +* NDK can be configured with a default `relayAuthDefaultPolicy` function. +* NDK provides some generic policies: + * `NDKAuthPolicies.signIn`: Authenticate to the relay (using the `ndk.signer` signer). + * `NDKAuthPolicies.disconnect`: Immediately disconnect from the relay if asked to authenticate. + +<<< @/core/docs/snippets/connect_auth.ts + +Clients should typically allow their users to choose where to authenticate. This can be accomplished by returning the +decision the user made from the `NDKAuthPolicy` function. + +```ts +import NDK, {NDKRelayAuthPolicies} from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); +ndk.relayAuthDefaultPolicy = (relay: NDKRelay) => { + return confirm(`Authenticate to ${relay.url}?`); +}; +``` + +## Connection Events + +There are a number of events you can hook into to get information about relay connection +status + +<<< @/core/docs/snippets/connection_events.ts + +## Code Snippets + +More snippets and examples can be found in the [snippets directory](/docs/snippets.md#connecting) \ No newline at end of file diff --git a/core/docs/fundamentals/events.md b/core/docs/fundamentals/events.md new file mode 100644 index 000000000..e9e41e2eb --- /dev/null +++ b/core/docs/fundamentals/events.md @@ -0,0 +1,51 @@ +# Events + +Events are at the heart of the Nostr protocol and described +in [NIP-01.](https://github.com/nostr-protocol/nips/blob/master/01.md). To support +a wide range of functionality, Nostr comes +with [a number of event types](https://github.com/nostr-protocol/nips/blob/master/Readme.md#event-kinds) but you +can also create your own. + +## Creating an event + +This is the simplest example of creating a text note [ +`kind:1`](https://github.com/nostr-protocol/nips/tree/master?tab=readme-ov-file#event-kinds) event. + +<<< @/core/docs/snippets/event_create.ts + +No need to fill in event's `id`, `tags`, `pubkey`, `created_at`, NDK will do that (if not set). + +## Tagging users (or events) + +Tags tell the protocol about related entities like mentioned users, relays, topics, other events, etc. Details about +tags can be found in the [tags section of NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md#tags) and +in [the reference list of +standardized tags](https://github.com/nostr-protocol/nips/tree/master?tab=readme-ov-file#common-tags). + +NDK automatically adds the appropriate tags for mentions in the content when a user or event is mentioned. + +<<< @/core/docs/snippets/tag_user.ts + +Calling `event.sign()` will finalize the event, adding the appropriate tags. + +The resulting event will look like: + +<<< @/core/docs/snippets/tag_user_result.json + +## Signing Events + +NDK uses the default signer `ndk.signer` to sign events. + +<<< @/core/docs/snippets/sign_event.ts + +Read more about signers in [the signer documentation](/core/docs/fundamentals/signers.md) + +## Encoding Events + +NDK events have a built-in `encode()` method that automatically determines the appropriate NIP-19 format: + +<<< @/core/docs/snippets/event_encode.ts + +## Code Snippets + +More snippets and examples can be found in the [snippets directory](/docs/snippets.md#events). diff --git a/core/docs/fundamentals/helpers.md b/core/docs/fundamentals/helpers.md new file mode 100644 index 000000000..3118a0be1 --- /dev/null +++ b/core/docs/fundamentals/helpers.md @@ -0,0 +1,52 @@ +# Helpers + +NDK comes with a ton of additional helpers and utilities to do things faster. A number of those utilities come from the +[nostr-tools library](https://github.com/nbd-wtf/nostr-tools) which is included as part of NDK + +## NIP-19 Identifiers + +NDK re-exports [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) from the +[nostr-tools library](https://github.com/nbd-wtf/nostr-tools): + +### Encoding + +<<< @/core/docs/snippets/nip-19-encoding.ts + +### Decoding + +<<< @/core/docs/snippets/nip-19-decoding.ts + +## NIP-49 Encryption + +NDK re-exports [NIP-49](https://github.com/nostr-protocol/nips/blob/master/49.md) from the +[nostr-tools library](https://github.com/nbd-wtf/nostr-tools): + +<<< @/core/docs/snippets/nip-49-encrypting.ts + +## Fetching Users + +The `ndk.fetchUser()` method accepts [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) encoded strings +directly, automatically detecting and decoding the format: + +<<< @/core/docs/snippets/user-fetching.ts + +## Generate Keys + +By using `NDKPrivateKeySigner` you can create keysets for signing and verifying messages. + +This snippet demonstrates how to generate a new key pair and obtain all its various formats (private key, public key, +nsec, npub). + +<<< @/core/docs/snippets/key_create.ts + +More about key generaton in [the signer documentation](/core/docs/fundamentals/signers). + +## Code Snippets + +More snippets and examples can be found in the [snippets directory](/docs/snippets.md#helpers) + + + + + + diff --git a/core/docs/fundamentals/publishing.md b/core/docs/fundamentals/publishing.md new file mode 100644 index 000000000..42d802835 --- /dev/null +++ b/core/docs/fundamentals/publishing.md @@ -0,0 +1,68 @@ +# Publishing Events + +Publishing events means sending them to one or multiple relays as described in +[NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md#communication-between-clients-and-relays). NDK +provides easy ways to publish +events and manage the status of that event. + +> [!NOTE] +> Please note that the behavior of `.publish()` requires a valid [signer](/core/docs/fundamentals/signers.html) and +> will only publish the events to the configured relays. +> More about relay interaction in the [connecting documentation](/core/docs/fundamentals/connecting.md). + +[//]: # (Read more about the [local-first](local-first.md) mode of operation.) + +## Publishing Events + +The easiest way to publish an event is to use the `publish()` method on the event object. + +<<< @/core/docs/snippets/publish_event.ts + +This will sign the event and send it to the network. + +## Publishing Replaceable Events + +Some events in Nostr allow for replacement. + +Kinds `0`, `3`, range `10000-19999`. Range `30000-39999` is dedicated for parameterized replaceable events, +which means that multiple events of the same kind under the same pubkey can exist and are differentiated via +their `d` tag. + +Since replaceable events depend on having a newer `created_at`, NDK provides a convenience method to reset `id`, `sig`, +and `created_at` to allow for easy replacement: `event.publishReplaceable()` + +<<< @/core/docs/snippets/replace_event.ts + +## Specifying Relays + +NDK will publish your event to the already connected relaySets. If you want to specify where to +publish that specific event to you can pass a `NDKRelaySet` to the publish method: + +<<< @/core/docs/snippets/publish_to_relayset.ts + +## Tracking Publication Status + +You can use the event.on `published` handler to keep track of where events are +published and the status of each publish attempt: + +<<< @/core/docs/snippets/publish_tracking.ts + +## Publication Failures + +When publishing to multiple relays, some may succeed while others fail. This can be handled +through the `event:publish-failed` handler + +<<< @/core/docs/snippets/publish_failure.ts + +## Event Status Properties + +- `event.publishedToRelays` - Array of relay URLs where the event was successfully published +- `event.failedPublishesToRelays` - Map of relay URLs to their errors +- `event.publishRelayStatus` - Map of all relay URLs to their detailed status +- `event.wasPublishedTo(url)` - Check if successfully published to a specific relay +- `event.publishStatus` - Overall status: "pending", "success", or "error" +- `event.publishError` - Error if the overall publish failed + +## Code Snippets + +More snippets and examples can be found in the [snippets directory](/docs/snippets.md#publishing) \ No newline at end of file diff --git a/core/docs/fundamentals/signers.md b/core/docs/fundamentals/signers.md new file mode 100644 index 000000000..3f8852b34 --- /dev/null +++ b/core/docs/fundamentals/signers.md @@ -0,0 +1,135 @@ +# Signers + +All events on the Nostr protocol are signed through a keypair +(described in [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md#events-and-signatures)). + +In NDK this is taken care of by the `NDKSigner` interface that can be passed in during initialization or later during +runtime. + +## Signing Methods + +Before you can sign events you need a signer set-up. There are different ways to sign events and this space is still +evolving. + +### Browser Extensions + +A common way to use NDK is to use a browser extension which is described +in [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md). +This mechanism allows the user to sign events with a browser extension to not share their private key with the +application. + +The most used browser extensions are [Nos2x](https://github.com/fiatjaf/nos2x) +and [Alby](https://getalby.com/alby-extension). + +<<< @/core/docs/snippets/sign_event.ts + +Anytime you call `sign()` or `publish()` on an [NDK Event](/core/docs/fundamentals/events.html) the browser +extension will prompt the user to sign the event. + +### Private Key Signer + +NDK provides `NDKPrivateKeySigner` for managing in-memory private keys. This is useful for development, testing, or +applications that manage keys locally. + +> [!WARNING] +> We strongly recommend not using this in production. Requiring users to share their private key is a security +> risk and should be avoided in favor of +> using [a browser extension](/core/docs/fundamentals/signers.html#browser-extensions) +> or [a remote signer](/core/docs/fundamentals/signers.html#remote-signer). + +The `NDKPrivateKeySigner` can be instantiated with an nsec or hex private key: + +<<< @/core/docs/snippets/private-signer.ts + +After that the signer can be used to sign events: + +<<< @/core/docs/snippets/sign_event_nsec.ts + +This library can also [help with generating new keys](/core/docs/fundamentals/signers.html#generate-keys). + +### Remote Signer + +A Nostr remote signer (aka `bunker`) is an application or device that securely stores your private key and signs Nostr +events on your behalf, preventing you from having to expose the private key. It works by establishing a secure +connection (over +Nostr relays), as described in [NIP-46](https://github.com/nostr-protocol/nips/blob/master/46.md), where the bunker +implementation can approve or deny requests. + +To add remote signing support to your application, there are a few things you need: + +* a bunker:// connection string provided by the user +* A local (client) keypair used to communicate with + remote-signer. [Can be generated by NDK](/core/docs/fundamentals/signers.html#generate-keys) + +Create a `NDKNip46Signer` with the bunker connection string and local keypair. + +<<< @/core/docs/snippets/sign_with_bunker.ts + +## Sign Events + +Once the signer is initialized, you can use it to sign and [publish](/core/docs/fundamentals/publishing.html) events: + +<<< @/core/docs/snippets/sign_event.ts + +## Signer Relays + +If the [signer](/core/docs/fundamentals/signers.md) implements the `getRelays()` method, +NDK will use the relays returned by that method as the explicit relays. + +## Combining signers + +You can specify the use of a different signer to sign with different keys. + +> [!TIP] +> If you plan on allowing multiple signers we recommend using [@nostr-dev-kit/sessions](/sessions/README.html). + +<<< @/core/docs/snippets/sign_event_with_other_signers.ts + +## Read Public key + +**Read the user's public key** + +```ts +nip07signer.user().then(async (user) => { + if (!!user.npub) { + console.log("Permission granted to read their public key:", user.npub); + } +}); +``` + +## Generate Keys + +One good case where you would want to use `NDKPrivateKeySigner` is to help you generate keys as the signer +provides helper methods. + +This snippet demonstrates how to generate a new key pair and obtain all its various formats (private key, public key, +nsec, npub). + +<<< @/core/docs/snippets/key_create.ts + +You can use these different formats for different purposes: + +- `privateKey`: Raw private key for cryptographic operations +- `publicKey`: Raw public key (hex format) for verification +- `nsec`: Encoded private key format (bech32) - used for secure sharing when needed +- `npub`: Encoded public key format (bech32) - used for user identification + +### Encrypting Keys + +For storing keys securely with password protection, +use [NIP-49](https://github.com/nostr-protocol/nips/blob/master/49.md) (ncryptsec format): + +<<< @/core/docs/snippets/key_create_store.ts + +See [Encrypted Keys (NIP-49)](/core/docs/advanced/encrypted-keys-nip49.md) for more examples and best practices. + +## Signer Persistence + +In a lot of applications you would want to persist the signer preferences so the state +can be maintained without requiring re-authentication. + +Please consult [the dedicated section about signer persistence](/core/docs/advanced/signer-persistence.md). + +## Code Snippets + +More snippets and examples can be found in the [snippets directory](/docs/snippets.md#signers) \ No newline at end of file diff --git a/core/docs/fundamentals/subscribing.md b/core/docs/fundamentals/subscribing.md new file mode 100644 index 000000000..03b16f0d3 --- /dev/null +++ b/core/docs/fundamentals/subscribing.md @@ -0,0 +1,100 @@ +# Subscribing to Events + +Once [connected](/core/docs/getting-started/usage#connecting), you can subscribe to events using `ndk.subscribe()` by +providing filters you can specify the events you're interested in. + +More about how this all works +in [the dedicated section about subscription internals](/core/docs/advanced/subscription-internals.md). + +## Subscribe + +The `ndk.subscribe()` method accepts these parameters: + +- `filters`: A single or array of `NDKFilter`. + See [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md#communication-between-clients-and-relays). +- `opts?`: Subscription options object `NDKSubscriptionOptions`. +- `autoStart?`: [Event handlers](#event-handlers) for that subscription. + +<<< @/core/docs/snippets/subscribe.ts + +## Specifying Relays + +By default, NDK will use the already connected relay set or provided through the signer. You can +override this behavior by providing explicit relays in the `relayUrls` or `relaySet` options. + +<<< @/core/docs/snippets/subscribe_relayset.ts + +By default, NDK subscriptions use cross-subscription matching: when an event comes in from any relay, it's delivered to +all subscriptions whose filters match, regardless of which relays the subscription was targeting. + +## Event Handlers + +### Handler Functions + +> [!TIP] +> The **recommended** way to handle events is to provide handler functions directly when calling `ndk.subscribe()`. This +> is done using the third argument (`autoStart`), which accepts an object containing `onEvent`, `onEvents`, and/or +`onEose` callbacks. + +**Why is this preferred?** Subscriptions can start receiving events (especially from a fast cache) almost immediately +after `ndk.subscribe()` is called. By providing handlers directly, you ensure they are attached *before* any events are +emitted, preventing potential race conditions where you might miss the first few events if you attached handlers later +using `.on()`. + +<<< @/core/docs/snippets/subscribe_event_handlers.ts + +### Attaching Handlers + +You can also attach event listeners *after* creating the subscription using the `.on()` method. + +> [!WARNING] +> While functional, be mindful of the potential race condition mentioned above, especially if you rely on immediate +> cache results. + +<<< @/core/docs/snippets/subscribe_event_attach.ts + +## Functions + +### onEvent + +The `onEvent` handler is called for every event received from relays or the cache. + +### onEvents + +Using the `onEvents` handler provides an efficient way to process events loaded from the cache. When you provide +`onEvents`: + +1. If NDK finds matching events in its cache *synchronously* when the subscription starts, `onEvents` is called **once** + with an array of all those cached events. +2. The `onEvent` handler is **skipped** for this initial batch of cached events. +3. `onEvent` will still be called for any subsequent events received from relays or later asynchronous cache updates. + +This is ideal for scenarios like populating initial UI state, as it allows you to process the cached data in a single +batch, preventing potentially numerous individual updates that would occur if `onEvent` were called for each cached +item. + +If you *don't* provide `onEvents`, the standard `onEvent` handler will be triggered for every event, whether it comes +from the cache or a relay. + +### onEose + +Called when the subscription is closed. + +## Targetting Relays + +By default, NDK subscriptions use cross-subscription matching: when an event comes in from any relay, it's delivered to +all subscriptions whose filters match, regardless of which relays the subscription was targeting. + +The `exclusiveRelay` option allows you to create subscriptions that **only** accept events from their specified relays, +ignoring events that match the filter but come from other relays. + +<<< @/core/docs/snippets/subscribe_relay_targetting.ts + +Without `exclusiveRelay`, subscriptions receive events from any relay (Cross-Subscription Matching). + +More information, use-cases and examples of exclusive relays is available in +the [advanced exclusive relay documentation](/core/docs/advanced/exclusive-relay.md#subscribing). + +## Code Snippets + +More snippets and examples can be found in the [snippets directory](/docs/snippets.md#subscribing) diff --git a/core/docs/fundamentals/zapping.md b/core/docs/fundamentals/zapping.md new file mode 100644 index 000000000..13b98e122 --- /dev/null +++ b/core/docs/fundamentals/zapping.md @@ -0,0 +1,10 @@ +# Zaps + +NDK comes with an interface to make zapping as simple as possible. + +<<< @/core/docs/snippets/zap.ts + +## NDK-Wallet + +Refer to [the Wallet section of the documentation](/wallet/README) to learn more about zapping. +NDK-wallet provides many conveniences to integrate with zaps. diff --git a/core/docs/getting-started/debugging.md b/core/docs/getting-started/debugging.md new file mode 100644 index 000000000..8824b8ff8 --- /dev/null +++ b/core/docs/getting-started/debugging.md @@ -0,0 +1,47 @@ +# Debugging + +## Enable Debug Mode + +NDK uses the `debug` package to assist in understanding what's happening behind the hood. If you are building a package +that runs on the server define the `DEBUG` envionment variable like + +```sh +export DEBUG='ndk:*' +``` + +or in the browser enable it by writing in the DevTools console + +```sh +localStorage.debug = 'ndk:*' +``` + +## Network Debugging + +You can construct NDK passing a netDebug callback to receive network traffic events, particularly useful for debugging applications not running in a browser. + +```ts +const netDebug = (msg: string, relay: NDKRelay, direction?: "send" | "recv") = { + const hostname = new URL(relay.url).hostname; + netDebug(hostname, msg, direction); +} + +ndk = new NDK({ netDebug }); +``` + +## Development Relays + +When you're developing an application you can initialise NDK with the `devWriteRelayUrls` property to tell the NDK +instance to write to specific relays: + +```ts +import NDK from "@nostr-dev-kit/ndk"; + +const ndk = new NDK({ + devWriteRelayUrls: ["wss://staging.relay", "wss://another.test.relay"], +}); + +await ndk.connect(); +``` + +This will write new events to those relays only. Note that if you have provided relays in +`explicitRelayUrls` these will also be used to write events to. diff --git a/core/docs/getting-started/installing.md b/core/docs/getting-started/installing.md new file mode 100644 index 000000000..1b69a8d43 --- /dev/null +++ b/core/docs/getting-started/installing.md @@ -0,0 +1,54 @@ +# Getting started + +## Installation + +You can install NDK core using your favorite package manager. + +::: code-group + +```sh [npm] +npm i @nostr-dev-kit/ndk +``` + +```sh [pnpm] +pnpm add @nostr-dev-kit/ndk +``` + +```sh [yarn] +yarn add @nostr-dev-kit/ndk +``` + +```sh [bun] +bun add @nostr-dev-kit/ndk +``` +::: + +NDK is compatible with node v16+ + + +## Other packages + +For other functionality you might need additional packages: + +### Extras +* [@nostr-dev-kit/blossom](/blossom/README.md): Blossom Protocol Support for assets +* [@nostr-dev-kit/sessions](/sessions/README.md): Session Management with Multi-Account support +* [@nostr-dev-kit/sync](/sync/README.md): Event synchronization using Negentropy +* [@nostr-dev-kit/wallet](/wallet/README.md): Support for WebLN, NWC, Cashu/eCash wallets +* [@nostr-dev-kit/wot](/wot/README.md): Web of Trust (WOT) utilities + +### Framework Integrations +* [@nostr-dev-kit/react](/react/README.md): Hooks and utilities to integrate Nostr into your React applications +* [@nostr-dev-kit/svelte](/svelte/README.md): Modern, performant, and beautiful Svelte 5 integration + +### Cache Adapters + +These NDK adapters are used to store and retrieve data from a cache so relays do not need to be +re-queried for the same data. + +* [@nostr-dev-kit/cache-memory](/cache-memory/README.md): In-memory LRU cache adapter +* [@nostr-dev-kit/cache-nostr](/cache-nostr/README.md): Local Nostr relay cache adapter +* [@nostr-dev-kit/cache-redis](/cache-redis/README.md): A cache adapter for Redis +* [@nostr-dev-kit/cache-dexie](/cache-dexie/README.md): Dexie (IndexedDB, in browser database) adapter +* [@nostr-dev-kit/cache-sqlite](/cache-sqlite/README.md): SQLite (better-sqlite3) adapter +* [@nostr-dev-kit/cache-sqlite-wasm](/cache-sqlite-wasm/md): In browser (WASM) SQLite adapter diff --git a/core/docs/getting-started/usage.md b/core/docs/getting-started/usage.md index dfb9bc2cf..8fd8a7226 100644 --- a/core/docs/getting-started/usage.md +++ b/core/docs/getting-started/usage.md @@ -1,277 +1,57 @@ # Usage -## Instantiate an NDK instance +## Quick Start -You can pass an object with several options to a newly created instance of NDK. +A simple example of how to use NDK in a Node.js application: -- `explicitRelayUrls` – an array of relay URLs. -- `signer` - an instance of a [signer](#signers). -- `cacheAdapter` - an instance of a [Cache Adapter](#caching) -- `debug` - Debug instance to use for logging. Defaults to `debug("ndk")`. +<<< @/core/docs/snippets/examples/quick-start-with-guardrails.ts -```ts -// Import the package -import NDK from "@nostr-dev-kit/ndk"; +## NDK Usage -// Create a new NDK instance with explicit relays -const ndk = new NDK({ - explicitRelayUrls: ["wss://a.relay", "wss://another.relay"], -}); -``` +### Instantiate NDK -If the signer implements the `getRelays()` method, NDK will use the relays returned by that method as the explicit -relays. +You can pass an object with several options to a newly created instance of NDK. -```ts -// Import the package -import NDK, { NDKNip07Signer } from "@nostr-dev-kit/ndk"; +- `explicitRelayUrls` – an array of relay URLs. +- `signer` - an instance of a signer ([signer documentation](/core/docs/fundamentals/signers.md)). +- `cacheAdapter` - an instance of a cache adapter. +- `debug` - debugger instance ([debugging documentation](/core/docs/getting-started/debugging.md)). -// Create a new NDK instance with just a signer (provided the signer implements the getRelays() method) -const nip07signer = new NDKNip07Signer(); -const ndk = new NDK({ signer: nip07signer }); -``` +<<< @/core/docs/snippets/initialise.ts Note: In normal client use, it's best practice to instantiate NDK as a singleton class. [See more below](#architecture-decisions--suggestions). -## Connecting - -After you've instatiated NDK, you need to tell it to connect before you'll be able to interact with any relays. - -```ts -// Import the package -import NDK from "@nostr-dev-kit/ndk"; - -// Create a new NDK instance with explicit relays -const ndk = new NDK({ - explicitRelayUrls: ["wss://a.relay", "wss://another.relay"], -}); -// Now connect to specified relays -await ndk.connect(); -``` - -## Creating Users - -NDK provides flexible ways to fetch user objects, including support for NIP-19 encoded identifiers and NIP-05 addresses: - -```typescript -// From hex pubkey -const user1 = await ndk.fetchUser("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"); - -// From npub (NIP-19 encoded) -const user2 = await ndk.fetchUser("npub1n0sturny6w9zn2wwexju3m6asu7zh7jnv2jt2kx6tlmfhs7thq0qnflahe"); - -// From nprofile (includes relay hints) -const user3 = await ndk.fetchUser("nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"); - -// From NIP-05 identifier -const user4 = await ndk.fetchUser("pablo@test.com"); -const user5 = await ndk.fetchUser("test.com"); // Uses _@test.com - -// The method automatically detects the format -const user6 = await ndk.fetchUser("deadbeef..."); // Assumes hex pubkey -``` - -Note: `fetchUser` is async and returns a Promise. For NIP-05 lookups, it may return `undefined` if the address cannot be -resolved. - -## Working with NIP-19 Identifiers - -NDK re-exports NIP-19 utilities for encoding and decoding Nostr identifiers: - -```typescript -import { nip19 } from '@nostr-dev-kit/ndk'; - -// Encode a pubkey as npub -const npub = nip19.npubEncode("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"); - -// Decode any NIP-19 identifier -const decoded = nip19.decode("npub1..."); -console.log(decoded.type); // "npub" -console.log(decoded.data); // hex pubkey - -// Encode events -const nevent = nip19.neventEncode({ - id: eventId, - relays: ["wss://relay.example.com"], - author: authorPubkey -}); -``` - -See the [NIP-19 tutorial](/tutorial/nip19.html) for comprehensive examples and use cases. - -## Usage with React Hooks (`ndk-hooks`) - -When using the `ndk-hooks` package in a React application, the initialization process involves creating the NDK instance -and then using the `useNDKInit` hook to make it available to the rest of your application via Zustand stores. - -This hook ensures that both the core NDK store and dependent stores (like the user profiles store) are properly -initialized with the NDK instance. - -It's recommended to create and connect your NDK instance outside of your React components, potentially in a dedicated -setup file or at the root of your application. Then, use the `useNDKInit` hook within your main App component or a -context provider to initialize the stores once the component mounts. - -```tsx -import React, { useEffect } from 'react'; // Removed useState -import NDK from '@nostr-dev-kit/ndk'; -import { useNDKInit } from '@nostr-dev-kit/ndk-hooks'; // Assuming package name - -// 1. Configure your NDK instance (e.g., in src/ndk.ts or similar) -const ndk = new NDK({ - explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.primal.net'], - // Add signer or cache adapter if needed -}); - -// 2. Connect the instance immediately -ndk.connect() - .then(() => console.log('NDK connected')) - .catch((e) => console.error('NDK connection error:', e)); - -// Example: App component or Context Provider that initializes NDK stores -function App() { - const initializeNDK = useNDKInit(); // Hook returns the function directly - - useEffect(() => { - // 3. Initialize stores once the component mounts - initializeNDK(ndk); - }, [initializeNDK]); // Dependency ensures this runs if initializeNDK changes, though unlikely - - // Your application components can now use other ndk-hooks - // No need to wait for connection state here, as hooks handle NDK readiness internally - return ( -
- {/* ... Your app content using useProfile, useSubscribe, etc. ... */} -
- ); -} - -export default App; -``` - -**Key Points:** - -* Create and configure your `NDK` instance globally or outside components. -* Call `ndk.connect()` immediately after creation. Connection happens in the background. -* In your main App or Provider component, get the `initializeNDK` function from `useNDKInit`. -* Use `useEffect` with an empty dependency array (or `[initializeNDK]`) to call `initializeNDK(ndk)` once on mount. -* This sets up the necessary Zustand stores. Other `ndk-hooks` will access the initialized `ndk` instance from the store - and handle its readiness internally. - ---- - -## Architecture decisions & suggestions - -- Users of NDK should instantiate a single NDK instance. -- That instance tracks state with all relays connected, explicit and otherwise. -- All relays are tracked in a single pool that handles connection errors/reconnection logic. -- RelaySets are assembled ad-hoc as needed depending on the queries set, although some RelaySets might be long-lasting, - like the `explicitRelayUrls` specified by the user. -- RelaySets are always a subset of the pool of all available relays. - -## Subscribing to Events - -Once connected, you can subscribe to events using `ndk.subscribe()`. You provide filters to specify the events you're -interested in. - -### Preferred Method: Direct Event Handlers - -The **recommended** way to handle events is to provide handler functions directly when calling `ndk.subscribe()`. This -is done using the third argument (`autoStart`), which accepts an object containing `onEvent`, `onEvents`, and/or -`onEose` callbacks. - -**Why is this preferred?** Subscriptions can start receiving events (especially from a fast cache) almost immediately -after `ndk.subscribe()` is called. By providing handlers directly, you ensure they are attached *before* any events are -emitted, preventing potential race conditions where you might miss the first few events if you attached handlers later -using `.on()`. - -```typescript -// Example with default relay calculation -ndk.subscribe( - { kinds: [1], authors: [pubkey] }, // Filters - { closeOnEose: true }, // Options (no explicit relays specified) - { // Direct handlers via autoStart parameter (now the 3rd argument) - onEvent: (event: NDKEvent, relay?: NDKRelay) => { - // Called for events received from relays after the initial cache load (if onEvents is used) - console.log("Received event from relay (id):", event.id); - }, - onEvents: (events: NDKEvent[]) => { // Parameter renamed to 'events' - console.log(`Received ${events.length} events from cache initially.`); - }, - onEose: (subscription: NDKSubscription) => { - console.log("Subscription reached EOSE:", subscription.internalId); - } - } -); +### Connecting -// Example specifying explicit relays using relayUrls option -ndk.subscribe( - { kinds: [0], authors: [pubkey] }, // Filters - { // Options object now includes relayUrls - closeOnEose: true, - relayUrls: ["wss://explicit1.relay", "wss://explicit2.relay"] - }, - { // Direct handlers - onEvent: (event: NDKEvent) => { /* ... */ } - } -); +After you've instantiated NDK, you need to tell it to connect before you'll be able to interact with any relays. -// Example specifying explicit relays using relaySet option -const explicitRelaySet = NDKRelaySet.fromRelayUrls(["wss://explicit.relay"], ndk); -ndk.subscribe( - { kinds: [7], authors: [pubkey] }, // Filters - { // Options object now includes relaySet - closeOnEose: true, - relaySet: explicitRelaySet - }, - { // Direct handlers - onEvent: (event: NDKEvent) => { /* ... */ } - } -); -``` +<<< @/core/docs/snippets/connecting.ts -### Efficient Cache Handling with `onEvents` +### Using NIP-19 Identifiers -Using the `onEvents` handler provides an efficient way to process events loaded from the cache. When you provide -`onEvents`: +NDK re-exports [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) from the +[nostr-tools library](https://github.com/nbd-wtf/nostr-tools) which provides different utilities for encoding and +decoding Nostr identifiers: -1. If NDK finds matching events in its cache *synchronously* when the subscription starts, `onEvents` is called **once** - with an array of all those cached events. -2. The `onEvent` handler is **skipped** for this initial batch of cached events. -3. `onEvent` will still be called for any subsequent events received from relays or later asynchronous cache updates. +<<< @/core/docs/snippets/nip-19-identifiers.ts -This is ideal for scenarios like populating initial UI state, as it allows you to process the cached data in a single -batch, preventing potentially numerous individual updates that would occur if `onEvent` were called for each cached -item. +### Managing Users -If you *don't* provide `onEvents`, the standard `onEvent` handler will be triggered for every event, whether it comes -from the cache or a relay. +NDK provides flexible ways to fetch user objects, including support for +[NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) encoded identifiers +and [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md) addresses: -### Alternative Method: Attaching Handlers with `.on()` +<<< @/core/docs/snippets/user-fetching.ts -You can also attach event listeners *after* creating the subscription using the `.on()` method. While functional, be -mindful of the potential race condition mentioned above, especially if you rely on immediate cache results. +Note: `fetchUser` is async and returns a Promise. For [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md) +lookups, it may return `undefined` if the address cannot be resolved. -```typescript -// Subscribe using default relay calculation -const subscription = ndk.subscribe( - { kinds: [1], authors: [pubkey] }, - { closeOnEose: true } // Options -); +See the [NIP-19 tutorial](/core/docs/tutorial/nip19.md) for comprehensive examples and use cases. -// Subscribe using explicit relays via options -const subscriptionWithRelays = ndk.subscribe( - { kinds: [0], authors: [pubkey] }, - { relayUrls: ["wss://explicit.relay"] } // Options with explicit relays -); +## Framework Integrations -// Attach handlers later -subscription.on("event", (event) => { - console.log("Received event:", event.id); -}); -subscription.on("eose", () => { - console.log("Initial events loaded"); -}); +If you're planning to use `NDK` in a react framework, check out the documentation for these specific packages: -// Remember to stop the subscription when it's no longer needed -// setTimeout(() => subscription.stop(), 5000); +* [@nostr-dev-kit/react](/react/README.md): Hooks and utilities to integrate NDK into a React applications +* [@nostr-dev-kit/svelte](/svelte/README.md): Modern, performant, and beautiful Svelte 5 integration diff --git a/core/MIGRATION-2.16.md b/core/docs/migration/2.13-to-2.16.md similarity index 100% rename from core/MIGRATION-2.16.md rename to core/docs/migration/2.13-to-2.16.md diff --git a/core/docs/snippets/.gitignore b/core/docs/snippets/.gitignore new file mode 100644 index 000000000..f52e6f577 --- /dev/null +++ b/core/docs/snippets/.gitignore @@ -0,0 +1,2 @@ +node_modules +pnpm-lock.yaml \ No newline at end of file diff --git a/core/docs/snippets/connect_auth.ts b/core/docs/snippets/connect_auth.ts new file mode 100644 index 000000000..52a85cb50 --- /dev/null +++ b/core/docs/snippets/connect_auth.ts @@ -0,0 +1,5 @@ +import NDK, { NDKRelayAuthPolicies } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); + +ndk.addExplicitRelay("wss://relay.f7z.io", NDKRelayAuthPolicies.signIn({ ndk })); diff --git a/core/docs/snippets/connect_dev_relays.ts b/core/docs/snippets/connect_dev_relays.ts new file mode 100644 index 000000000..e3737eb2a --- /dev/null +++ b/core/docs/snippets/connect_dev_relays.ts @@ -0,0 +1,7 @@ +import NDK from "@nostr-dev-kit/ndk"; + +const ndk = new NDK({ + devWriteRelayUrls: ["wss://staging.relay", "wss://another.test.relay"], +}); + +await ndk.connect(); diff --git a/core/docs/snippets/connect_explicit.ts b/core/docs/snippets/connect_explicit.ts new file mode 100644 index 000000000..97e091c78 --- /dev/null +++ b/core/docs/snippets/connect_explicit.ts @@ -0,0 +1,10 @@ +// Import the package +import NDK from "@nostr-dev-kit/ndk"; + +// Create a new NDK instance with explicit relays +const ndk = new NDK({ + explicitRelayUrls: ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"], +}); + +// Now connect to specified relays +await ndk.connect(); diff --git a/core/docs/snippets/connect_explicit_alt.ts b/core/docs/snippets/connect_explicit_alt.ts new file mode 100644 index 000000000..d556f3eaf --- /dev/null +++ b/core/docs/snippets/connect_explicit_alt.ts @@ -0,0 +1,12 @@ +// Import the package +import NDK from "@nostr-dev-kit/ndk"; + +// Create a new NDK instance with explicit relays +const ndk = new NDK(); + +ndk.addExplicitRelay("wss://relay.damus.io"); +ndk.addExplicitRelay("wss://nos.lol"); +ndk.addExplicitRelay("wss://relay.nostr.band"); + +// Now connect to specified relays +await ndk.connect(); diff --git a/core/docs/snippets/connect_nip07.ts b/core/docs/snippets/connect_nip07.ts new file mode 100644 index 000000000..2c86c66c5 --- /dev/null +++ b/core/docs/snippets/connect_nip07.ts @@ -0,0 +1,8 @@ +// Import NDK + NIP07 signer +import NDK, { NDKNip07Signer } from "@nostr-dev-kit/ndk"; + +// Create a new NDK instance with signer +// provided the signer implements the getRelays() method +const nip07signer = new NDKNip07Signer(); + +const ndk = new NDK({ signer: nip07signer }); diff --git a/core/docs/snippets/connect_pools.ts b/core/docs/snippets/connect_pools.ts new file mode 100644 index 000000000..23baf1bdf --- /dev/null +++ b/core/docs/snippets/connect_pools.ts @@ -0,0 +1,16 @@ +import NDK, { NDKPool, NDKRelay } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); + +const largeRelays = new NDKPool([`wss://relay.damus.io`, "wss://premium.primal.net"], ndk); +largeRelays.addRelay(new NDKRelay("wss://nos.lol", undefined, ndk)); + +largeRelays.connect(); + +ndk.pools.length; // 1 + +const nicheRelays = new NDKPool([`wss://relay.vertexlab.io`, "wss://purplepag.es/"], ndk); + +nicheRelays.connect(); + +ndk.pools.length; // 2 diff --git a/core/docs/snippets/connecting.ts b/core/docs/snippets/connecting.ts new file mode 100644 index 000000000..efab0ffbf --- /dev/null +++ b/core/docs/snippets/connecting.ts @@ -0,0 +1,10 @@ +// Import the package +import NDK from "@nostr-dev-kit/ndk"; + +// Create a new NDK instance with explicit relays +const ndk = new NDK({ + explicitRelayUrls: ["wss://a.relay", "wss://another.relay"], +}); + +// Now connect to specified relays +await ndk.connect(); diff --git a/core/docs/snippets/connection_events.ts b/core/docs/snippets/connection_events.ts new file mode 100644 index 000000000..6a74c57fd --- /dev/null +++ b/core/docs/snippets/connection_events.ts @@ -0,0 +1,19 @@ +import NDK, { type NDKRelay } from "@nostr-dev-kit/ndk"; + +// Create a new NDK instance with explicit relays +const ndk = new NDK({ + explicitRelayUrls: ["wss://a.relay", "wss://another.relay"], +}); + +// Main pool events +ndk.pool.on("relay:connecting", (relay: NDKRelay) => { + console.log(`⟳ [Main Pool] Connecting to relay: ${relay.url}`); +}); + +ndk.pool.on("relay:connect", (relay: NDKRelay) => { + console.log(`✓ [Main Pool] Connected to relay: ${relay.url}`); +}); + +ndk.pool.on("relay:disconnect", (relay: NDKRelay) => { + console.log(`✗ [Main Pool] Disconnected from relay: ${relay.url}`); +}); diff --git a/core/docs/snippets/event_create.ts b/core/docs/snippets/event_create.ts new file mode 100644 index 000000000..543904251 --- /dev/null +++ b/core/docs/snippets/event_create.ts @@ -0,0 +1,8 @@ +import NDK, { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(/* initialization options for the ndk singleton */); + +const event = new NDKEvent(ndk, { + kind: NDKKind.Text, + content: "Hello world", +}); diff --git a/core/docs/snippets/event_encode.ts b/core/docs/snippets/event_encode.ts new file mode 100644 index 000000000..4965d6285 --- /dev/null +++ b/core/docs/snippets/event_encode.ts @@ -0,0 +1,17 @@ +import NDK, { NDKEvent } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); + +const event = new NDKEvent(ndk); +event.kind = 1; +event.content = "Hello Nostr!"; +await event.sign(); + +// Automatically chooses the right format: +// - naddr for parameterized replaceable events +// - nevent for events with relay information +// - note for simple note references +const encoded = event.encode(); + +// Control relay hints +const encodedWith5Relays = event.encode(5); // Include up to 5 relay hints diff --git a/core/docs/snippets/examples/ai-guardrails-examples.ts b/core/docs/snippets/examples/ai-guardrails-examples.ts new file mode 100644 index 000000000..7c9b6a6da --- /dev/null +++ b/core/docs/snippets/examples/ai-guardrails-examples.ts @@ -0,0 +1,167 @@ +/** + * AI Guardrails Example + * + * This example demonstrates how AI Guardrails help catch common mistakes + * when using NDK, especially useful for LLM-generated code. + */ + +import NDK, {NDKEvent} from "@nostr-dev-kit/ndk"; +import {nip19} from "nostr-tools"; + +// Example 1: Enable all guardrails (recommended for development) +const ndk = new NDK({ + explicitRelayUrls: ["wss://relay.damus.io"], + aiGuardrails: true, // Enable all checks +}); + +await ndk.connect(); + +// Example 2: Filter with bech32 - will throw error +try { + const npub = "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft"; + + // ❌ This will throw an AI Guardrails error + ndk.subscribe({ + authors: [npub], // Wrong! Should be hex + }); +} catch (error) { + console.log("Caught error:", error.message); + // Error message will explain how to fix it +} + +// ✅ Correct way - decode bech32 first +const npub = "npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft"; +const { data: pubkey } = nip19.decode(npub); +ndk.subscribe({ + authors: [pubkey as string], +}); + +// Example 3: Empty filter - will throw error +try { + // ❌ This will throw + ndk.subscribe({}); +} catch (error) { + console.log("Caught empty filter error"); +} + +// Example 4: Filter with only limit - will throw error +try { + // ❌ This will throw + ndk.subscribe({ limit: 10 }); +} catch (error) { + console.log("Caught limit-only filter error"); +} + +// Example 5: Event signing without kind - will throw error +try { + const event = new NDKEvent(ndk); + event.content = "Hello world"; + // ❌ Missing kind! + await event.sign(); +} catch (error) { + console.log("Caught missing kind error"); +} + +// ✅ Correct way +const event = new NDKEvent(ndk); +event.kind = 1; +event.content = "Hello world"; +await event.sign(); + +// Example 6: Created_at in milliseconds - will throw error +try { + const event = new NDKEvent(ndk); + event.kind = 1; + event.content = "Hello"; + event.created_at = Date.now(); // ❌ Milliseconds! + await event.sign(); +} catch (error) { + console.log("Caught milliseconds error"); +} + +// ✅ Correct way +const event2 = new NDKEvent(ndk); +event2.kind = 1; +event2.content = "Hello"; +event2.created_at = Math.floor(Date.now() / 1000); // Seconds +await event2.sign(); + +// Example 7: fetchEvents warning (doesn't throw, just warns) +console.log("\nThis will show a warning:"); +const events = await ndk.fetchEvents({ kinds: [1], limit: 5 }); +console.log("Got events:", events.size); + +// Example 8: Selectively disable checks +const ndk2 = new NDK({ + explicitRelayUrls: ["wss://relay.damus.io"], + aiGuardrails: { + skip: new Set([ + GuardrailCheckId.FETCH_EVENTS_USAGE, // Disable fetchEvents warning + GuardrailCheckId.FILTER_LARGE_LIMIT, // Disable large limit warning + ]), + }, +}); + +await ndk2.connect(); + +// ✅ This won't warn because we skipped the check +const manyEvents = await ndk2.fetchEvents({ kinds: [1], limit: 5 }); + +// Example 9: Programmatic control +ndk.aiGuardrails.skip(GuardrailCheckId.FETCH_EVENTS_USAGE); +// Now fetchEvents won't warn +const more = await ndk.fetchEvents({ kinds: [1], limit: 5 }); + +// Re-enable it +ndk.aiGuardrails.enable(GuardrailCheckId.FETCH_EVENTS_USAGE); + +// Example 10: Check what's currently skipped +const skipped = ndk.aiGuardrails.getSkipped(); +console.log("Currently skipped checks:", skipped); + +// Example 11: Disable all guardrails at runtime +ndk.aiGuardrails.setMode(false); +// Now no checks will run + +// Example 12: Production setup - disable by default +const productionNdk = new NDK({ + explicitRelayUrls: ["wss://relay.damus.io"], + aiGuardrails: process.env.NODE_ENV === "development", // Only in dev +}); + +// Example 13: LLM-friendly error handling +/** + * When an LLM generates code that triggers a guardrail, + * it should read the error message and self-correct: + */ +async function llmGeneratedCode() { + try { + const event = new NDKEvent(ndk); + // LLM forgot to set kind + event.content = "Generated content"; + await event.sign(); + } catch (error) { + console.log("\nLLM would see this error:"); + console.log(error.message); + + /** + * The error message includes: + * 1. What went wrong + * 2. How to fix it + * 3. How to disable the check if needed + * + * LLM can then: + * - Fix the code (add event.kind = 1) + * - Or skip the check if it knows better + */ + + // LLM self-corrects: + const correctedEvent = new NDKEvent(ndk); + correctedEvent.kind = 1; // Fixed! + correctedEvent.content = "Generated content"; + await correctedEvent.sign(); + console.log("\n✅ LLM successfully self-corrected!"); + } +} + +await llmGeneratedCode(); diff --git a/core/docs/snippets/examples/quick-start-with-guardrails.ts b/core/docs/snippets/examples/quick-start-with-guardrails.ts new file mode 100644 index 000000000..28e571607 --- /dev/null +++ b/core/docs/snippets/examples/quick-start-with-guardrails.ts @@ -0,0 +1,46 @@ +import NDK, {NDKEvent, NDKPrivateKeySigner} from "@nostr-dev-kit/ndk"; + +async function main() { + const signer = NDKPrivateKeySigner.generate(); + const ndk = new NDK({ + explicitRelayUrls: ["wss://relay.primal.net"], + signer, + + // ⚠️ STRONGLY RECOMMENDED: Enable during development + // Catches common mistakes before they cause silent failures + aiGuardrails: true, + }); + + // Connect to relays + await ndk.connect(); + + // Publish a simple text note + const event = new NDKEvent(ndk, { + kind: 1, + content: "Hello Nostr via NDK!", + }); + await event.sign(); + event.publish(); + + // subscribe to all event interactions + ndk.subscribe( + event.filter(), + {closeOnEose: false}, + { + onEvent: (replyEvent: NDKEvent) => + console.log(replyEvent.author.npub, "interacted with our hello world with a kind", replyEvent.kind), + }, + ); + + // Subscribe to incoming text notes + const subscription = ndk.subscribe( + {kinds: [1]}, + {closeOnEose: true}, + { + onEvent: (evt) => console.log("Received event:", evt), + onEose: () => console.log("End of stream"), + }, + ); +} + +main().catch(console.error); diff --git a/core/test-auth-publish.ts b/core/docs/snippets/examples/test-auth-publish.ts similarity index 96% rename from core/test-auth-publish.ts rename to core/docs/snippets/examples/test-auth-publish.ts index 207e17846..61f92b405 100755 --- a/core/test-auth-publish.ts +++ b/core/docs/snippets/examples/test-auth-publish.ts @@ -5,9 +5,9 @@ * Usage: bun run test-auth-publish.ts --nsec --msg "your message" */ -import { NDKEvent } from "./src/events"; -import { NDK } from "./src/ndk"; -import { NDKPrivateKeySigner } from "./src/signers/private-key"; +import {NDKEvent} from "./src/events"; +import {NDK} from "./src/ndk"; +import {NDKPrivateKeySigner} from "./src/signers/private-key"; // Parse command line arguments const args = process.argv.slice(2); diff --git a/core/docs/snippets/initialise.ts b/core/docs/snippets/initialise.ts new file mode 100644 index 000000000..2f40ff066 --- /dev/null +++ b/core/docs/snippets/initialise.ts @@ -0,0 +1,7 @@ +// Import the package +import NDK from "@nostr-dev-kit/ndk"; + +// Create a new NDK instance with explicit relays +const ndk = new NDK({ + explicitRelayUrls: ["wss://a.relay", "wss://another.relay"], +}); diff --git a/core/docs/snippets/interest_event.ts b/core/docs/snippets/interest_event.ts new file mode 100644 index 000000000..49bfe904c --- /dev/null +++ b/core/docs/snippets/interest_event.ts @@ -0,0 +1,26 @@ +import NDK, { NDKInterestList, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK({ + explicitRelayUrls: ["wss://relay.damus.io", "wss://relay.primal.net"], +}); + +// Create a signer (in production, use proper key management) +const signer = NDKPrivateKeySigner.generate(); +ndk.signer = signer; + +await ndk.connect(); + +// Create a new interest list +const interestList = new NDKInterestList(ndk); + +// Add individual interests +interestList.addInterest("nostr"); +interestList.addInterest("bitcoin"); +interestList.addInterest("technology"); +interestList.addInterest("privacy"); + +console.log("Has 'nostr'?", interestList.hasInterest("nostr")); +console.log("Has 'ethereum'?", interestList.hasInterest("ethereum")); + +// Publish the list (which also signs) +await interestList.publish(); diff --git a/core/docs/snippets/key_create.ts b/core/docs/snippets/key_create.ts new file mode 100644 index 000000000..6b266dd4e --- /dev/null +++ b/core/docs/snippets/key_create.ts @@ -0,0 +1,9 @@ +import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; + +// Generate a new private key +const signer = NDKPrivateKeySigner.generate(); + +const privateKey = signer.privateKey; // Get the hex private key +const publicKey = signer.pubkey; // Get the hex public key +const nsec = signer.nsec; // Get the private key in nsec format +const npub = signer.userSync.npub; // Get the public key in npub format diff --git a/core/docs/snippets/key_create_store.ts b/core/docs/snippets/key_create_store.ts new file mode 100644 index 000000000..a5e64e8fb --- /dev/null +++ b/core/docs/snippets/key_create_store.ts @@ -0,0 +1,20 @@ +import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; + +const signer = NDKPrivateKeySigner.generate(); + +// Encrypt with a password +const password = "user-chosen-password"; +const ncryptsec = signer.encryptToNcryptsec(password); + +// Store securely (e.g., localStorage) +localStorage.setItem("encrypted_key", ncryptsec); + +const restoredButEncrypted = localStorage.getItem("encrypted_key"); + +if (restoredButEncrypted) { + // Later, restore the signer + const restoredSigner = NDKPrivateKeySigner.fromNcryptsec(restoredButEncrypted, password); + + console.log("Original pubkey:", signer.pubkey); + console.log("Restored pubkey:", restoredSigner.pubkey); +} diff --git a/core/docs/snippets/nip-19-conversion.ts b/core/docs/snippets/nip-19-conversion.ts new file mode 100644 index 000000000..be2168e69 --- /dev/null +++ b/core/docs/snippets/nip-19-conversion.ts @@ -0,0 +1,24 @@ +import { nip19 } from "@nostr-dev-kit/ndk"; + +// Convert hex pubkey to npub +function hexToNpub(hexPubkey: string): string { + return nip19.npubEncode(hexPubkey); +} + +// Extract pubkey from any NIP-19 identifier +function extractPubkey(nip19String: string): string | undefined { + const decoded = nip19.decode(nip19String); + + switch (decoded.type) { + case "npub": + return decoded.data; + case "nprofile": + return decoded.data.pubkey; + case "naddr": + return decoded.data.pubkey; + case "nevent": + return decoded.data.author; + default: + return undefined; + } +} diff --git a/core/docs/snippets/nip-19-decoding.ts b/core/docs/snippets/nip-19-decoding.ts new file mode 100644 index 000000000..794eef0f8 --- /dev/null +++ b/core/docs/snippets/nip-19-decoding.ts @@ -0,0 +1,12 @@ +import { nip19 } from "@nostr-dev-kit/ndk"; + +// Decoding +const decoded = nip19.decode("npub1..."); +console.log(decoded.type); // "npub" +console.log(decoded.data); // hex pubkey + +// Type-specific decoding +if (decoded.type === "nprofile") { + console.log(decoded.data.pubkey); + console.log(decoded.data.relays); +} diff --git a/core/docs/snippets/nip-19-encoding.ts b/core/docs/snippets/nip-19-encoding.ts new file mode 100644 index 000000000..0247e8321 --- /dev/null +++ b/core/docs/snippets/nip-19-encoding.ts @@ -0,0 +1,29 @@ +import { nip19 } from "@nostr-dev-kit/ndk"; + +const pubkey = "a6f6b6a535ba73f593579e919690b7c29df3ba0d764790326c1d3b68d0bfde2e"; +const privateKey = "df63bea84840e5fd07961af1c76286aa628264ad5f151546007530551fb8068f"; +const eventId = "aef62d07b65b80974f0bd8d6b8fc85e4ffcdf34c86de40e5c92aa5654b7a91d4"; + +// Encoding +const npub = nip19.npubEncode(pubkey); +const nsec = nip19.nsecEncode(privateKey); +const note = nip19.noteEncode(eventId); + +// Encoding with metadata +const nprofile = nip19.nprofileEncode({ + pubkey: "hexPubkey", + relays: ["wss://relay1.example.com", "wss://relay2.example.com"], +}); + +const nevent = nip19.neventEncode({ + id: eventId, + relays: ["wss://relay.example.com"], + author: pubkey, +}); + +const naddr = nip19.naddrEncode({ + kind: 30023, + pubkey: pubkey, + identifier: "article-slug", + relays: ["wss://relay.example.com"], +}); diff --git a/core/docs/snippets/nip-19-identifiers.ts b/core/docs/snippets/nip-19-identifiers.ts new file mode 100644 index 000000000..9cf4e3485 --- /dev/null +++ b/core/docs/snippets/nip-19-identifiers.ts @@ -0,0 +1,19 @@ +import { nip19 } from "@nostr-dev-kit/ndk"; + +// Encode a pubkey as npub +const npub = nip19.npubEncode("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"); +console.log(npub); // + +// Decode any NIP-19 identifier +const decoded = nip19.decode("npub1..."); +console.log(decoded.type); // "npub" +console.log(decoded.data); // hex pubkey + +// Encode events +const nevent = nip19.neventEncode({ + id: "574033c986bea1d7493738b46fec1bb98dd6a826391d6aa893137e89790027ec", + relays: ["wss://relay.example.com"], + author: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", +}); + +console.log(nevent); diff --git a/core/docs/snippets/nip-19-profile-event.ts b/core/docs/snippets/nip-19-profile-event.ts new file mode 100644 index 000000000..f3af93390 --- /dev/null +++ b/core/docs/snippets/nip-19-profile-event.ts @@ -0,0 +1,18 @@ +import NDK, { nip19 } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); + +const eventId = "aef62d07b65b80974f0bd8d6b8fc85e4ffcdf34c86de40e5c92aa5654b7a91d4"; +const pubkey = "a6f6b6a535ba73f593579e919690b7c29df3ba0d764790326c1d3b68d0bfde2e"; + +// Create shareable event reference with relay hints +const event = await ndk.fetchEvent(eventId); + +if (event) { + const shareableLink = event.encode(3); // Include up to 3 relay hints + + const nprofile = nip19.nprofileEncode({ + pubkey: pubkey, + relays: ["wss://relay.damus.io", "wss://nos.lol"], + }); +} diff --git a/core/docs/snippets/nip-19-validate.ts b/core/docs/snippets/nip-19-validate.ts new file mode 100644 index 000000000..e68cc5dcc --- /dev/null +++ b/core/docs/snippets/nip-19-validate.ts @@ -0,0 +1,19 @@ +import { nip19 } from "@nostr-dev-kit/ndk"; + +function isValidNip19(str: string): boolean { + try { + nip19.decode(str); + return true; + } catch { + return false; + } +} + +function isNpub(str: string): boolean { + try { + const decoded = nip19.decode(str); + return decoded.type === "npub"; + } catch { + return false; + } +} diff --git a/core/docs/snippets/nip-49-encrypting.ts b/core/docs/snippets/nip-49-encrypting.ts new file mode 100644 index 000000000..3fdad18b1 --- /dev/null +++ b/core/docs/snippets/nip-49-encrypting.ts @@ -0,0 +1,15 @@ +import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; +import { nip49 } from "@nostr-dev-kit/ndk"; + +// Encrypt raw private key bytes +const privateKeyHex = "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a"; +const privateKeyBytes = hexToBytes(privateKeyHex); +const password = "my-password"; + +const ncryptsec = nip49.encrypt(privateKeyBytes, password, 16, 0x02); +console.log("Encrypted:", ncryptsec); + +// Decrypt to raw bytes +const decryptedBytes = nip49.decrypt(ncryptsec, password); +const decryptedHex = bytesToHex(decryptedBytes); +console.log("Decrypted:", decryptedHex); diff --git a/core/docs/snippets/package.json b/core/docs/snippets/package.json new file mode 100644 index 000000000..79c2ff31c --- /dev/null +++ b/core/docs/snippets/package.json @@ -0,0 +1,11 @@ +{ + "name": "snippets", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": {}, + "dependencies": { + "@nostr-dev-kit/ndk": "3.0.0-beta.62", + "@nostr-dev-kit/wallet": "1.0.0-beta.62" + } +} diff --git a/core/docs/snippets/private-signer.ts b/core/docs/snippets/private-signer.ts new file mode 100644 index 000000000..1d68d1e2c --- /dev/null +++ b/core/docs/snippets/private-signer.ts @@ -0,0 +1,7 @@ +import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; + +// From nsec +const signer = new NDKPrivateKeySigner("nsec1..."); + +// From hex private key +const signer2 = new NDKPrivateKeySigner("hexPrivateKey"); diff --git a/core/docs/snippets/publish_event.ts b/core/docs/snippets/publish_event.ts new file mode 100644 index 000000000..b65a3fc27 --- /dev/null +++ b/core/docs/snippets/publish_event.ts @@ -0,0 +1,10 @@ +import NDK, { NDKEvent, NDKNip07Signer } from "@nostr-dev-kit/ndk"; + +const nip07signer = new NDKNip07Signer(); +const ndk = new NDK({ signer: nip07signer }); + +const event = new NDKEvent(ndk); +event.kind = 1; +event.content = "Hello world"; + +await event.publish(); // [!code focus] diff --git a/core/docs/snippets/publish_failure.ts b/core/docs/snippets/publish_failure.ts new file mode 100644 index 000000000..0fab81a9a --- /dev/null +++ b/core/docs/snippets/publish_failure.ts @@ -0,0 +1,18 @@ +import NDK, { NDKEvent, type NDKPublishError } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK({ + explicitRelayUrls: ["wss://relay.damus.io", "wss://non.existing.relay", "wss://nos.lol"], +}); + +await ndk.connect(); + +const event = new NDKEvent(ndk, { + kind: 1, + content: "Hello Nostr!", +}); + +ndk.on(`event:publish-failed`, (event: NDKEvent, error: NDKPublishError, relays: WebSocket["url"][]) => { + console.log("Event failed to publish on", event, relays, error); +}); + +await event.publish(); diff --git a/core/docs/snippets/publish_to_relayset.ts b/core/docs/snippets/publish_to_relayset.ts new file mode 100644 index 000000000..1c4d23d1d --- /dev/null +++ b/core/docs/snippets/publish_to_relayset.ts @@ -0,0 +1,10 @@ +import NDK, { NDKEvent, NDKRelaySet } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); + +const event = new NDKEvent(ndk); +event.kind = 1; +event.content = "Hello world"; + +const customRelaySet = NDKRelaySet.fromRelayUrls(["wss://relay.snort.social", "wss://relay.primal.net"], ndk); +await event.publish(customRelaySet); diff --git a/core/docs/snippets/publish_tracking.ts b/core/docs/snippets/publish_tracking.ts new file mode 100644 index 000000000..5aa51016d --- /dev/null +++ b/core/docs/snippets/publish_tracking.ts @@ -0,0 +1,17 @@ +import NDK, { NDKEvent, type NDKRelay, type NDKRelaySet } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK({ + explicitRelayUrls: ["wss://relay.damus.io", "wss://relay.nostr.band", "wss://nos.lol"], +}); + +await ndk.connect(); + +const event = new NDKEvent(ndk, { + kind: 1, + content: "Hello Nostr!", +}); + +event.on("published", (data: { relaySet: NDKRelaySet; publishedToRelays: Set }) => { + // Get all relays where the event was successfully published + console.log("Published to:", data.publishedToRelays); +}); diff --git a/core/docs/snippets/replace_event.ts b/core/docs/snippets/replace_event.ts new file mode 100644 index 000000000..7dd09ae92 --- /dev/null +++ b/core/docs/snippets/replace_event.ts @@ -0,0 +1,14 @@ +import NDK from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); +const existingEvent = await ndk.fetchEvent("574033c986bea1d7493738b46fec1bb98dd6a826391d6aa893137e89790027ec"); // fetch the event to replace + +if (existingEvent) { + existingEvent.tags.push( + ["p", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"], // follow a new user + ); + + await existingEvent.publish(); // this will NOT work + + await existingEvent.publishReplaceable(); // this WILL work +} diff --git a/core/docs/snippets/sign_event.ts b/core/docs/snippets/sign_event.ts new file mode 100644 index 000000000..efc8def1a --- /dev/null +++ b/core/docs/snippets/sign_event.ts @@ -0,0 +1,9 @@ +import NDK, { NDKEvent, NDKNip07Signer } from "@nostr-dev-kit/ndk"; + +const nip07signer = new NDKNip07Signer(); +const ndk = new NDK({ signer: nip07signer }); + +const event = new NDKEvent(ndk); +event.kind = 1; +event.content = "Hello world"; +await event.sign(); diff --git a/core/docs/snippets/sign_event_nsec.ts b/core/docs/snippets/sign_event_nsec.ts new file mode 100644 index 000000000..632b142ef --- /dev/null +++ b/core/docs/snippets/sign_event_nsec.ts @@ -0,0 +1,9 @@ +import NDK, { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; + +const privateKeySigner = NDKPrivateKeySigner.generate(); +const ndk = new NDK({ signer: privateKeySigner }); + +const event = new NDKEvent(ndk); +event.kind = 1; +event.content = "Hello world"; +await event.sign(); diff --git a/core/snippets/event/signing-with-different-signers.md b/core/docs/snippets/sign_event_with_other_signers.ts similarity index 54% rename from core/snippets/event/signing-with-different-signers.md rename to core/docs/snippets/sign_event_with_other_signers.ts index 1f5b5fffd..cd9f5dd08 100644 --- a/core/snippets/event/signing-with-different-signers.md +++ b/core/docs/snippets/sign_event_with_other_signers.ts @@ -1,11 +1,4 @@ -# Signing events with different signers - -NDK uses the default signer `ndk.signer` to sign events. - -But you can specify the use of a different signer to sign with different pubkeys. - -```ts -import { NDKPrivateKeySigner, NDKEvent } from "@nostr-dev-kit/ndk"; +import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; const signer1 = NDKPrivateKeySigner.generate(); const pubkey1 = signer1.pubkey; @@ -15,7 +8,7 @@ event1.kind = 1; event1.content = "Hello world"; await event1.sign(signer1); -event1.pubkey === pubkey1; // true +console.log(event1.pubkey === pubkey1); // true const signer2 = NDKPrivateKeySigner.generate(); const pubkey2 = signer2.pubkey; @@ -25,5 +18,4 @@ event2.kind = 1; event2.content = "Hello world"; await event2.sign(signer2); -event2.pubkey === pubkey2; // true -``` +console.log(event2.pubkey === pubkey2); // true diff --git a/core/docs/snippets/sign_with_bunker.ts b/core/docs/snippets/sign_with_bunker.ts new file mode 100644 index 000000000..ae7b0af0e --- /dev/null +++ b/core/docs/snippets/sign_with_bunker.ts @@ -0,0 +1,17 @@ +// provided by the user +import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; + +const signerConnectionString = "bunker://...."; +const ndk = new NDK(); + +// local keypair generated when signer if first initialised +const clientKeypair = NDKPrivateKeySigner.generate(); // +const clientNsec = clientKeypair.nsec; + +// initiate NIP-46 signer +const signer = NDKNip46Signer.bunker(ndk, signerConnectionString, clientNsec); + +// promise will resolve once the `kind:24133` event is received +const user = await signer.blockUntilReady(); + +console.log("Welcome", user.npub); diff --git a/core/docs/snippets/subscribe.ts b/core/docs/snippets/subscribe.ts new file mode 100644 index 000000000..be985dc09 --- /dev/null +++ b/core/docs/snippets/subscribe.ts @@ -0,0 +1,8 @@ +import NDK from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); + +ndk.subscribe( + { kinds: [1] }, // Filters + { closeOnEose: true }, // Options (no explicit relays specified) +); diff --git a/core/docs/snippets/subscribe_event_attach.ts b/core/docs/snippets/subscribe_event_attach.ts new file mode 100644 index 000000000..fa1287adb --- /dev/null +++ b/core/docs/snippets/subscribe_event_attach.ts @@ -0,0 +1,19 @@ +import NDK from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); + +const subscription = ndk.subscribe( + { kinds: [1] }, // Filters + { closeOnEose: true }, // Options (no explicit relays specified) +); + +// Attach handlers later +subscription.on("event", (event) => { + console.log("Received event:", event.id); +}); +subscription.on("eose", () => { + console.log("Initial events loaded"); +}); + +// Remember to stop the subscription when it's no longer needed +// setTimeout(() => subscription.stop(), 5000); diff --git a/core/docs/snippets/subscribe_event_handlers.ts b/core/docs/snippets/subscribe_event_handlers.ts new file mode 100644 index 000000000..c523637a7 --- /dev/null +++ b/core/docs/snippets/subscribe_event_handlers.ts @@ -0,0 +1,22 @@ +import NDK, { type NDKEvent, type NDKRelay, type NDKSubscription } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); + +ndk.subscribe( + { kinds: [1] }, // Filters + { closeOnEose: true }, // Options (no explicit relays specified) + { + // Direct handlers via autoStart parameter (now the 3rd argument) + onEvent: (event: NDKEvent, relay?: NDKRelay) => { + // Called for events received from relays after the initial cache load (if onEvents is used) + console.log("Received event from relay (id):", event.id, relay); + }, + onEvents: (events: NDKEvent[]) => { + // Parameter renamed to 'events' + console.log(`Received ${events.length} events from cache initially.`); + }, + onEose: (subscription: NDKSubscription) => { + console.log("Subscription reached EOSE:", subscription.internalId); + }, + }, +); diff --git a/core/docs/snippets/subscribe_relay_targetting.ts b/core/docs/snippets/subscribe_relay_targetting.ts new file mode 100644 index 000000000..c5b734662 --- /dev/null +++ b/core/docs/snippets/subscribe_relay_targetting.ts @@ -0,0 +1,17 @@ +import NDK from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); + +// Subscription that ONLY accepts events from relay-a.com +const exclusiveSub = ndk.subscribe( + { kinds: [7] }, + { + relayUrls: ["wss://relay-a.com"], + exclusiveRelay: true, // 🔑 Key option + onEvent: (event) => { + console.log("Event from relay-a.com:", event.content); + // This will ONLY fire for events from relay-a.com + // Events from relay-b.com or relay-c.com are rejected + }, + }, +); diff --git a/core/docs/snippets/subscribe_relayset.ts b/core/docs/snippets/subscribe_relayset.ts new file mode 100644 index 000000000..58b23e456 --- /dev/null +++ b/core/docs/snippets/subscribe_relayset.ts @@ -0,0 +1,12 @@ +import NDK, {NDKRelaySet} from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); +const explicitRelaySet = NDKRelaySet.fromRelayUrls(["wss://explicit.relay"], ndk); +ndk.subscribe( + {kinds: [7]}, // Filters + { + // Options object now includes relaySet + closeOnEose: true, + relaySet: explicitRelaySet, + }, +); diff --git a/core/docs/snippets/tag_user.ts b/core/docs/snippets/tag_user.ts new file mode 100644 index 000000000..7ac334689 --- /dev/null +++ b/core/docs/snippets/tag_user.ts @@ -0,0 +1,9 @@ +import NDK, { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); +const event = new NDKEvent(ndk, { + kind: NDKKind.Text, + content: + "Hello, nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft this is a test from an NDK snippet.", +}); +await event.sign(); diff --git a/core/docs/snippets/tag_user_result.json b/core/docs/snippets/tag_user_result.json new file mode 100644 index 000000000..016f3614e --- /dev/null +++ b/core/docs/snippets/tag_user_result.json @@ -0,0 +1,9 @@ +{ + "created_at": 1742904504, + "content": "Hello, nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft this is a test from an NDK snippet.", + "tags": [["p", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"]], + "kind": 1, + "pubkey": "cbf66fa8cf9877ba98cd218a96d77bed5abdbfd56fdd3d0393d7859d58a313fb", + "id": "26df08155ceb82de8995081bf63a36017cbfd3a616fe49820d8427d22e0af20f", + "sig": "eb6125248cf4375d650b13fa284e81f4270eaa8cb3cae6366ab8cda27dc99c1babe5b5a2782244a9673644f53efa72aba6973ac3fc5465cf334413d90f4ea1b0" +} diff --git a/core/docs/snippets/tsconfig.json b/core/docs/snippets/tsconfig.json new file mode 100644 index 000000000..d052ac246 --- /dev/null +++ b/core/docs/snippets/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "module": "preserve", + "noEmit": true, + "lib": [ + "es2022" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/core/docs/snippets/user-fetching.ts b/core/docs/snippets/user-fetching.ts new file mode 100644 index 000000000..ab3722e7c --- /dev/null +++ b/core/docs/snippets/user-fetching.ts @@ -0,0 +1,21 @@ +import NDK from "@nostr-dev-kit/ndk"; + +const ndk = new NDK(); + +// From npub +const user1 = await ndk.fetchUser("npub1n0sturny6w9zn2wwexju3m6asu7zh7jnv2jt2kx6tlmfhs7thq0qnflahe"); + +// From nprofile (includes relay hints) +const user2 = await ndk.fetchUser( + "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p", +); + +// From hex pubkey +const user3 = await ndk.fetchUser("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"); + +// From NIP-05 identifier +const user4 = await ndk.fetchUser("pablo@test.com"); +const user5 = await ndk.fetchUser("test.com"); // Uses _@test.com + +// Note: fetchUser is async and returns a Promise +// For NIP-05 lookups, it may return undefined if the address cannot be resolved diff --git a/core/docs/snippets/zap.ts b/core/docs/snippets/zap.ts new file mode 100644 index 000000000..a32a5d2dd --- /dev/null +++ b/core/docs/snippets/zap.ts @@ -0,0 +1,17 @@ +// Import the package +import NDK, { NDKZapper } from "@nostr-dev-kit/ndk"; +import { NDKWebLNWallet } from "@nostr-dev-kit/wallet"; + +// Create a new NDK instance with explicit relays +const ndk = new NDK(); + +const wallet = new NDKWebLNWallet(ndk); + +ndk.wallet = wallet; + +const user = await ndk.fetchUser("pablo@f7z.io"); +if (user) { + const zapper = new NDKZapper(user, 1000, "msat", { ndk }); + + await zapper.zap(); +} diff --git a/core/snippets/testing/event-generation.md b/core/docs/testing/event-generation.md similarity index 100% rename from core/snippets/testing/event-generation.md rename to core/docs/testing/event-generation.md diff --git a/core/snippets/testing/mock-relays.md b/core/docs/testing/mock-relays.md similarity index 100% rename from core/snippets/testing/mock-relays.md rename to core/docs/testing/mock-relays.md diff --git a/core/snippets/testing/nutzap-testing.md b/core/docs/testing/nutzap-testing.md similarity index 100% rename from core/snippets/testing/nutzap-testing.md rename to core/docs/testing/nutzap-testing.md diff --git a/core/snippets/testing/relay-pool-testing.md b/core/docs/testing/relay-pool-testing.md similarity index 100% rename from core/snippets/testing/relay-pool-testing.md rename to core/docs/testing/relay-pool-testing.md diff --git a/core/docs/tutorial/auth.md b/core/docs/tutorial/auth.md deleted file mode 100644 index 38a32fea0..000000000 --- a/core/docs/tutorial/auth.md +++ /dev/null @@ -1,31 +0,0 @@ -# Relay Authentication - -NIP-42 defines that relays can request authentication from clients. - -NDK makes working with NIP-42 very simple. NDK uses an `NDKAuthPolicy` callback to provide a way to handle -authentication requests. - -* Relays can have specific `NDKAuthPolicy` functions. -* NDK can be configured with a default `relayAuthDefaultPolicy` function. -* NDK provides some generic policies: - * `NDKAuthPolicies.signIn`: Authenticate to the relay (using the `ndk.signer` signer). - * `NDKAuthPolicies.disconnect`: Immediately disconnect from the relay if asked to authenticate. - -```ts -import { NDK, NDKRelayAuthPolicies } from "@nostr-dev-kit/ndk"; - -const ndk = new NDK(); -ndk.addExplicitRelay("wss://relay.f7z.io", NDKRelayAuthPolicies.signIn({ndk})); -``` - -Clients should typically allow their users to choose where to authenticate. This can be accomplished by returning the -decision the user made from the `NDKAuthPolicy` function. - -```ts -import { NDK, NDKRelayAuthPolicies } from "@nostr-dev-kit/ndk"; - -const ndk = new NDK(); -ndk.relayAuthDefaultPolicy = (relay: NDKRelay) => { - return confirm(`Authenticate to ${relay.url}?`); -}; -``` diff --git a/core/docs/tutorial/nip19.md b/core/docs/tutorial/nip19.md deleted file mode 100644 index c6b19092b..000000000 --- a/core/docs/tutorial/nip19.md +++ /dev/null @@ -1,188 +0,0 @@ -# Working with NIP-19 Identifiers - -NDK provides comprehensive support for NIP-19 identifiers (npub, nprofile, nevent, etc.), both for encoding/decoding -data and for working with NDK entities. - -## Direct NIP-19 Utilities - -NDK re-exports all NIP-19 utilities from nostr-tools for lightweight data conversion without needing to instantiate NDK -objects: - -```typescript -import { nip19 } from '@nostr-dev-kit/ndk'; - -// Encoding -const npub = nip19.npubEncode(pubkey); -const nsec = nip19.nsecEncode(privateKey); -const note = nip19.noteEncode(eventId); - -// Encoding with metadata -const nprofile = nip19.nprofileEncode({ - pubkey: "hexPubkey", - relays: ["wss://relay1.example.com", "wss://relay2.example.com"] -}); - -const nevent = nip19.neventEncode({ - id: eventId, - relays: ["wss://relay.example.com"], - author: authorPubkey -}); - -const naddr = nip19.naddrEncode({ - kind: 30023, - pubkey: authorPubkey, - identifier: "article-slug", - relays: ["wss://relay.example.com"] -}); - -// Decoding -const decoded = nip19.decode("npub1..."); -console.log(decoded.type); // "npub" -console.log(decoded.data); // hex pubkey - -// Type-specific decoding -if (decoded.type === 'nprofile') { - console.log(decoded.data.pubkey); - console.log(decoded.data.relays); -} -``` - -## Creating NDK Users from NIP-19 - -The `ndk.fetchUser()` method accepts NIP-19 encoded strings directly, automatically detecting and decoding the format: - -```typescript -import NDK from '@nostr-dev-kit/ndk'; - -const ndk = new NDK({ /* ... */ }); - -// From npub -const user1 = await ndk.fetchUser("npub1n0sturny6w9zn2wwexju3m6asu7zh7jnv2jt2kx6tlmfhs7thq0qnflahe"); - -// From nprofile (includes relay hints) -const user2 = await ndk.fetchUser("nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"); - -// From hex pubkey -const user3 = await ndk.fetchUser("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"); - -// From NIP-05 identifier -const user4 = await ndk.fetchUser("pablo@test.com"); -const user5 = await ndk.fetchUser("test.com"); // Uses _@test.com - -// Note: fetchUser is async and returns a Promise -// For NIP-05 lookups, it may return undefined if the address cannot be resolved -``` - -## Encoding NDK Events - -NDK events have a built-in `encode()` method that automatically determines the appropriate NIP-19 format: - -```typescript -const event = new NDKEvent(ndk); -event.kind = 1; -event.content = "Hello Nostr!"; -await event.sign(); - -// Automatically chooses the right format: -// - naddr for parameterized replaceable events -// - nevent for events with relay information -// - note for simple note references -const encoded = event.encode(); - -// Control relay hints -const encodedWith5Relays = event.encode(5); // Include up to 5 relay hints -``` - -## Working with Private Keys - -The `NDKPrivateKeySigner` can be instantiated with an nsec: - -```typescript -import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; - -// From nsec -const signer = new NDKPrivateKeySigner("nsec1..."); - -// From hex private key -const signer2 = new NDKPrivateKeySigner("hexPrivateKey"); -``` - -## Common Use Cases - -### Converting between formats - -```typescript -import { nip19 } from '@nostr-dev-kit/ndk'; - -// Convert hex pubkey to npub -function hexToNpub(hexPubkey: string): string { - return nip19.npubEncode(hexPubkey); -} - -// Extract pubkey from any NIP-19 identifier -function extractPubkey(nip19String: string): string | undefined { - const decoded = nip19.decode(nip19String); - - switch (decoded.type) { - case 'npub': - return decoded.data; - case 'nprofile': - return decoded.data.pubkey; - case 'naddr': - return decoded.data.pubkey; - case 'nevent': - return decoded.data.author; - default: - return undefined; - } -} -``` - -### Sharing content with relay hints - -```typescript -// Create shareable event reference with relay hints -const event = await ndk.fetchEvent({ id: eventId }); -const shareableLink = event.encode(3); // Include up to 3 relay hints - -// Create user profile reference with relays -const user = ndk.getUser({ pubkey: userPubkey }); -const nprofile = nip19.nprofileEncode({ - pubkey: user.pubkey, - relays: ["wss://relay.damus.io", "wss://nos.lol"] -}); -``` - -### Validating NIP-19 strings - -```typescript -import { nip19 } from '@nostr-dev-kit/ndk'; - -function isValidNip19(str: string): boolean { - try { - nip19.decode(str); - return true; - } catch { - return false; - } -} - -function isNpub(str: string): boolean { - try { - const decoded = nip19.decode(str); - return decoded.type === 'npub'; - } catch { - return false; - } -} -``` - -## Best Practices - -1. **Use NIP-19 for user-facing displays**: Always show npub/nprofile to users instead of hex pubkeys -2. **Include relay hints for better discovery**: When sharing events or profiles, include 2-3 relay hints -3. **Handle decoding errors**: Always wrap `nip19.decode()` in try-catch blocks -4. **Use the right tool**: - - Use `nip19` utilities for pure data conversion - - Use `ndk.getUser()` when you need an NDK User object - - Use `event.encode()` for encoding existing NDK events \ No newline at end of file diff --git a/core/docs/tutorial/publishing.md b/core/docs/tutorial/publishing.md deleted file mode 100644 index a36735878..000000000 --- a/core/docs/tutorial/publishing.md +++ /dev/null @@ -1,26 +0,0 @@ -# Publishing Events - -## Optimistic publish lifecycle - -Read more about the [local-first](./local-first.md) mode of operation. - -## Publishing Replaceable Events - -Some events in Nostr allow for replacement. - -Kinds `0`, `3`, range `10000-19999`. - -Range `30000-39999` is parameterized replaceable events, which means that multiple events of the same kind under the -same pubkey can exist and are differentiated via their `d` tag. - -Since replaceable events depend on having a newer `created_at`, NDK provides a convenience method to reset `id`, `sig`, -and `created_at` to allow for easy replacement: `event.publishReplaceable()` - -```ts -const existingEvent = await ndk.fetchEvent({ kinds: [0], authors: []}); // fetch the event to replace -existingEvent.tags.push( - [ "p", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" ] // follow a new user -); -existingEvent.publish(); // this will NOT work -existingEvent.publishReplaceable(); // this WILL work -``` diff --git a/core/docs/tutorial/zaps/index.md b/core/docs/tutorial/zaps/index.md deleted file mode 100644 index b8cb74117..000000000 --- a/core/docs/tutorial/zaps/index.md +++ /dev/null @@ -1,17 +0,0 @@ -# Zaps - -NDK comes with an interface to make zapping as simple as possible. - -```ts -const user = await ndk.fetchUser("pablo@f7z.io"); -const lnPay = ({ pr: string }) => { - console.log("please pay to complete the zap", pr); -}; -const zapper = new NDKZapper(user, 1000, { lnPay }); -zapper.zap(); -``` - -## NDK-Wallet - -Refer to the Wallet section of the tutorial to learn more about zapping. NDK-wallet provides many conveniences to -integrate with zaps. diff --git a/core/snippets/event/basic.md b/core/snippets/event/basic.md deleted file mode 100644 index ffc0368da..000000000 --- a/core/snippets/event/basic.md +++ /dev/null @@ -1,18 +0,0 @@ -# Basic Nostr Event generation - -NDK uses `NDKEvent` as the basic interface to generate and handle nostr events. - -## Generating a basic event - -```ts -import NDK, { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; - -const ndk = new NDK(/* initialization options for the ndk singleton */); - -const event = new NDKEvent(ndk, { - kind: NDKKind.Text, - content: "Hello world", -}); -``` - -There is no need to fill in the event's `id`, `tags`, `pubkey`, `created_at`, `sig` -- when these are empty, NDK will automatically fill them in with the appropriate values. diff --git a/core/snippets/event/publish-tracking.md b/core/snippets/event/publish-tracking.md deleted file mode 100644 index 9dcc57ec9..000000000 --- a/core/snippets/event/publish-tracking.md +++ /dev/null @@ -1,158 +0,0 @@ -# Event Publish Tracking - -NDK provides comprehensive tracking of where events are published and the status of each publish attempt. - -## Basic Usage - -When you publish an event, NDK tracks the status of each relay: - -```typescript -import NDK from "@nostr-dev-kit/ndk"; - -const ndk = new NDK({ - explicitRelayUrls: ["wss://relay.damus.io", "wss://relay.nostr.band", "wss://nos.lol"], -}); - -await ndk.connect(); - -const event = new NDKEvent(ndk, { - kind: 1, - content: "Hello Nostr!", -}); - -try { - await event.publish(); - - // Get all relays where the event was successfully published - console.log("Published to:", event.publishedToRelays); - // Output: ["wss://relay.damus.io", "wss://nos.lol"] - - // Check if published to a specific relay - if (event.wasPublishedTo("wss://relay.damus.io")) { - console.log("Successfully published to Damus relay"); - } -} catch (error) { - // Even if publish fails, you can see which relays succeeded - console.log("Published to:", event.publishedToRelays); - - // Get failed relays and their errors - const failures = event.failedPublishesToRelays; - for (const [relay, error] of failures) { - console.error(`Failed to publish to ${relay}:`, error.message); - } -} -``` - -## Detailed Status Information - -Each relay has detailed status information including timestamps: - -```typescript -// Get detailed status for all relays -for (const [relayUrl, status] of event.publishRelayStatus) { - console.log(`Relay: ${relayUrl}`); - console.log(` Status: ${status.status}`); // "pending", "success", or "error" - console.log(` Timestamp: ${new Date(status.timestamp)}`); - if (status.error) { - console.log(` Error: ${status.error.message}`); - } -} -``` - -## Handling Partial Failures - -When publishing to multiple relays, some may succeed while others fail: - -```typescript -try { - // Require at least 2 relays to receive the event - await event.publish(undefined, 5000, 2); -} catch (error) { - if (error instanceof NDKPublishError) { - console.log("Published to", error.publishedToRelays.size, "relays"); - console.log("Failed on", error.errors.size, "relays"); - - // The event object still tracks all statuses - console.log("Successful relays:", event.publishedToRelays); - console.log("Failed relays:", Array.from(event.failedPublishesToRelays.keys())); - } -} -``` - -## Custom Relay Sets - -You can publish to specific relay sets and track their status: - -```typescript -const customRelaySet = NDKRelaySet.fromRelayUrls( - ["wss://relay.snort.social", "wss://relay.primal.net"], - ndk, -); - -await event.publish(customRelaySet); - -// Check which of the custom relays received the event -for (const relayUrl of customRelaySet.relayUrls) { - if (event.wasPublishedTo(relayUrl)) { - console.log(`✓ Published to ${relayUrl}`); - } else { - console.log(`✗ Failed to publish to ${relayUrl}`); - } -} -``` - -## Republishing Events - -When republishing an event, the relay status is cleared and updated: - -```typescript -// First publish attempt -await event.publish(); -console.log("First publish:", event.publishedToRelays); - -// Republish to different relays -const newRelaySet = NDKRelaySet.fromRelayUrls(["wss://relay.nostr.bg", "wss://nostr.wine"], ndk); - -await event.publish(newRelaySet); -console.log("Second publish:", event.publishedToRelays); -// Only shows relays from the second publish -``` - -## Monitoring Relay Performance - -You can use publish tracking to monitor relay performance: - -```typescript -const publishStats = new Map(); - -// Track multiple publishes -for (const event of events) { - await event.publish(); - - for (const [relay, status] of event.publishRelayStatus) { - const stats = publishStats.get(relay) || { success: 0, failure: 0 }; - if (status.status === "success") { - stats.success++; - } else if (status.status === "error") { - stats.failure++; - } - publishStats.set(relay, stats); - } -} - -// Analyze relay performance -for (const [relay, stats] of publishStats) { - const total = stats.success + stats.failure; - const successRate = ((stats.success / total) * 100).toFixed(1); - console.log(`${relay}: ${successRate}% success rate`); -} -``` - -## Event Status Properties - -- `event.publishedToRelays` - Array of relay URLs where the event was successfully published -- `event.failedPublishesToRelays` - Map of relay URLs to their errors -- `event.publishRelayStatus` - Map of all relay URLs to their detailed status -- `event.wasPublishedTo(url)` - Check if successfully published to a specific relay -- `event.publishStatus` - Overall status: "pending", "success", or "error" -- `event.publishError` - Error if the overall publish failed diff --git a/core/snippets/event/tagging-users-and-events.md b/core/snippets/event/tagging-users-and-events.md deleted file mode 100644 index 1604e0108..000000000 --- a/core/snippets/event/tagging-users-and-events.md +++ /dev/null @@ -1,32 +0,0 @@ -# Tagging users and events - -NDK automatically adds the appropriate tags for mentions in the content. - -If the user wants to mention a user or an event, NDK will automatically add the appropriate tags: - -## Tagging a user - -```ts -import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; - -const event = new NDKEvent(ndk, { - kind: NDKKind.Text, - content: - "Hello, nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft this is a test from an NDK snippet.", -}); -await event.sign(); -``` - -Calling `event.sign()` will finalize the event, adding the appropriate tags, The resulting event will look like: - -```json -{ - "created_at": 1742904504, - "content": "Hello, nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft this is a test from an NDK snippet.", - "tags": [["p", "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"]], - "kind": 1, - "pubkey": "cbf66fa8cf9877ba98cd218a96d77bed5abdbfd56fdd3d0393d7859d58a313fb", - "id": "26df08155ceb82de8995081bf63a36017cbfd3a616fe49820d8427d22e0af20f", - "sig": "eb6125248cf4375d650b13fa284e81f4270eaa8cb3cae6366ab8cda27dc99c1babe5b5a2782244a9673644f53efa72aba6973ac3fc5465cf334413d90f4ea1b0" -} -``` diff --git a/core/snippets/subscription/exclusive-relay.md b/core/snippets/subscription/exclusive-relay.md deleted file mode 100644 index e95a14a00..000000000 --- a/core/snippets/subscription/exclusive-relay.md +++ /dev/null @@ -1,227 +0,0 @@ -# Exclusive Relay Subscriptions - -By default, NDK subscriptions use cross-subscription matching: when an event comes in from any relay, it's delivered to all subscriptions whose filters match, regardless of which relays the subscription was targeting. - -The `exclusiveRelay` option allows you to create subscriptions that **only** accept events from their specified relays, ignoring events that match the filter but come from other relays. - -## Basic Usage - -```typescript -import NDK from '@nostr-dev-kit/ndk'; - -const ndk = new NDK({ - explicitRelayUrls: [ - 'wss://relay-a.com', - 'wss://relay-b.com', - 'wss://relay-c.com' - ] -}); - -await ndk.connect(); - -// Subscription that ONLY accepts events from relay-a.com -const exclusiveSub = ndk.subscribe( - { kinds: [1], authors: ['pubkey...'] }, - { - relayUrls: ['wss://relay-a.com'], - exclusiveRelay: true, // 🔑 Key option - onEvent: (event) => { - console.log('Event from relay-a.com:', event.content); - // This will ONLY fire for events from relay-a.com - // Events from relay-b.com or relay-c.com are rejected - } - } -); -``` - -## Default Behavior (Cross-Subscription Matching) - -Without `exclusiveRelay`, subscriptions receive events from any relay: - -```typescript -// Default behavior - accepts events from ANY relay -const normalSub = ndk.subscribe( - { kinds: [1], authors: ['pubkey...'] }, - { - relayUrls: ['wss://relay-a.com'], - exclusiveRelay: false, // or omit (default) - onEvent: (event) => { - // This fires for events from relay-a.com, relay-b.com, relay-c.com - // or any other relay, as long as the filter matches - } - } -); -``` - -## Use Cases - -### 1. Relay-Specific Data Fetching - -Fetch events exclusively from a specific relay: - -```typescript -// Only get events from a specific community relay -const communitySub = ndk.subscribe( - { kinds: [1], '#t': ['community'] }, - { - relayUrls: ['wss://community-relay.example.com'], - exclusiveRelay: true - } -); -``` - -### 2. Relay Isolation Testing - -Test relay-specific behavior: - -```typescript -// Test what a specific relay returns -const testSub = ndk.subscribe( - { kinds: [1], limit: 10 }, - { - relayUrls: ['wss://test-relay.com'], - exclusiveRelay: true, - closeOnEose: true, - onEose: () => { - console.log('Finished fetching from test-relay.com'); - } - } -); -``` - -### 3. Relay-Based Routing - -Route events based on relay provenance: - -```typescript -const publicRelaySub = ndk.subscribe( - { kinds: [1] }, - { - relayUrls: ['wss://public-relay.com'], - exclusiveRelay: true, - onEvent: (event) => { - console.log('Public event:', event.content); - } - } -); - -const privateRelaySub = ndk.subscribe( - { kinds: [1] }, - { - relayUrls: ['wss://private-relay.com'], - exclusiveRelay: true, - onEvent: (event) => { - console.log('Private event:', event.content); - } - } -); -``` - -## Using NDKRelaySet - -You can also use `NDKRelaySet` with `exclusiveRelay`: - -```typescript -import { NDKRelaySet } from '@nostr-dev-kit/ndk'; - -const relaySet = NDKRelaySet.fromRelayUrls( - ['wss://relay-a.com', 'wss://relay-b.com'], - ndk -); - -const sub = ndk.subscribe( - { kinds: [1] }, - { - relaySet, - exclusiveRelay: true - } -); - -// Only receives events from relay-a.com or relay-b.com -``` - -## Edge Cases - -### Cached Events - -Cached events are checked against their known relay provenance. If a cached event was previously seen on a relay in your exclusive relaySet, it will be delivered: - -```typescript -const sub = ndk.subscribe( - { kinds: [1] }, - { - relayUrls: ['wss://relay-a.com'], - exclusiveRelay: true, - cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST - } -); - -// Cached events that came from relay-a.com: ✅ Delivered -// Cached events from other relays: ❌ Rejected -``` - -### Optimistic Publishes - -Optimistic publishes (local events before relay confirmation) respect the `skipOptimisticPublishEvent` setting: - -```typescript -const sub = ndk.subscribe( - { kinds: [1] }, - { - relayUrls: ['wss://relay-a.com'], - exclusiveRelay: true, - skipOptimisticPublishEvent: false // Accept optimistic publishes - } -); - -// Optimistic publishes: ✅ Delivered (if skipOptimisticPublishEvent is false) -``` - -### No RelaySet Specified - -If `exclusiveRelay: true` but no `relaySet` or `relayUrls` is specified, the check is not applied: - -```typescript -const sub = ndk.subscribe( - { kinds: [1] }, - { - exclusiveRelay: true // Has no effect without relaySet/relayUrls - } -); - -// Behaves like a normal subscription - accepts events from any relay -``` - -## Combining Exclusive and Non-Exclusive Subscriptions - -You can mix exclusive and non-exclusive subscriptions in the same NDK instance: - -```typescript -// Exclusive subscription - only relay-a.com -const exclusiveSub = ndk.subscribe( - { kinds: [1], '#t': ['exclusive'] }, - { - relayUrls: ['wss://relay-a.com'], - exclusiveRelay: true - } -); - -// Non-exclusive subscription - any relay -const globalSub = ndk.subscribe( - { kinds: [1], '#t': ['global'] }, - { - exclusiveRelay: false - } -); - -// exclusiveSub: Only gets #t=exclusive events from relay-a.com -// globalSub: Gets #t=global events from any connected relay -``` - -## Performance Considerations - -The `exclusiveRelay` check happens after filter matching, so there's minimal performance impact. The check only applies to subscriptions that have both: -- `exclusiveRelay: true` -- A specified `relaySet` or `relayUrls` - -All other subscriptions skip the relay provenance check entirely. diff --git a/core/snippets/user/generate-keys.md b/core/snippets/user/generate-keys.md deleted file mode 100644 index 1d7c74b98..000000000 --- a/core/snippets/user/generate-keys.md +++ /dev/null @@ -1,45 +0,0 @@ -# Generate Keys - -This snippet demonstrates how to generate a new key pair and obtain all its various formats (private key, public key, nsec, npub). - -```typescript -import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; - -const signer = NDKPrivateKeySigner.generate(); -const privateKey = signer.privateKey!; // Get the hex private key -const publicKey = signer.pubkey; // Get the hex public key -const nsec = signer.nsec; // Get the private key in nsec format -const npub = signer.userSync.npub; // Get the public key in npub format -``` - -You can use these different formats for different purposes: - -- `privateKey`: Raw private key for cryptographic operations -- `publicKey`: Raw public key (hex format) for verification -- `nsec`: Encoded private key format (bech32) - used for secure sharing when needed -- `npub`: Encoded public key format (bech32) - used for user identification - -## Secure Storage with Password Protection - -For storing keys securely with password protection, use NIP-49 (ncryptsec format): - -```typescript -import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; - -const signer = NDKPrivateKeySigner.generate(); - -// Encrypt with a password -const password = "user-chosen-password"; -const ncryptsec = signer.encryptToNcryptsec(password); - -// Store securely (e.g., localStorage) -localStorage.setItem("encrypted_key", ncryptsec); - -// Later, restore the signer -const restoredSigner = NDKPrivateKeySigner.fromNcryptsec( - localStorage.getItem("encrypted_key"), - password, -); -``` - -See [Encrypted Keys (NIP-49)](./encrypted-keys-nip49.md) for more examples and best practices. diff --git a/core/snippets/user/get-profile.md b/core/snippets/user/get-profile.md deleted file mode 100644 index 2f1358a2b..000000000 --- a/core/snippets/user/get-profile.md +++ /dev/null @@ -1,38 +0,0 @@ -# Getting Profile Information - -This snippet demonstrates how to fetch user profile information using NDK. - -## Basic Profile Fetching - -Use `NDKUser`'s `fetchProfile()` to fetch a user's profile. - -```typescript -// Get an NDKUser instance for a specific pubkey -const user = ndk.getUser({ pubkey: "user_pubkey_here" }); - -// Fetch their profile -try { - const profile = await user.fetchProfile(); - console.log("Profile loaded:", profile); -} catch (e) { - console.error("Error fetching profile:", e); -} -``` - -## Profile Data Structure - -The profile object contains standard Nostr profile fields: - -```typescript -interface NDKUserProfile { - name?: string; - displayName?: string; - image?: string; - banner?: string; - about?: string; - nip05?: string; - lud06?: string; // Lightning Address - lud16?: string; // LNURL - website?: string; -} -``` diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..764bc3756 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +.vitepress/cache +.vitepress/dist +public/* +/snippets/pnpm-lock.yaml diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts deleted file mode 100644 index 4a054c1b7..000000000 --- a/docs/.vitepress/config.mts +++ /dev/null @@ -1,137 +0,0 @@ -import { defineConfig } from "vitepress"; - -// https://vitepress.dev/reference/site-config -export default defineConfig({ - title: "NDK", - description: "NDK Docs", - base: "/ndk/", - ignoreDeadLinks: true, - markdown: { - theme: { - light: 'github-light', - dark: 'github-dark' - } - }, - themeConfig: { - // https://vitepress.dev/reference/default-theme-config - nav: [ - { text: "Home", link: "/" }, - { text: "API Reference", link: "/api/", target: "_blank" }, - { text: "Cookbook", link: "/cookbook/" }, - { text: "Snippets", link: "/snippets/" }, - { text: "Wiki", link: "https://wikifreedia.xyz/?c=NDK", target: "_blank" }, - ], - - sidebar: [ - { - text: "Getting Started", - items: [ - { text: "Introduction", link: "/getting-started/introduction" }, - { text: "Usage", link: "/getting-started/usage" }, - { text: "Signers", link: "/getting-started/signers" }, - ], - }, - { - text: "Tutorial", - items: [ - { text: "Local-first", link: "/tutorial/local-first" }, - { text: "Publishing", link: "/tutorial/publishing" }, - { - text: "Subscription Management", - link: "/tutorial/subscription-management", - }, - { text: "Mute Filtering", link: "/tutorial/mute-filtering" }, - { text: "Signer Persistence", link: "/tutorial/signer-persistence" }, - { text: "Speed", link: "/tutorial/speed" }, - { text: "Zaps", link: "/tutorial/zaps" }, - ], - }, - { - text: "Cache Adapters", - items: [ - { text: "In-memory LRU", link: "/cache/memory" }, - { text: "In-memory + dexie", link: "/cache/dexie" }, - { text: "Local Nostr Relay", link: "/cache/nostr" }, - { - text: "SQLite (WASM)", - link: "/cache/sqlite-wasm/INDEX", - items: [ - { text: "Bundling", link: "/cache/sqlite-wasm/bundling" }, - { text: "Web Worker Setup", link: "/cache/sqlite-wasm/web-worker-setup" } - ] - }, - ], - }, - { - text: "Wallet", - items: [ - { text: "Introduction", link: "/wallet/index" }, - { text: "Nutsack (NIP-60)", link: "/wallet/nutsack" }, - { text: "Nutzaps", link: "/wallet/nutzaps" }, - ], - }, - { - text: "Sync & Negentropy", - items: [ - { text: "Introduction", link: "/sync/index" }, - ], - }, - { - text: "Web of Trust", - items: [ - { text: "Introduction", link: "/wot/index" }, - { text: "Negentropy Integration", link: "/wot/negentropy" }, - ], - }, - { - text: "Wrappers", - items: [ - { text: "NDK Svelte", link: "/wrappers/svelte" }, - { - text: "NDK React Hooks", - link: "/hooks/index", - items: [ - { text: "Session Management", link: "/hooks/session-management" }, - { text: "Muting", link: "/hooks/muting" } - ] - } - ], - }, - { - text: "Sessions", - items: [ - { text: "Introduction", link: "/sessions/index" }, - { text: "Quick Start", link: "/sessions/quick-start" }, - { text: "API Reference", link: "/sessions/api" }, - { text: "Migration Guide", link: "/sessions/migration" } - ], - }, - { - text: "Mobile", - items: [ - { text: "Introduction", link: "/mobile/index" }, - { text: "Session", link: "/mobile/session" }, - { text: "Wallet", link: "/mobile/wallet" }, - ], - }, - { - text: "Blossom (Media)", - items: [ - { text: "Introduction", link: "/blossom/getting-started" }, - ] - }, - { - text: "Testing", - items: [ - { text: "Introduction", link: "/testing/index" }, - ], - }, - { - text: "Internals", - items: [{ text: "Subscription Lifecycle", link: "/internals/subscriptions" }], - }, - ], - - socialLinks: [{ icon: "github", link: "https://github.com/nostr-dev-kit/ndk" }], - }, -}); diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 000000000..abf32c144 --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,213 @@ +import { defineConfig } from "vitepress"; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "NDK", + srcDir: "../", + description: "NDK Docs", + cleanUrls: true, + outDir: "../public", + // base: "/ndk/", + ignoreDeadLinks: true, + markdown: { + theme: { + light: "github-light", + dark: "github-dark", + }, + }, + rewrites: { + "docs/index.md": "index.md", + }, + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: "Home ✅", link: "/docs/index.md" }, + // { text: "API Reference", link: "/api/", target: "_blank" }, + // { text: "Cookbook", link: "/cookbook/" }, + { text: "Snippets ✅", link: "/docs/snippets.md" }, + { text: "Wiki ✅", link: "https://wikifreedia.xyz/?c=NDK", target: "_blank" }, + ], + + sidebar: [ + { + items: [ + { text: "Introduction ✅", link: "/docs/introduction" }, + { text: "About Nostr ✅", link: "/docs/about-nostr" }, + { text: "Contributing ❌", link: "/docs/contributing" }, + { text: "Changelog ✅", link: "/docs/changelogs" }, + ], + }, + { + text: "Getting Started", + items: [ + { text: "Installing ✅", link: "/core/docs/getting-started/installing" }, + { text: "Usage ✅", link: "/core/docs/getting-started/usage" }, + { text: "Debugging ✅", link: "/core/docs/getting-started/debugging" }, + ], + }, + { + text: "Fundamentals", + collapsed: false, + items: [ + { text: "Events ✅", link: "/core/docs/fundamentals/events" }, + { text: "Connecting ✅", link: "/core/docs/fundamentals/connecting" }, + { text: "Publishing ✅", link: "/core/docs/fundamentals/publishing" }, + { text: "Subscribing ✅", link: "/core/docs/fundamentals/subscribing" }, + { text: "Signers ✅", link: "/core/docs/fundamentals/signers" }, + { text: "Zaps ✅", link: "/core/docs/fundamentals/zapping" }, + { text: "Helpers ✅", link: "/core/docs/fundamentals/helpers" }, + ], + }, + { + text: "Sessions", + collapsed: true, + items: [ + { text: "Introduction ✅", link: "/sessions/docs/introduction" }, + { text: "Quick Start ✅", link: "/sessions/docs/quick-start" }, + { text: "Storage Options ✅", link: "/sessions/docs/storage-options" }, + { text: "Multi-Account ✅", link: "/sessions/docs/multi-account" }, + { text: "Migration Guide ✅", link: "/sessions/docs/migration" }, + { text: "Advanced ❌", link: "/sessions/docs/advanced" }, + { text: "Best Practices ❌", link: "/sessions/docs/best-practices" }, + { text: "API Reference ✅", link: "/sessions/docs/api" }, + { text: "README ⛔", link: "/sessions/README" }, + ], + }, + { + text: "Wallet", + collapsed: true, + items: [ + { text: "Introduction ❌", link: "/wallet/README" }, + { text: "WebLN Wallet ❌", link: "/wallet/docs/NDKWebLNWallet" }, + { text: "NWC Wallet ❌", link: "/wallet/docs/NDKNWCWallet" }, + { + text: "CashuWallet (NIP-60) ❌", + link: "/wallet/docs/NDKCashuWallet", + items: [ + { text: "Nutsack ❌", link: "/wallet/docs/nutsack" }, + { text: "Nutzaps ❌", link: "/wallet/docs/nutzaps" }, + { text: "Nutzap Monitor ❌", link: "/wallet/docs/nutzap-monitor" }, + { text: "Monitor State Store ❌", link: "/wallet/docs/nutzap-monitor-state-store" }, + ], + }, + ], + }, + { + text: "Wrappers", + collapsed: true, + items: [ + { + text: "Svelte ⛔", + link: "/svelte/README ⛔", + items: [ + { text: "Blossom ⛔", link: "/svelte/blossom-upload" }, + { text: "Changelog ⛔", link: "/svelte/CHANGELOG.md" }, + ], + }, + { + text: "React ❌", + link: "/react/README", + items: [ + { text: "Getting Started ❌", link: "/react/docs/getting-started" }, + { text: "Muting ❌", link: "/react/muting" }, + { text: "Session Management ❌", link: "/react/session-management" }, + ], + }, + ], + }, + { + text: "Mobile", + collapsed: true, + items: [ + { text: "Introduction ❌", link: "/mobile/README" }, + { text: "Session ⛔", link: "/mobile/session" }, + { text: "Wallet ⛔", link: "/mobile/wallet" }, + { text: "Subscriptions ⛔", link: "/mobile/subscriptions" }, + { text: "Nutzaps ⛔", link: "/mobile/nutzaps" }, + { text: "Mint ⛔", link: "/mobile/mint" }, + ], + }, + { + text: "Blossom (Media)", + collapsed: true, + items: [ + { text: "Introduction ❌", link: "/blossom/README" }, + { text: "Getting Started ⛔", link: "/blossom/getting-started" }, + { text: "Error Handling ⛔", link: "/blossom/error-handling" }, + { text: "Mirroring ⛔", link: "/blossom/mirroring" }, + { text: "Optimization ⛔", link: "/blossom/optimization" }, + ], + }, + { + text: "Advanced Topics", + collapsed: true, + items: [ + { + text: "Signer Persistence ❌", + link: "/core/docs/tutorial/signer-persistence", + }, + { + text: "Speed / Performance ❌", + link: "/core/docs/tutorial/speed", + }, + { + text: "AI Guardrails ✅", + link: "/core/docs/advanced/ai-guardrails", + }, + { + text: "Exclusive Relays ✅", + link: "/core/docs/advanced/exclusive-relay", + }, + { + text: "Encrypting Keys (NIP-49) ✅", + link: "/core/docs/advanced/encrypted-keys-nip49", + }, + { + text: "Subscription Lifecycle ❌", + link: "/core/docs/advanced/subscription-internals", + }, + { + text: "Event Class Registration ❌", + link: "/core/docs/advanced/event-class-registration", + }, + { + text: "Relay Metadata Caching ⛔", + link: "/core/docs/advanced/relay-metadata-caching", + }, + { + text: "Sync & Negentropy ⛔", + link: "/sync", + }, + { + text: "Web of Trust (WOT) ⛔", + link: "/wot/README", + }, + { + text: "Cache Adapters ❌", + collapsed: true, + items: [ + { text: "Memory / LRU ❌", link: "/cache/memory/README" }, + { text: "Dexie / IndexedDB ❌", link: "cache/dexie/README" }, + { text: "Local Nostr Relay ❌", link: "/cache/nostr/README" }, + { text: "Redis ❌", link: "/cache/redis/README" }, + { text: "SQLite ❌", link: "/cache/sqlite/README" }, + { + text: "SQLite WASM ❌", + link: "/cache/sqlite-wasm/README", + items: [ + { text: "Bundling ❌", link: "/cache/sqlite-wasm/bundling" }, + { text: "Web Worker Setup ❌", link: "/cache/sqlite-wasm/web-worker-setup" }, + ], + }, + ], + }, + ], + }, + ], + outline: { + level: [2, 3], + }, + + socialLinks: [{ icon: "github", link: "https://github.com/nostr-dev-kit/ndk" }], + }, +}); diff --git a/docs/TODOS.md b/docs/TODOS.md new file mode 100644 index 000000000..7e48e06e8 --- /dev/null +++ b/docs/TODOS.md @@ -0,0 +1,38 @@ +# Clean up documentation + +Full PR + progress available at https://github.com/nostr-dev-kit/ndk/pull/344 +Preview URL https://ndkdocs.asknostr.site/ + +Some notes: + +- Items with ✅ in sidebar are mostly done +- Items with ❌ in sidebar are mostly done, unless they have things highlighted below +- Items with ⛔ in sidebar have docs, but need to be linked+cleaned up + +## Todo List + +- [x] Move vitepress to root and use rewrites to clean up paths +- [x] Upgrade vitepress +- [x] [Introduction/Essential pages](https://ndkdocs.asknostr.site/core/docs/getting-started/introduction.html) +- [x] [Make sure changelogs can be found](https://ndkdocs.asknostr.site/docs/changelogs.html) +- [x] [Fix snippets](https://ndkdocs.asknostr.site/docs/snippets.html). +- [x] Fundamentals + - [x] [Events](https://ndkdocs.asknostr.site/core/docs/fundamentals/events.html) + - [ ] [Signers WIP](https://ndkdocs.asknostr.site/core/docs/fundamentals/signers.html) + - [ ] Add QR code / Nostrconnect:// docs remote signer docs + - [x] [Publishing](https://ndkdocs.asknostr.site/core/docs/fundamentals/publishing.html) + - [x] [Connecting](https://ndkdocs.asknostr.site/core/docs/fundamentals/connecting.html) + - [ ] Explain/Cleanup Outbox Docs + - [ ] Wallets + - [ ] Zaps + - [ ] ... +- [x] Link to snippets on key pages +- [ ] Try to find (or remove) API reference link +- [ ] Remove warnings (homepage and main intro) about Docs WIP +- [ ] Remove ✅ ❌ ⛔ icons +- [x] Check revert commit https://github.com/nostr-dev-kit/ndk/commit/4a7b086bd1f67f22a6c29adcf4d6c51b4dd7077c + +### Changes made that need double check + +- core/tsconfig -> added paths. This makes the snippets in core/docs/snippets have biome/linting +- upgrade bun 1.3.0 and node to +22. \ No newline at end of file diff --git a/docs/about-nostr.md b/docs/about-nostr.md new file mode 100644 index 000000000..bd8341f72 --- /dev/null +++ b/docs/about-nostr.md @@ -0,0 +1,33 @@ +# About Nostr + +Nostr (Notes and Other Stuff Transmitted by Relays) is an open, censorship-resistant protocol for publishing and +subscribing to events. It’s not a platform or company—it's a simple, extensible standard that anyone can implement. + +## Core Concepts + +- Identities with keys: + - You are your keys. A private key signs your messages; a public key identifies you. + - No accounts, emails, or servers required. +- Events: + - The atomic unit of data (notes, profiles, likes, zaps, etc.). + - Each event is a JSON object signed by the creator’s private key. +- Relays: + - Dumb servers that store and forward events. + - You can publish to many relays and read from many relays. + - Relays don’t authenticate; they verify signatures and apply their own policies. +- Clients: + - Apps that let you create, sign, publish, and read events. + - You can use multiple clients with the same keys—your identity is portable. + +## Why Nostr? + +- **Censorship resistance**: No single relay or company can silence you. +- **Portability**: Your identity and data travel with your keys. +- **Interoperability**: One protocol, many apps—social, chat, marketplaces, media. +- **Simplicity**: Minimal spec, easy to implement, composable extensions. + +## Read more + +- [The nostr protocol](https://github.com/nostr-protocol/nostr) +- [Network Implementation Proposals (NIPs)](https://github.com/nostr-protocol/nips) +- [Awesome Nostr](https://github.com/aljazceru/awesome-nostr) \ No newline at end of file diff --git a/docs/changelogs.md b/docs/changelogs.md new file mode 100644 index 000000000..014f248e7 --- /dev/null +++ b/docs/changelogs.md @@ -0,0 +1,30 @@ +# Changelogs + +We maintain changelogs for each package in the multi-repo. Consult the individual changelogs for more details. + +## Core + +* [@nostr-dev-kit/core](/core/README.md) -> [CHANGELOG.md](/core/CHANGELOG.md) + * [Migration guide from `2.12` to `2.13`](/core/docs/migration/2.12-to-2.13.md) + * [Migration guide from `2.13` to `2.16`](/core/docs/migration/2.13-to-2.16.md) + +## Extras + +* [@nostr-dev-kit/blossom](/blossom/README.html) -> [CHANGELOG.md](/blossom/CHANGELOG.md) +* [@nostr-dev-kit/sessions](/sessions/README.html) -> [CHANGELOG.md](/sessions/CHANGELOG.md) +* [@nostr-dev-kit/sync](/sync) -> [CHANGELOG.md](/sync/CHANGELOG.md) +* [@nostr-dev-kit/wallet](/wallet/README.html) -> [CHANGELOG.md](/wallet/CHANGELOG.md) +* [@nostr-dev-kit/wot](/wot/README.html) -> [CHANGELOG.md](/wot/CHANGELOG.md) + +## Framework Integrations +* [@nostr-dev-kit/react](/react/README.md) -> [CHANGELOG.md](/react/CHANGELOG.md) +* [@nostr-dev-kit/svelte](/svelte/README.md) -> [CHANGELOG.md](/svelte/CHANGELOG.md) + +## Cache Adapters + +* [@nostr-dev-kit/cache-memory](/cache-memory/README.md) -> [CHANGELOG.md](/cache-memory/CHANGELOG.md) +* [@nostr-dev-kit/cache-nostr](/cache-nostr/README.md) -> [CHANGELOG.md](/cache-nostr/CHANGELOG.md) +* [@nostr-dev-kit/cache-redis](/cache-redis/README.md) -> [CHANGELOG.md](/cache-redis/CHANGELOG.md) +* [@nostr-dev-kit/cache-dexie](/cache-dexie/README.md) -> [CHANGELOG.md](/cache-dexie/CHANGELOG.md) +* [@nostr-dev-kit/cache-sqlite](/cache-sqlite/README.md) -> [CHANGELOG.md](/cache-sqlite/CHANGELOG.md) +* [@nostr-dev-kit/cache-sqlite-wasm](/cache-sqlite-wasm/README.md) -> [CHANGELOG.md](/cache-sqlite-wasm/CHANGELOG.md) diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 000000000..8026c9aa3 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,13 @@ +# Contributing + +We work in GitHub using pull requests (PRs). + +- Fork [the repo](https://github.com/nostr-dev-kit/ndk/issues), create a feature branch, make your changes. +- Keep PRs small and focused; include a clear description and rationale. +- Ensure builds/tests pass and follow the project’s style. +- Open a PR to the main branch; reference related issues. +- Be responsive to review feedback; update your PR as needed. + +Bugs can be filed on the [issue tracker](https://github.com/nostr-dev-kit/ndk/issues). + +Thank you for contributing! \ No newline at end of file diff --git a/docs/cookbook/README.md b/docs/cookbook/README.md deleted file mode 100644 index eb7baa57d..000000000 --- a/docs/cookbook/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# NDK Cookbook - -Welcome to the NDK Cookbook! This is a collection of self-contained recipes for building with the Nostr Development Kit (NDK). - -## What is a Cookbook Recipe? - -A cookbook recipe is a focused, practical guide that shows you how to accomplish a specific task with NDK. Unlike tutorials that teach concepts step-by-step, recipes are: - -- **Self-contained**: Everything you need is in one place -- **Practical**: Focuses on a specific, real-world use case -- **Copy-pasteable**: Includes complete working code examples -- **Focused**: Covers one topic deeply rather than many topics shallowly - -## Recipe Structure - -Each recipe follows a consistent structure: - -1. **Problem Statement**: What you'll build and why -2. **Prerequisites**: Required packages and knowledge -3. **Quick Start**: Minimal working example -4. **Step-by-Step**: Detailed implementation -5. **Complete Example**: Full, production-ready code -6. **Common Patterns**: Alternative approaches -7. **Troubleshooting**: Solutions to common issues -8. **Best Practices**: Tips from experience - -## Browse Recipes - -Visit [/ndk/cookbook/](/ndk/cookbook/) to browse all recipes. - -### By Category - -- 🔐 **Authentication** - Login flows, signers, sessions -- 📝 **Events** - Creating, publishing, and handling events -- 🌐 **Relays** - Connection management, hints, outbox model -- 💰 **Payments** - Zaps, NWC, Cashu, Lightning -- 🧪 **Testing** - Mocks, fixtures, test patterns -- 📱 **Mobile** - React Native specific recipes - -### By Package - -- **ndk-core** - Core functionality, events, relays -- **svelte** - Svelte 5 reactive patterns -- **ndk-mobile** - React Native mobile apps -- **ndk-wallet** - Payment and wallet integration - -### By Difficulty - -- ⭐ **Beginner** - No prior NDK knowledge required -- ⭐⭐ **Intermediate** - Assumes basic NDK understanding -- ⭐⭐⭐ **Advanced** - Complex concepts, multiple systems - -## Contributing a Recipe - -We welcome contributions! Here's how to add a new recipe: - -1. **Copy the template**: Use `/docs/cookbook/TEMPLATE.md` as a starting point -2. **Choose a category**: Place your recipe in the appropriate subdirectory -3. **Fill in metadata**: Complete all frontmatter fields -4. **Write the recipe**: Follow the structure in the template -5. **Add to index**: Update `/docs/cookbook/index.md` with your recipe card -6. **Submit PR**: Open a pull request with your recipe - -### Recipe Guidelines - -**Good recipes:** -- Solve a specific, real-world problem -- Include complete, working code examples -- Explain why, not just how -- Handle errors and edge cases -- Follow NDK best practices - -**Avoid:** -- Overly broad topics (split into multiple recipes) -- Incomplete or broken examples -- Copying API documentation -- Explaining concepts without practical examples - -### File Naming - -Use kebab-case for file names: -- ✅ `implementing-outbox-model.md` -- ✅ `custom-event-validation.md` -- ❌ `Implementing_Outbox_Model.md` -- ❌ `customEventValidation.md` - -### Directory Structure - -``` -cookbook/ -├── index.md # Main cookbook index -├── TEMPLATE.md # Recipe template -├── README.md # This file -├── core/ # ndk-core recipes -│ ├── events/ -│ ├── relays/ -│ └── signers/ -├── svelte5/ # svelte recipes -│ ├── authentication/ -│ └── state-management/ -├── mobile/ # ndk-mobile recipes -└── wallet/ # ndk-wallet recipes -``` - -## Getting Help - -- **Questions**: Ask in [GitHub Discussions](https://github.com/nostr-dev-kit/ndk/discussions) -- **Issues**: Report bugs in [GitHub Issues](https://github.com/nostr-dev-kit/ndk/issues) -- **Chat**: Join the Nostr developer community - -## License - -All recipes are released under the MIT License unless otherwise specified. diff --git a/docs/cookbook/TEMPLATE.md b/docs/cookbook/TEMPLATE.md deleted file mode 100644 index ae67d123f..000000000 --- a/docs/cookbook/TEMPLATE.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: Your Recipe Title Here -description: A brief 1-2 sentence description of what this recipe teaches -tags: ['ndk-core', 'tag1', 'tag2', 'tag3'] -difficulty: beginner|intermediate|advanced -timeToRead: 10 -package: ndk-core|svelte|ndk-mobile|ndk-wallet -author: Your Name -dateAdded: 2024-03-04 ---- - -# Your Recipe Title Here - -A compelling introduction paragraph that explains what the reader will learn and why it's useful. Be specific about the problem this recipe solves. - -## What You'll Build - -- Bullet point of feature 1 -- Bullet point of feature 2 -- Bullet point of feature 3 -- Expected outcome - -## Prerequisites - -```bash -npm install @nostr-dev-kit/ndk [other packages] -``` - -Optional: List any knowledge prerequisites: -- Understanding of X -- Familiarity with Y - -## Quick Start - -A minimal working example that demonstrates the core concept: - -```typescript -import NDK from "@nostr-dev-kit/ndk" - -// Minimal example here -const ndk = new NDK() -``` - -## Step-by-Step Implementation - -### Step 1: Setup - -Explain the first step with code: - -```typescript -// Code example -``` - -### Step 2: Core Logic - -Explain the main implementation: - -```typescript -// Code example -``` - -### Step 3: Error Handling - -Show how to handle errors properly: - -```typescript -// Code example -``` - -## Complete Working Example - -Provide a full, copy-pasteable example: - -```typescript -// Complete example that ties everything together -``` - -## Common Patterns - -### Pattern 1: Use Case - -```typescript -// Example -``` - -### Pattern 2: Another Use Case - -```typescript -// Example -``` - -## Troubleshooting - -### Issue: Common Problem - -**Solution:** How to fix it - -### Issue: Another Common Problem - -**Solution:** How to fix it - -## Best Practices - -1. **Practice 1**: Explanation -2. **Practice 2**: Explanation -3. **Practice 3**: Explanation - -## Key Points - -- Important takeaway 1 -- Important takeaway 2 -- Important takeaway 3 - -## See Also - -- [Related Recipe](/ndk/cookbook/category/recipe-name) - Brief description -- [API Reference](/ndk/api/) - Full API documentation -- [NIP-XX Spec](https://github.com/nostr-protocol/nips/blob/master/XX.md) - If relevant - ---- - -## Metadata Guidelines - -### Title -- Clear and specific -- Action-oriented (e.g., "Building a...", "Implementing...", "Creating...") -- Max 60 characters - -### Description -- 1-2 sentences -- Focuses on what the reader learns -- Max 160 characters - -### Tags -- Use lowercase -- Separate words with hyphens -- Include: package name, NIPs, main concepts -- 3-6 tags recommended - -### Difficulty -- **beginner**: No prior NDK knowledge required -- **intermediate**: Assumes basic NDK understanding -- **advanced**: Complex concepts, multiple systems - -### Time to Read -- Realistic estimate in minutes -- Include time to understand and implement -- Round to nearest 5 minutes - -### Package -- The main NDK package used -- Options: ndk-core, svelte, ndk-mobile, ndk-wallet, ndk-react - -### Author -- Your name or handle -- Can be "NDK Team" for official recipes diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md deleted file mode 100644 index b7170a3bc..000000000 --- a/docs/cookbook/index.md +++ /dev/null @@ -1,384 +0,0 @@ ---- -layout: page -title: NDK Cookbook ---- - - - -
-

NDK Cookbook

-

Discover self-contained recipes for building with NDK

- - - - -
- -## Recent Recipes - -
-

- Multi-Session Management with Account Switcher -

- -

- Learn how to manage multiple Nostr accounts simultaneously in a Svelte 5 application. Includes session switching, profile management, and a complete UI for handling multiple logged-in users. -

- -
- #svelte - #authentication - #multi-session - #session-management -
- -
- 📦 svelte - ⭐⭐⭐ Advanced - ⏱ 20 min -
- 👍 47 - 💬 12 - 📖 1.2k -
-
-
- -
-

- Complete Authentication Flow with NIP-07, nsec, and NIP-46 -

- -

- Implement a complete authentication system supporting browser extensions (NIP-07), private keys (nsec), and remote signers (NIP-46) with bunker:// and nostrconnect:// flows. Includes QR code generation and error handling. -

- -
- #svelte - #authentication - #nip-07 - #nip-46 - #sessions -
- -
- 📦 svelte - ⭐⭐ Intermediate - ⏱ 15 min -
- 👍 234 - 💬 23 - 📖 3.4k -
-
-
- -
-

- Testing with Mock Relays -

- -

- Create and use mock relays for testing NDK applications without connecting to real Nostr relays. Perfect for unit tests, integration tests, and development environments. -

- -
- #ndk-core - #testing - #mock - #relays -
- -
- 📦 ndk-core - ⭐ Beginner - ⏱ 8 min -
- 👍 189 - 💬 15 - 📖 2.8k -
-
-
- -
-

- Connect to Nostr Wallet Connect (NWC) -

- -

- Set up a connection to an NWC wallet and configure it for zapping. Learn how to handle wallet events, request permissions, and send Lightning payments through Nostr. -

- -
- #ndk-wallet - #nwc - #zaps - #payments - #lightning -
- -
- 📦 ndk-wallet - ⭐⭐ Intermediate - ⏱ 12 min -
- 👍 156 - 💬 8 - 📖 1.9k -
-
-
- -
-

- Using Cashu Wallet for E-Cash -

- -

- Create and use a Cashu wallet for managing e-cash tokens. Includes minting, melting, sending, and receiving Cashu tokens with complete error handling and balance management. -

- -
- #ndk-wallet - #cashu - #e-cash - #nutzaps - #nip-60 -
- -
- 📦 ndk-wallet - ⭐⭐⭐ Advanced - ⏱ 25 min -
- 👍 98 - 💬 19 - 📖 876 -
-
-
- -
-

- Publishing Your First Nostr Event -

- -

- Learn the fundamentals of creating and publishing Nostr events with NDK. Covers event structure, signing, and publishing to relays with proper error handling. -

- -
- #ndk-core - #events - #publishing - #beginner -
- -
- 📦 ndk-core - ⭐ Beginner - ⏱ 5 min -
- 👍 445 - 💬 34 - 📖 8.2k -
-
-
- ---- - -## Browse by Category - -
-
-

🔐 Authentication

-

8 recipes

-
- -
-

📝 Events

-

12 recipes

-
- -
-

🌐 Relays

-

6 recipes

-
- -
-

💰 Payments

-

10 recipes

-
- -
-

🧪 Testing

-

8 recipes

-
- -
-

📱 Mobile

-

7 recipes

-
-
- -## Popular Tags - - diff --git a/docs/cookbook/svelte5/basic-authentication.md b/docs/cookbook/svelte5/basic-authentication.md deleted file mode 100644 index 49a31b262..000000000 --- a/docs/cookbook/svelte5/basic-authentication.md +++ /dev/null @@ -1,375 +0,0 @@ ---- -title: Complete Authentication Flow with NIP-07, nsec, and NIP-46 -description: Implement a complete authentication system supporting browser extensions, private keys, and remote signers -tags: ['svelte', 'authentication', 'nip-07', 'nip-46', 'sessions'] -difficulty: intermediate -timeToRead: 15 -package: svelte -author: NDK Team -dateAdded: 2024-03-04 ---- - -# Complete Authentication Flow with NIP-07, nsec, and NIP-46 - -This cookbook demonstrates how to implement a complete authentication system in a Svelte 5 application using NDK's session management. It supports multiple login methods including NIP-07 (browser extensions), private keys (nsec), and NIP-46 (remote signers). - -## What You'll Build - -- Browser extension login (NIP-07) with nos2x/Alby support -- Private key (nsec) login with validation -- Remote signer via bunker:// URI -- Remote signer via nostrconnect:// with QR code generation -- Session management and persistence -- Error handling and loading states - -## Prerequisites - -```bash -npm install @nostr-dev-kit/ndk @nostr-dev-kit/svelte qrcode -``` - -## Basic Setup - -First, initialize NDK and the session stores: - -```typescript -import NDK from "@nostr-dev-kit/ndk" -import { sessions, initStores } from "@nostr-dev-kit/svelte" - -// Initialize NDK with your relay configuration -const ndk = new NDK({ - explicitRelayUrls: [ - 'wss://relay.damus.io', - 'wss://nos.lol', - 'wss://relay.nostr.band' - ] -}) - -// Initialize stores and connect -let initialized = $state(false) -initStores(ndk).then(() => { - ndk.connect() - initialized = true -}) -``` - -## NIP-07: Browser Extension Login - -Login using a Nostr browser extension like nos2x or Alby: - -```typescript -import { NDKNip07Signer } from "@nostr-dev-kit/ndk" - -async function loginWithNip07() { - try { - const signer = new NDKNip07Signer() - await sessions.login(signer) - console.log('Logged in with browser extension') - } catch (error) { - console.error('Failed to connect to extension:', error) - } -} -``` - -## Private Key (nsec) Login - -Login using an nsec private key: - -```typescript -import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk" - -async function loginWithNsec(nsecKey: string) { - try { - const signer = new NDKPrivateKeySigner(nsecKey) - await sessions.login(signer) - console.log('Logged in with private key') - } catch (error) { - console.error('Invalid nsec:', error) - } -} -``` - -## NIP-46: Remote Signer (bunker://) - -Login using a bunker:// URI from a remote signer: - -```typescript -import { NDKNip46Signer } from "@nostr-dev-kit/ndk" - -async function loginWithBunker(bunkerUri: string) { - try { - const signer = new NDKNip46Signer(ndk, bunkerUri) - await signer.blockUntilReady() - await sessions.login(signer) - console.log('Connected via bunker://') - } catch (error) { - console.error('Failed to connect via bunker://', error) - } -} -``` - -## NIP-46: Generate nostrconnect:// URI - -Generate a nostrconnect:// URI and QR code for remote signers to scan: - -```typescript -import { NDKNip46Signer } from "@nostr-dev-kit/ndk" -import QRCode from "qrcode" - -async function generateNostrConnect() { - try { - // Use a relay from your NDK pool - const relay = ndk.pool.relays.values().next().value?.url || 'wss://relay.damus.io' - - const signer = NDKNip46Signer.nostrconnect(ndk, relay) - - // Get the generated URI - const uri = signer.nostrConnectUri - if (!uri) throw new Error('Failed to generate nostrconnect URI') - - // Generate QR code - const qrCodeDataUrl = await QRCode.toDataURL(uri, { - width: 300, - margin: 2, - }) - - // Display the QR code to the user - console.log('Scan this QR code:', qrCodeDataUrl) - console.log('Or use this URI:', uri) - - // Wait for the remote signer to connect - await signer.blockUntilReady() - await sessions.login(signer) - - console.log('Connected via nostrconnect://') - } catch (error) { - console.error('Failed to generate nostrconnect URI:', error) - } -} -``` - -## Complete Login Modal Component - -Here's a complete Svelte 5 component with all login methods: - -```svelte - - -
- {#if !initialized} -

Initializing...

- {:else if !sessions.current} - - {:else} -
-

Logged in as: {sessions.current.pubkey}

-

Name: {sessions.profile?.name || '(no name)'}

- -
- {/if} -
- -{#if showLoginModal} - -{/if} -``` - -## Key Points - -- Always initialize stores with `initStores(ndk)` before using session management -- The `sessions.current` store contains the active session or `undefined` if not logged in -- The `sessions.profile` store contains the current user's profile metadata -- All session data persists across page reloads -- NIP-46 requires `blockUntilReady()` before logging in - -## See Also - -- [Multi-Session Management](/ndk/cookbook/svelte5/multi-session-management) - Handle multiple accounts -- [Session API Reference](/ndk/api/) - Full API documentation -- [NIP-46 Spec](https://github.com/nostr-protocol/nips/blob/master/46.md) - Remote signer protocol diff --git a/docs/cookbook/svelte5/multi-session-management.md b/docs/cookbook/svelte5/multi-session-management.md deleted file mode 100644 index 7e44a6b34..000000000 --- a/docs/cookbook/svelte5/multi-session-management.md +++ /dev/null @@ -1,384 +0,0 @@ ---- -title: Multi-Session Management with Account Switcher -description: Manage multiple Nostr accounts simultaneously with session switching and profile management -tags: ['svelte', 'authentication', 'multi-session', 'session-management'] -difficulty: advanced -timeToRead: 20 -package: svelte -author: NDK Team -dateAdded: 2024-03-04 ---- - -# Multi-Session Management with Account Switcher - -This cookbook demonstrates how to manage multiple Nostr accounts simultaneously in a Svelte 5 application. Users can log in with multiple accounts, switch between them instantly, and view all active sessions with a beautiful account switcher UI. - -## What You'll Build - -- Multiple account management -- Session switching with instant updates -- Account switcher dropdown component -- Session list view -- Profile data for each session -- Logout individual or all sessions - -## Understanding Multi-Session - -NDK's session management system allows you to: -- Have multiple accounts logged in simultaneously -- Switch between accounts instantly -- Maintain separate profile data, follows, and mutes for each account -- Persist all sessions across page reloads - -## Adding Multiple Sessions - -### Add a New Session - -Use `sessions.add()` to add an additional session without logging out of the current one: - -```typescript -import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk" -import { sessions } from "@nostr-dev-kit/svelte" - -async function addAnotherAccount(nsecKey: string) { - try { - const signer = new NDKPrivateKeySigner(nsecKey) - await sessions.add(signer) - console.log('Added new session') - } catch (error) { - console.error('Failed to add session:', error) - } -} -``` - -### Generate and Add a New Account - -```typescript -import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk" -import { sessions } from "@nostr-dev-kit/svelte" - -async function generateAndAddAccount() { - const signer = NDKPrivateKeySigner.generate() - await sessions.add(signer) - console.log('Generated and added new account') -} -``` - -## Switching Between Sessions - -Use `sessions.switch()` to switch to a different logged-in account: - -```typescript -function switchSession(pubkey: string) { - sessions.switch(pubkey) - console.log('Switched to session:', pubkey) -} -``` - -## Accessing All Sessions - -The `sessions.all` store contains an array of all logged-in sessions: - -```typescript -// Get all sessions -console.log('Total sessions:', sessions.all.length) - -// Iterate through all sessions -sessions.all.forEach(session => { - console.log('Pubkey:', session.pubkey) - console.log('Follows:', session.follows.size) - console.log('Mutes:', session.mutes.size) -}) - -// Check if a specific pubkey is logged in -const isActive = sessions.all.some(s => s.pubkey === sessions.current?.pubkey) -``` - -## Session Data Structure - -Each session in `sessions.all` contains: - -```typescript -interface Session { - pubkey: string // The user's public key - follows: Set // Set of followed pubkeys - mutes: Set // Set of muted pubkeys -} -``` - -## Logging Out - -### Logout Current Session - -```typescript -function logout() { - sessions.logout() -} -``` - -### Logout All Sessions - -```typescript -function logoutAll() { - sessions.logoutAll() -} -``` - -## Complete Session Switcher Component - -Here's a beautiful dropdown session switcher: - -```svelte - - - - -{#if initialized && sessions.current} -
- - - {#if showSessionMenu} -
-
- Logged in Accounts -
- - {#each sessions.all as session} -
switchSession(session.pubkey)} - > -
- - {formatPubkey(session.pubkey)} - - {#if sessions.current?.pubkey === session.pubkey} - ACTIVE - {/if} -
-
- Follows: {session.follows.size} · Mutes: {session.mutes.size} -
-
- {/each} - - -
- {/if} -
-{/if} -``` - -## Session List Component - -A simple list showing all sessions: - -```svelte - - -
-

All Sessions ({sessions.all.length})

- - {#if sessions.all.length === 0} -

No sessions available

- {:else} - {#each sessions.all as session} -
-
- {session.pubkey.slice(0, 16)}... - {#if sessions.current?.pubkey === session.pubkey} - ACTIVE - {/if} -
-
- Follows: {session.follows.size} | Mutes: {session.mutes.size} -
- {#if sessions.current?.pubkey !== session.pubkey} - - {/if} -
- {/each} - {/if} -
-``` - -## Best Practices - -1. **Check for current session**: Always use `sessions.current` to check if a user is logged in -2. **Handle session switching**: Subscriptions update automatically when switching sessions -3. **Session persistence**: All sessions persist in localStorage across page reloads -4. **Memory management**: Each session loads its own profile, follows, and mutes independently -5. **UI feedback**: Always show which session is currently active - -## Common Patterns - -### Quick Session Switch in Header - -```svelte -
- {#if sessions.all.length > 1} - - {/if} -
-``` - -### Reactive Session Updates - -```svelte - -``` - -## See Also - -- [Complete Authentication Flow](/ndk/cookbook/svelte5/basic-authentication) - Login methods -- [Session API Reference](/ndk/api/) - Full API documentation diff --git a/docs/index.md b/docs/index.md index 52fac990a..f0080cb46 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,12 +7,22 @@ hero: tagline: "Nostr Development Kit Docs" actions: - theme: brand - text: Getting Started - link: /getting-started/introduction.html + text: Documentation + link: /docs/introduction - theme: secondary - text: References - link: https://github.com/nostr-dev-kit/ndk/blob/master/REFERENCES.md + text: Github Repository + link: https://github.com/nostr-dev-kit/ndk/ --- -NDK is a nostr development kit that makes the experience of building Nostr-related applications, whether they are relays, clients, or anything in between, better, more reliable and overall nicer to work with than existing solutions. \ No newline at end of file +NDK (**Nostr Development Kit**) is a comprehensive type-safe Typescript toolkit for building Nostr applications. + +The library is a monorepo containing different packages and tools you need to create modern, performant, and +feature-rich +Nostr clients and applications. The package contains relay interaction code, reactive UI bindings, wallet abstractions, +caching libraries, Web of Trust functionality, and more. + +> [!WARNING] +> The documentation of the NDK project is under heavy construction +> [More information available in open pull request](https://github.com/nostr-dev-kit/ndk/pull/344). +> \ No newline at end of file diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 000000000..ff56921b2 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,67 @@ +# NDK (Nostr Development Kit) + +NDK is a TypeScript/JavaScript library that simplifies building Nostr clients, relays, and related applications. + +> [!WARNING] +> The documentation of the NDK project is under heavy construction. +> [More information available in open pull request](https://github.com/nostr-dev-kit/ndk/pull/344). + +## Features + +- Event creation, validation, and wrappers for [major NIPs](#nip-support) +- Flexible subscription API with caching, batching, and auto-closing +- Event Signing through private key, encrypted + keys ([NIP-49](https://github.com/nostr-protocol/nips/blob/master/49.md)), + browser extension ([NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md)) or remote signer + ([NIP-46](https://github.com/nostr-protocol/nips/blob/master/46.md)) +- Relay connection pool with automatic reconnection and failover +- Outbox model support +- Pluggable cache adapters (Redis, Dexie, SQLite, etc) +- Data Vending Machine support ([NIP-90](https://github.com/nostr-protocol/nips/blob/master/90.md)) +- Zap + utilities ([NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md), [NIP-61](https://github.com/nostr-protocol/nips/blob/master/61.md)) +- Threading, event kinds, and utility functions (URL normalization, metadata tags, filters) +- Modular design with packages for different frameworks (Mobile, Svelte, React) + +### NIP Support + +- [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) +- [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) +- [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md) +- [NIP-18](https://github.com/nostr-protocol/nips/blob/master/18.md) +- [NIP-49](https://github.com/nostr-protocol/nips/blob/master/49.md) +- [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md) +- [NIP-60](https://github.com/nostr-protocol/nips/blob/master/60.md) +- [NIP-61](https://github.com/nostr-protocol/nips/blob/master/61.md) + +## Multi-Repo + +NDK is a monorepo with different packages. The main package is `@nostr-dev-kit/core` and contains the core +functionality. + +For other functionality you might need additional packages: + +### Extras + +* [@nostr-dev-kit/blossom](/blossom/README.md): Blossom Protocol Support for assets +* [@nostr-dev-kit/sessions](/sessions/README.md): Session Management with Multi-Account support +* [@nostr-dev-kit/sync](/sync/README.md): Event synchronization using Negentropy +* [@nostr-dev-kit/wallet](/wallet/README.md): Support for WebLN, NWC, Cashu/eCash wallets +* [@nostr-dev-kit/wot](/wot/README.md): Web of Trust (WOT) utilities + +### Framework Integrations + +* [@nostr-dev-kit/react](/react/README.md): Hooks and utilities to integrate Nostr into your React applications +* [@nostr-dev-kit/svelte](/svelte/README.md): Modern, performant, and beautiful Svelte 5 integration + +### Cache Adapters + +These NDK adapters are used to store and retrieve data from a cache so relays do not need to be +re-queried for the same data. + +* [@nostr-dev-kit/cache-memory](/cache-memory/README.md): In-memory LRU cache adapter +* [@nostr-dev-kit/cache-nostr](/cache-nostr/README.md): Local Nostr relay cache adapter +* [@nostr-dev-kit/cache-redis](/cache-redis/README.md): A cache adapter for Redis +* [@nostr-dev-kit/cache-dexie](/cache-dexie/README.md): Dexie (IndexedDB, in browser database) adapter +* [@nostr-dev-kit/cache-sqlite](/cache-sqlite/README.md): SQLite (better-sqlite3) adapter +* [@nostr-dev-kit/cache-sqlite-wasm](/cache-sqlite-wasm/md): In browser (WASM) SQLite adapter diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index 703f6b53a..000000000 --- a/docs/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "docs", - "version": "1.0.0", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "description": "", - "dependencies": { - "vitepress": "^1.6.3" - }, - "devDependencies": { - "vitepress-plugin-mermaid": "^2.0.17" - } -} diff --git a/docs/sessions/api.md b/docs/sessions/api.md deleted file mode 100644 index 6ac8c5dc6..000000000 --- a/docs/sessions/api.md +++ /dev/null @@ -1,555 +0,0 @@ -# API Reference - -Complete API documentation for `@nostr-dev-kit/sessions`. - -## NDKSessionManager - -The main class for managing user sessions. - -### Constructor - -```typescript -new NDKSessionManager(ndk: NDK, options?: SessionManagerOptions) -``` - -**Parameters:** -- `ndk: NDK` - The NDK instance to use -- `options?: SessionManagerOptions` - Configuration options - -**SessionManagerOptions:** - -```typescript -interface SessionManagerOptions { - storage?: SessionStorage; // Storage backend (default: MemoryStorage) - autoSave?: boolean; // Auto-persist on changes (default: true) - saveDebounceMs?: number; // Debounce time for auto-save (default: 500ms) - fetches?: SessionStartOptions; // What to fetch on login/restore -} -``` - -**Example:** - -```typescript -import { NDKSessionManager, LocalStorage } from '@nostr-dev-kit/sessions'; - -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage(), - autoSave: true, - saveDebounceMs: 500, - fetches: { - follows: true, - mutes: true - } -}); -``` - -### Methods - -#### login() - -Login with a signer or user. - -```typescript -async login( - userOrSigner: NDKUser | NDKSigner, - options?: { setActive?: boolean } -): Promise -``` - -**Parameters:** -- `userOrSigner` - An NDKSigner for full sessions or NDKUser for read-only -- `options?: { setActive?: boolean }` - Whether to set as active session (default: true) - -**Returns:** `Promise` - The public key of the logged-in user - -**Example:** - -```typescript -const signer = new NDKPrivateKeySigner(nsec); - -// Configure fetches in constructor -const sessions = new NDKSessionManager(ndk, { - fetches: { - follows: true, - mutes: true, - relayList: true, - wallet: true - } -}); - -const pubkey = await sessions.login(signer); -console.log('Logged in:', pubkey); - -// Or don't set as active -const pubkey2 = await sessions.login(signer2, { setActive: false }); -``` - -#### logout() - -Remove a session. If no pubkey provided, removes the active session. - -```typescript -logout(pubkey?: Hexpubkey): void -``` - -**Parameters:** -- `pubkey?: Hexpubkey` - Public key of session to remove (optional) - -**Example:** - -```typescript -// Logout specific user -sessions.logout(somePubkey); - -// Logout active user -sessions.logout(); -``` - -#### switchTo() - -Switch the active session to a different user. - -```typescript -switchTo(pubkey: Hexpubkey): void -``` - -**Parameters:** -- `pubkey: Hexpubkey` - Public key of session to activate - -**Example:** - -```typescript -sessions.switchTo(pubkey); -console.log('Now active:', sessions.activePubkey); -``` - -#### restore() - -Restore sessions from storage. - -```typescript -async restore(): Promise -``` - -**Example:** - -```typescript -await sessions.restore(); - -if (sessions.activeUser) { - console.log('Restored session for', sessions.activeUser.npub); -} -``` - -#### persist() - -Manually persist sessions to storage. - -```typescript -async persist(): Promise -``` - -**Example:** - -```typescript -await sessions.persist(); -``` - -#### clear() - -Clear all sessions from storage. - -```typescript -async clear(): Promise -``` - -**Example:** - -```typescript -await sessions.clear(); -``` - -#### subscribe() - -Subscribe to session state changes. - -```typescript -subscribe(callback: (state: SessionState) => void): UnsubscribeFn -``` - -**Parameters:** -- `callback` - Function called when state changes - -**Returns:** `UnsubscribeFn` - Function to unsubscribe - -**SessionState:** - -```typescript -interface SessionState { - sessions: Map; - activePubkey?: Hexpubkey; -} -``` - -**Example:** - -```typescript -const unsubscribe = sessions.subscribe((state) => { - console.log('Active:', state.activePubkey); - console.log('Sessions:', state.sessions.size); -}); - -// Later... -unsubscribe(); -``` - -#### destroy() - -Cleanup and stop all subscriptions and timers. - -```typescript -destroy(): void -``` - -**Example:** - -```typescript -sessions.destroy(); -``` - -#### getSessions() - -Get all sessions. - -```typescript -getSessions(): Map -``` - -**Example:** - -```typescript -const allSessions = sessions.getSessions(); -for (const [pubkey, session] of allSessions) { - console.log(pubkey, session.user.profile?.name); -} -``` - -#### getSession() - -Get a specific session by pubkey. - -```typescript -getSession(pubkey: Hexpubkey): NDKSession | undefined -``` - -**Parameters:** -- `pubkey: Hexpubkey` - Public key of session to get - -**Example:** - -```typescript -const session = sessions.getSession(pubkey); -if (session) { - console.log('Follows:', session.followSet?.size); -} -``` - -### Properties - -#### activeSession - -Get the currently active session. - -```typescript -get activeSession(): NDKSession | undefined -``` - -**Example:** - -```typescript -const session = sessions.activeSession; -if (session) { - console.log('Follows:', session.followSet?.size); - console.log('Mutes:', session.muteSet?.size); -} -``` - -#### activeUser - -Get the currently active user. - -```typescript -get activeUser(): NDKUser | undefined -``` - -**Example:** - -```typescript -const user = sessions.activeUser; -if (user) { - console.log('Active user:', user.npub); - console.log('Profile:', user.profile?.name); -} -``` - -#### activePubkey - -Get the currently active pubkey. - -```typescript -get activePubkey(): Hexpubkey | undefined -``` - -**Example:** - -```typescript -console.log('Active pubkey:', sessions.activePubkey); -``` - -## NDKSession - -Represents an individual user session. - -### Properties - -```typescript -interface NDKSession { - // User instance - user: NDKUser; - - // Signer for this session (undefined for read-only sessions) - signer?: NDKSigner; - - // Set of followed pubkeys - followSet?: Set; - - // Set of muted items (users, events, words, hashtags) - muteSet?: Set; - - // User's relay list - relayList?: NDKRelayList; - - // Blocked relay URLs - blockedRelayUrls?: Set; - - // NIP-60 wallet event - wallet?: NDKEvent; - - // Additional fetched events by kind - events: Map; -} -``` - -## Storage Implementations - -### LocalStorage - -Browser localStorage implementation. - -```typescript -new LocalStorage(key?: string) -``` - -**Parameters:** -- `key?: string` - Storage key (default: `'ndk-sessions'`) - -**Example:** - -```typescript -import { LocalStorage } from '@nostr-dev-kit/sessions'; - -const storage = new LocalStorage('my-app-sessions'); -``` - -### FileStorage - -Node.js filesystem implementation. - -```typescript -new FileStorage(filePath?: string) -``` - -**Parameters:** -- `filePath?: string` - File path (default: `'./.ndk-sessions.json'`) - -**Example:** - -```typescript -import { FileStorage } from '@nostr-dev-kit/sessions'; - -const storage = new FileStorage('~/.config/myapp/sessions.json'); -``` - -### MemoryStorage - -In-memory implementation (no persistence). - -```typescript -new MemoryStorage() -``` - -**Example:** - -```typescript -import { MemoryStorage } from '@nostr-dev-kit/sessions'; - -const storage = new MemoryStorage(); -``` - -## Custom Storage - -Implement the `SessionStorage` interface for custom storage: - -```typescript -interface SessionStorage { - save( - sessions: Map, - activePubkey?: Hexpubkey - ): Promise; - - load(): Promise<{ - sessions: Map; - activePubkey?: Hexpubkey; - }>; - - clear(): Promise; -} -``` - -**Example:** - -```typescript -class MyCustomStorage implements SessionStorage { - async save(sessions, activePubkey) { - // Save to your backend... - } - - async load() { - // Load from your backend... - return { sessions: new Map(), activePubkey: undefined }; - } - - async clear() { - // Clear from your backend... - } -} - -const storage = new MyCustomStorage(); -const sessions = new NDKSessionManager(ndk, { storage }); -``` - -## Types - -### Hexpubkey - -```typescript -type Hexpubkey = string; -``` - -Hex-encoded public key string. - -### MuteItem - -```typescript -type MuteItem = { - type: 'user' | 'event' | 'word' | 'hashtag'; - value: string; - // ... other properties -}; -``` - -Represents a muted item. - -### SerializedSession - -```typescript -interface SerializedSession { - pubkey: Hexpubkey; - signer?: string; // Serialized signer - followSet?: Hexpubkey[]; - muteSet?: MuteItem[]; - relayList?: RelayListData; - blockedRelayUrls?: string[]; - wallet?: NostrEvent; - events?: [NDKKind, NostrEvent][]; -} -``` - -Serialized session format for storage. - -### UnsubscribeFn - -```typescript -type UnsubscribeFn = () => void; -``` - -Function to call to unsubscribe from updates. - -## Error Handling - -The sessions package may throw errors during: - -- Login (invalid signer, network errors) -- Storage operations (permissions, disk full) -- Restoration (corrupted data) - -Always wrap async operations in try-catch: - -```typescript -try { - await sessions.login(signer); -} catch (error) { - console.error('Login failed:', error); -} - -try { - await sessions.restore(); -} catch (error) { - console.error('Failed to restore sessions:', error); -} -``` - -## Best Practices - -### 1. Always Call destroy() - -```typescript -// In your cleanup code -sessions.destroy(); -``` - -### 2. Use autoSave - -```typescript -const sessions = new NDKSessionManager(ndk, { - autoSave: true, - saveDebounceMs: 500 -}); -``` - -### 3. Handle No Active Session - -```typescript -if (!sessions.activeUser) { - // Show login UI -} -``` - -### 4. Subscribe to Changes - -```typescript -const unsubscribe = sessions.subscribe((state) => { - // Update UI when sessions change -}); -``` - -### 5. Security - -```typescript -// ⚠️ NEVER commit .ndk-sessions.json to git! -// Add to .gitignore: -// .ndk-sessions.json - -// Use environment variables for sensitive keys -const nsec = process.env.NOSTR_NSEC; -``` diff --git a/docs/sessions/index.md b/docs/sessions/index.md deleted file mode 100644 index be5502d52..000000000 --- a/docs/sessions/index.md +++ /dev/null @@ -1,141 +0,0 @@ -# Sessions - -`@nostr-dev-kit/sessions` is a framework-agnostic session management library for NDK that provides multi-account support, automatic data fetching, and flexible persistence. - -## Why Sessions? - -Managing user authentication and session state in Nostr applications can be complex. The sessions package simplifies: - -- **Multi-account management** - Let users switch between multiple Nostr identities seamlessly -- **Automatic data fetching** - Automatically fetch and cache follows, mutes, relay lists, and more -- **Persistence** - Save and restore sessions across app restarts -- **Framework agnostic** - Works with React, Svelte, Vue, vanilla JS, Node.js, etc. - -## Key Features - -### 🔐 Multiple Account Support - -Users can log in with multiple Nostr accounts and switch between them instantly. Perfect for: -- Personal and business accounts -- Testing with multiple identities -- Content creators managing multiple personas - -### 💾 Flexible Storage - -Built-in storage adapters for: -- **LocalStorage** - Browser-based persistence -- **FileStorage** - Node.js/CLI applications -- **MemoryStorage** - Testing or temporary sessions -- **Custom** - Implement your own storage backend - -### 🔄 Auto-Fetch User Data - -On login, automatically fetch: -- Contact list (kind 3 follows) -- Mute lists (kind 10000) -- Relay lists (kind 10002) -- Blocked relay lists (kind 10001) -- NIP-60 wallet data (kind 17375) -- Any custom replaceable event kinds - -### 🎯 Framework Integration - -Works seamlessly with: -- React (via `@nostr-dev-kit/react`) -- Svelte 5 (via `@nostr-dev-kit/svelte`) -- Mobile (React Native via `@nostr-dev-kit/mobile`) -- Vanilla JavaScript -- Node.js/CLI applications - -## Installation - -```bash -npm install @nostr-dev-kit/sessions -# or -bun add @nostr-dev-kit/sessions -``` - -## Quick Example - -```typescript -import NDK from '@nostr-dev-kit/ndk'; -import { NDKSessionManager, LocalStorage } from '@nostr-dev-kit/sessions'; -import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; - -const ndk = new NDK({ explicitRelayUrls: ['wss://relay.damus.io'] }); -await ndk.connect(); - -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage(), - autoSave: true, - fetches: { - follows: true, - mutes: true, - relayList: true - } -}); - -// Restore previous sessions -await sessions.restore(); - -// Login with auto-fetch -const signer = new NDKPrivateKeySigner(nsec); -await sessions.login(signer); - -console.log('Active user:', sessions.activeUser); -console.log('Follows:', sessions.activeSession?.followSet?.size); -``` - -## Next Steps - -- [Quick Start Guide](./quick-start) - Get up and running -- [API Reference](./api) - Complete API documentation -- [Migration Guide](./migration) - Migrating from ndk-hooks - -## Use Cases - -### Browser Applications -Perfect for web apps that need: -- User login/logout -- Multi-account switching -- Persistent sessions across page reloads -- Automatic relay and follow list management - -### Node.js/CLI Tools -Ideal for command-line tools that need: -- Saved credentials -- Multiple identity management -- Automated publishing with saved accounts - -### Mobile Applications -Great for React Native apps needing: -- Secure session storage -- Multi-account support -- Offline-first data caching - -## Architecture - -The sessions package is built on three core components: - -1. **NDKSessionManager** - Main API for managing sessions -2. **SessionStorage** - Pluggable storage backends -3. **NDKSession** - Individual session state and data - -All session state changes are observable via the subscribe pattern, making it easy to integrate with any reactive framework. - -## Security Considerations - -⚠️ **Important:** Session serialization stores private keys. In production: - -1. Use encrypted storage when possible -2. Never commit session files to version control -3. Use environment variables for sensitive keys -4. Consider NIP-07 (browser extensions) or NIP-46 (remote signers) for better security - -## Framework-Specific Documentation - -For framework-specific implementations using sessions: - -- **React** - See [`@nostr-dev-kit/react` hooks documentation](/hooks/session-management) -- **Svelte 5** - See [`@nostr-dev-kit/svelte` documentation](/wrappers/svelte) -- **Mobile** - See [`@nostr-dev-kit/mobile` documentation](/mobile/session) diff --git a/docs/sessions/migration.md b/docs/sessions/migration.md deleted file mode 100644 index c24146e08..000000000 --- a/docs/sessions/migration.md +++ /dev/null @@ -1,467 +0,0 @@ -# Migration Guide - -This guide helps you migrate from the legacy session management in `@nostr-dev-kit/react` (ndk-hooks) to the new standalone `@nostr-dev-kit/sessions` package. - -## Why Migrate? - -The new `@nostr-dev-kit/sessions` package provides: - -- **Framework agnostic** - Works with React, Svelte, Vue, vanilla JS, Node.js -- **Better separation of concerns** - Session logic is independent of UI framework -- **Improved testing** - Easier to test without framework dependencies -- **More flexible** - Better storage options and customization -- **Actively maintained** - The old hooks-based session management is deprecated - -## Overview of Changes - -### Old (ndk-hooks) - -```typescript -// Hooks-based, React-specific -import { useNDKSessionLogin, useNDKCurrentUser } from '@nostr-dev-kit/react'; - -function MyComponent() { - const login = useNDKSessionLogin(); - const currentUser = useNDKCurrentUser(); - - // Login tied to React component lifecycle - await login(signer); -} -``` - -### New (sessions package) - -```typescript -// Framework-agnostic, standalone -import { NDKSessionManager, LocalStorage } from '@nostr-dev-kit/sessions'; - -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage() -}); - -// Session management independent of UI framework -await sessions.login(signer); -``` - -## Migration Steps - -### 1. Install New Package - -```bash -npm install @nostr-dev-kit/sessions -``` - -### 2. Update React Integration - -#### Before (ndk-hooks) - -```typescript -// In your app setup -import { useNDKInit, useNDKSessionMonitor } from '@nostr-dev-kit/react'; - -function NDKHeadless() { - const initNDK = useNDKInit(); - - useNDKSessionMonitor(sessionStorage, { - profile: true, - follows: true - }); - - useEffect(() => { - initNDK(ndk); - }, []); - - return null; -} -``` - -#### After (sessions package) - -```typescript -// In your app setup - sessions are now managed outside React -import NDK from '@nostr-dev-kit/ndk'; -import { NDKSessionManager, LocalStorage } from '@nostr-dev-kit/sessions'; - -// Create session manager once, outside component tree -const ndk = new NDK({ explicitRelayUrls: [...] }); -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage() -}); - -// Initialize in app setup -await ndk.connect(); -await sessions.restore(); - -// Then use React hooks from @nostr-dev-kit/react for UI integration -function MyApp() { - // React hooks still available for reactive updates - const { ndk } = useNDK(); - const currentUser = useNDKCurrentUser(); - - return ; -} -``` - -### 3. Update Login/Logout Logic - -#### Before - -```typescript -import { useNDKSessionLogin, useNDKSessionLogout } from '@nostr-dev-kit/react'; - -function LoginComponent() { - const login = useNDKSessionLogin(); - const logout = useNDKSessionLogout(); - - const handleLogin = async () => { - await login(signer, { follows: true }); - }; - - const handleLogout = async () => { - await logout(); - }; -} -``` - -#### After - -```typescript -import { sessions } from './ndk-setup'; // Your session manager instance - -function LoginComponent() { - const handleLogin = async () => { - // Configure fetches in session manager constructor instead - await sessions.login(signer); - }; - - const handleLogout = () => { - sessions.logout(); - }; -} -``` - -### 4. Update Multi-Account Switching - -#### Before - -```typescript -import { useNDKSessionSwitch } from '@nostr-dev-kit/react'; - -function AccountSwitcher() { - const switchSession = useNDKSessionSwitch(); - - const handleSwitch = async (pubkey: string) => { - await switchSession(pubkey); - }; -} -``` - -#### After - -```typescript -import { sessions } from './ndk-setup'; - -function AccountSwitcher() { - const handleSwitch = (pubkey: string) => { - sessions.switchTo(pubkey); - }; -} -``` - -### 5. Update Session State Access - -#### Before - -```typescript -import { useNDKSessions, useNDKCurrentUser } from '@nostr-dev-kit/react'; - -function UserInfo() { - const allSessions = useNDKSessions(); - const currentUser = useNDKCurrentUser(); -} -``` - -#### After - -```typescript -import { sessions } from './ndk-setup'; -import { useNDKCurrentUser } from '@nostr-dev-kit/react'; - -function UserInfo() { - const allSessions = sessions.getSessions(); - const currentUser = useNDKCurrentUser(); // Still available from React hooks -} -``` - -## Storage Migration - -### Before (ndk-hooks) - -```typescript -import { NDKSessionLocalStorage } from '@nostr-dev-kit/react'; - -const storage = new NDKSessionLocalStorage(); -``` - -### After (sessions package) - -```typescript -import { LocalStorage } from '@nostr-dev-kit/sessions'; - -const storage = new LocalStorage(); -``` - -The storage interface is the same, just imported from the new package. - -## Complete Example - -### Before (ndk-hooks) - -```typescript -// components/ndk.tsx -import { useNDKInit, useNDKSessionMonitor, NDKSessionLocalStorage } from '@nostr-dev-kit/react'; - -const sessionStorage = new NDKSessionLocalStorage(); - -export default function NDKHeadless() { - const initNDK = useNDKInit(); - - useNDKSessionMonitor(sessionStorage, { - profile: true, - follows: true - }); - - useEffect(() => { - if (ndk) initNDK(ndk); - }, []); - - return null; -} - -// In components -function LoginButton() { - const login = useNDKSessionLogin(); - const currentUser = useNDKCurrentUser(); - - const handleLogin = async () => { - const signer = new NDKPrivateKeySigner(nsec); - await login(signer, { follows: true }); - }; - - return currentUser ? ( -
Logged in as {currentUser.npub}
- ) : ( - - ); -} -``` - -### After (sessions package) - -```typescript -// ndk-setup.ts -import NDK from '@nostr-dev-kit/ndk'; -import { NDKSessionManager, LocalStorage } from '@nostr-dev-kit/sessions'; - -export const ndk = new NDK({ explicitRelayUrls: [...] }); - -export const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage(), - autoSave: true, - fetches: { - follows: true, - mutes: true - } -}); - -// Initialize once -export async function initializeNDK() { - await ndk.connect(); - await sessions.restore(); - - // Subscribe to session changes to update NDK state - sessions.subscribe((state) => { - if (state.activePubkey) { - const session = state.sessions.get(state.activePubkey); - if (session?.signer) { - ndk.signer = session.signer; - } - } - }); -} - -// App.tsx -import { initializeNDK, ndk } from './ndk-setup'; -import { useNDKInit } from '@nostr-dev-kit/react'; - -function App() { - const initNDK = useNDKInit(); - - useEffect(() => { - initializeNDK().then(() => { - initNDK(ndk); // Connect React hooks to NDK instance - }); - }, []); - - return ; -} - -// In components -import { sessions } from './ndk-setup'; -import { useNDKCurrentUser } from '@nostr-dev-kit/react'; - -function LoginButton() { - const currentUser = useNDKCurrentUser(); - - const handleLogin = async () => { - const signer = new NDKPrivateKeySigner(nsec); - // Fetches configured in session manager constructor - await sessions.login(signer); - }; - - return currentUser ? ( -
Logged in as {currentUser.npub}
- ) : ( - - ); -} -``` - -## Breaking Changes - -### 1. Storage Location Changed - -The default storage key has changed from `'ndk-session'` to `'ndk-sessions'` (plural). To migrate existing sessions: - -```typescript -// Option 1: Specify old key -const storage = new LocalStorage('ndk-session'); - -// Option 2: Migrate data manually -const oldData = localStorage.getItem('ndk-session'); -if (oldData) { - localStorage.setItem('ndk-sessions', oldData); - localStorage.removeItem('ndk-session'); -} -``` - -### 2. Async Login - -Login is now always async and returns the pubkey: - -```typescript -// Before -login(signer); // Fire and forget - -// After -const pubkey = await sessions.login(signer); -``` - -### 3. No Auto-Monitoring Hook - -The `useNDKSessionMonitor` hook is replaced by creating a session manager: - -```typescript -// Before -useNDKSessionMonitor(storage, options); - -// After -const sessions = new NDKSessionManager(ndk, { - storage, - autoSave: true -}); -await sessions.restore(); -``` - -### 4. Direct Access Instead of Hooks - -Many operations now use direct method calls instead of hooks: - -```typescript -// Before: Hook-based -const logout = useNDKSessionLogout(); -logout(); - -// After: Direct call -sessions.logout(); -``` - -## Svelte Migration - -If you're using Svelte, migration is similar: - -### Before (ndk-svelte with hooks) - -```typescript -// Mixed approach, not clean -import { ndk } from '$lib/stores/ndk'; -``` - -### After (sessions with svelte) - -```typescript -// ndk.svelte.ts -import NDK from '@nostr-dev-kit/ndk'; -import { NDKSessionManager, LocalStorage } from '@nostr-dev-kit/sessions'; - -export const ndk = new NDK({ explicitRelayUrls: [...] }); - -export const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage(), - fetches: { - follows: true - } -}); - -// Initialize -await ndk.connect(); -await sessions.restore(); -``` - -## Troubleshooting - -### Sessions Not Persisting - -Make sure `autoSave` is enabled: - -```typescript -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage(), - autoSave: true // ✓ Enable auto-save -}); -``` - -### Signer Not Set on NDK - -Subscribe to session changes and update NDK: - -```typescript -sessions.subscribe((state) => { - if (state.activePubkey) { - const session = state.sessions.get(state.activePubkey); - if (session?.signer) { - ndk.signer = session.signer; - ndk.activeUser = session.user; - } - } -}); -``` - -### React Not Re-rendering - -Make sure you've initialized NDK with the React store: - -```typescript -import { useNDKInit } from '@nostr-dev-kit/react'; - -const initNDK = useNDKInit(); -useEffect(() => { - initNDK(ndk); -}, []); -``` - -## Need Help? - -- [Quick Start Guide](./quick-start) - Fresh start with sessions -- [API Reference](./api) - Complete API documentation -- [GitHub Issues](https://github.com/nostr-dev-kit/ndk/issues) - Report migration issues diff --git a/docs/sessions/quick-start.md b/docs/sessions/quick-start.md deleted file mode 100644 index 58290d2b5..000000000 --- a/docs/sessions/quick-start.md +++ /dev/null @@ -1,260 +0,0 @@ -# Quick Start - -Get started with NDK Sessions in minutes. - -## Installation - -```bash -npm install @nostr-dev-kit/sessions @nostr-dev-kit/ndk -# or -bun add @nostr-dev-kit/sessions @nostr-dev-kit/ndk -``` - -## Basic Setup - -### 1. Initialize NDK - -First, create and connect your NDK instance: - -```typescript -import NDK from '@nostr-dev-kit/ndk'; - -const ndk = new NDK({ - explicitRelayUrls: [ - 'wss://relay.damus.io', - 'wss://nos.lol', - 'wss://relay.nostr.band' - ] -}); - -await ndk.connect(); -``` - -### 2. Create Session Manager - -Create a session manager with your preferred storage: - -```typescript -import { NDKSessionManager, LocalStorage } from '@nostr-dev-kit/sessions'; - -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage(), - autoSave: true, // Automatically save changes - saveDebounceMs: 500 // Debounce auto-saves -}); -``` - -### 3. Restore Previous Sessions - -Restore any previously saved sessions: - -```typescript -await sessions.restore(); - -if (sessions.activeUser) { - console.log('Welcome back!', sessions.activeUser.npub); -} -``` - -### 4. Login - -Login with a signer. To automatically fetch user data, configure `fetches` in the constructor: - -```typescript -import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; - -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage(), - autoSave: true, - fetches: { - follows: true, // Fetch contact list - mutes: true, // Fetch mute list - relayList: true, // Fetch relay list - wallet: true // Fetch NIP-60 wallet - } -}); - -const signer = new NDKPrivateKeySigner(nsecKey); -await sessions.login(signer); - -// Access session data -console.log('Following:', sessions.activeSession?.followSet?.size, 'users'); -console.log('Muted:', sessions.activeSession?.muteSet?.size, 'items'); -``` - -## Storage Options - -### Browser (LocalStorage) - -```typescript -import { LocalStorage } from '@nostr-dev-kit/sessions'; - -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage('my-app-sessions') // Custom key -}); -``` - -### Node.js (FileStorage) - -```typescript -import { FileStorage } from '@nostr-dev-kit/sessions'; - -const sessions = new NDKSessionManager(ndk, { - storage: new FileStorage('./.ndk-sessions.json') -}); -``` - -### Temporary (MemoryStorage) - -```typescript -import { MemoryStorage } from '@nostr-dev-kit/sessions'; - -const sessions = new NDKSessionManager(ndk, { - storage: new MemoryStorage(), // No persistence - autoSave: false -}); -``` - -## Multi-Account Management - -### Login Multiple Accounts - -```typescript -// Login first account (automatically active) -const signer1 = new NDKPrivateKeySigner(nsec1); -const pubkey1 = await sessions.login(signer1); - -// Login second account -const signer2 = new NDKPrivateKeySigner(nsec2); -const pubkey2 = await sessions.login(signer2, { setActive: false }); - -console.log('Accounts:', sessions.getSessions().size); -``` - -### Switch Between Accounts - -```typescript -// Switch to different account -sessions.switchTo(pubkey2); -console.log('Active:', sessions.activePubkey); - -// Switch back -sessions.switchTo(pubkey1); -``` - -### Logout - -```typescript -// Logout specific account -sessions.logout(pubkey1); - -// Or logout current active account -sessions.logout(); -``` - -## React to Changes - -Subscribe to session changes: - -```typescript -const unsubscribe = sessions.subscribe((state) => { - console.log('Active user:', state.activePubkey); - console.log('Total sessions:', state.sessions.size); - - // Update your UI... -}); - -// Later, cleanup -unsubscribe(); -``` - -## Read-Only Sessions - -Create a read-only session without a signer. Configure `fetches` in the constructor: - -```typescript -const sessions = new NDKSessionManager(ndk, { - fetches: { - follows: true, - relayList: true - } -}); - -const user = ndk.getUser({ pubkey: somePubkey }); -await sessions.login(user); - -// Data is fetched and cached, but user can't sign events -``` - -## Using with NIP-07 (Browser Extensions) - -```typescript -import { NDKNip07Signer } from '@nostr-dev-kit/ndk'; - -const sessions = new NDKSessionManager(ndk, { - fetches: { - follows: true, - mutes: true - } -}); - -const signer = new NDKNip07Signer(); -await sessions.login(signer); -``` - -## CLI Example - -Complete example for a Node.js CLI tool: - -```typescript -#!/usr/bin/env node -import NDK from '@nostr-dev-kit/ndk'; -import { NDKSessionManager, FileStorage } from '@nostr-dev-kit/sessions'; -import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; - -const ndk = new NDK({ explicitRelayUrls: ['wss://relay.damus.io'] }); -await ndk.connect(); - -const sessions = new NDKSessionManager(ndk, { - storage: new FileStorage('./.ndk-sessions.json'), - autoSave: true, - fetches: { - follows: true - } -}); - -// Restore previous session -await sessions.restore(); - -if (!sessions.activeUser) { - // First time - login - const nsec = process.env.NOSTR_NSEC; - if (!nsec) throw new Error('NOSTR_NSEC not set'); - - const signer = new NDKPrivateKeySigner(nsec); - await sessions.login(signer); - - console.log('Logged in as', sessions.activeUser.npub); -} else { - console.log('Welcome back', sessions.activeUser.npub); -} - -// Use the active session to publish -const event = new NDKEvent(ndk, { - kind: 1, - content: 'Hello from CLI!' -}); - -await event.publish(); -console.log('Published:', event.id); - -// Cleanup -sessions.destroy(); -``` - -## Next Steps - -- [API Reference](./api) - Complete API documentation -- [Migration Guide](./migration) - Migrating from ndk-hooks -- [React Hooks](/hooks/session-management) - Using with React -- [Svelte](/wrappers/svelte) - Using with Svelte diff --git a/core/snippets/index.md b/docs/snippets.md similarity index 74% rename from core/snippets/index.md rename to docs/snippets.md index b72b4a309..76413ea09 100644 --- a/core/snippets/index.md +++ b/docs/snippets.md @@ -1,17 +1,41 @@ # Code Snippets -This section contains a growing collection of code snippets demonstrating how to perform specific tasks with NDK. Each snippet is focused on a single, targeted use case to help you quickly find solutions for common implementation needs. +This section contains a growing collection of code snippets demonstrating how to perform specific tasks with NDK. +Each snippet is focused on a single, targeted use case to help you find solutions for common implementation needs. -## Categories +Snippets are grouped by category. Some of them are listed in more than one category. + +## Events + + + +## Connecting + + + +## Subscribing + + + +## Signers + + + +## Publishing + + + +## Helpers + + + + +## Not migrated yet -Snippets are organized into the following categories: - [User](./user/) - [Generate Keys](./user/generate-keys.md) - Generate a new key pair and obtain all formats (private key, public key, nsec, npub) - [Get Profile](./user/get-profile.md) - Fetch and handle user profile information -- [Event](./event/) - - [Basic](./event/basic.md) - Generate a basic Nostr event - - [Tagging Users and Events](./event/tagging-users-and-events.md) - Add tags to mention users and events - [Mobile](./mobile/) - [Basics] - [Initialize NDK + SQLite cache](./mobile/ndk/initializing-ndk.md) - Set up NDK with SQLite caching for mobile apps @@ -30,4 +54,4 @@ Snippets are organized into the following categories: - [Mock Relays](./testing/mock-relays.md) - Create and use mock relays for testing NDK applications - [Event Generation](./testing/event-generation.md) - Generate test events with different kinds and content - [Nutzap Testing](./testing/nutzap-testing.md) - Test Cashu token and Nutzap functionality - - [Relay Pool Testing](./testing/relay-pool-testing.md) - Test relay pool behavior and event handling + - [Relay Pool Testing](./testing/relay-pool-testing.md) - Test relay pool behavior and event handling \ No newline at end of file diff --git a/docs/snippets/connecting.md b/docs/snippets/connecting.md new file mode 100644 index 000000000..cd372a43e --- /dev/null +++ b/docs/snippets/connecting.md @@ -0,0 +1,23 @@ +### Specific Relays + +<<< @/core/docs/snippets/connect_explicit.ts + +### Adding Relays + +<<< @/core/docs/snippets/connect_explicit_alt.ts + +### NIP-07 Relays + +<<< @/core/docs/snippets/connect_nip07.ts + +### Dev Relays + +<<< @/core/docs/snippets/connect_dev_relays.ts + +### Relay Pools + +<<< @/core/docs/snippets/connect_pools.ts + +### Connection Events + +<<< @/core/docs/snippets/connection_events.ts diff --git a/docs/snippets/events.md b/docs/snippets/events.md new file mode 100644 index 000000000..99d2a8266 --- /dev/null +++ b/docs/snippets/events.md @@ -0,0 +1,19 @@ +### Creating an event + +<<< @/core/docs/snippets/event_create.ts + +### Tagging users + +<<< @/core/docs/snippets/tag_user.ts + +### Signing events + +<<< @/core/docs/snippets/sign_event.ts + +### Interest (kind 10015) Event + +Interest events are used to tell the network about your interest in a particular topic. +Those events and are making use of the [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md) specification +kind:10015 events. + +<<< @/core/docs/snippets/interest_event.ts diff --git a/docs/snippets/helpers.md b/docs/snippets/helpers.md new file mode 100644 index 000000000..17a06e099 --- /dev/null +++ b/docs/snippets/helpers.md @@ -0,0 +1,11 @@ +### Converting between formats + +<<< @/core/docs/snippets/nip-19-conversion.ts + +### Sharing content with relay hints + +<<< @/core/docs/snippets/nip-19-profile-event.ts + +### Validating NIP-19 strings + +<<< @/core/docs/snippets/nip-19-validate.ts \ No newline at end of file diff --git a/docs/snippets/publishing.md b/docs/snippets/publishing.md new file mode 100644 index 000000000..05337803f --- /dev/null +++ b/docs/snippets/publishing.md @@ -0,0 +1,19 @@ +### Publish Event + +<<< @/core/docs/snippets/publish_event.ts + +### Replaceable Event + +<<< @/core/docs/snippets/replace_event.ts + +### Specify Relays + +<<< @/core/docs/snippets/publish_to_relayset.ts + +### Track Publication Status + +<<< @/core/docs/snippets/publish_tracking.ts + +### Handling Failures + +<<< @/core/docs/snippets/publish_failure.ts \ No newline at end of file diff --git a/docs/snippets/signers.md b/docs/snippets/signers.md new file mode 100644 index 000000000..af167e5b8 --- /dev/null +++ b/docs/snippets/signers.md @@ -0,0 +1,27 @@ +### Signing events + +<<< @/core/docs/snippets/sign_event.ts + +### Sign with NSec + +<<< @/core/docs/snippets/sign_event_nsec.ts + +### Different signers + +<<< @/core/docs/snippets/sign_event_with_other_signers.ts + +### Sign using bunker + +<<< @/core/docs/snippets/sign_with_bunker.ts + +### Generate Keypair + +<<< @/core/docs/snippets/key_create.ts + +### Encrypt/Decrypt Keypair + +<<< @/core/docs/snippets/key_create_store.ts + +### NIP-49 encryption + +<<< @/core/docs/snippets/nip-49-encrypting.ts \ No newline at end of file diff --git a/docs/snippets/subscribing.md b/docs/snippets/subscribing.md new file mode 100644 index 000000000..3cd55928d --- /dev/null +++ b/docs/snippets/subscribing.md @@ -0,0 +1,19 @@ +### Simple Subscribe + +<<< @/core/docs/snippets/subscribe.ts + +### Subscribe on relays + +<<< @/core/docs/snippets/subscribe_relayset.ts + +### Event Handler + +<<< @/core/docs/snippets/subscribe_event_handlers.ts + +### Attaching Handlers + +<<< @/core/docs/snippets/subscribe_event_attach.ts + +### Exclusive Relays + +<<< @/core/docs/snippets/subscribe_relay_targetting.ts diff --git a/mobile/CHANGELOG.md b/mobile/CHANGELOG.md index fe3382e6c..32390ee02 100644 --- a/mobile/CHANGELOG.md +++ b/mobile/CHANGELOG.md @@ -54,13 +54,13 @@ - Add NIP-05 verification caching to SQLite adapters - Implements NIP-05 verification result caching in both cache-sqlite-wasm and mobile SQLite adapters to prevent unnecessary network requests. Both successful and failed verifications are now cached with smart expiration: + Implements NIP-05 verification result caching in both cache-sqlite-wasm and mobile SQLite adapters to prevent unnecessary network requests. Both successful and failed verifications are now cached with smart expiration: - Successful verifications are cached indefinitely - Failed verifications are cached for 1 hour (configurable) to prevent hammering down/non-existent endpoints - Expired failed verifications return "missing" to trigger retry - Fresh failed verifications return null to prevent re-fetch - This brings feature parity with cache-dexie and cache-memory adapters. + This brings feature parity with cache-dexie and cache-memory adapters. ### Patch Changes @@ -772,7 +772,6 @@ - c83166a: bump - 6e16e06: Enhance SQLite adapter to support decrypted events storage and retrieval. - import changes -- df73b9b: add component - Updated dependencies [c83166a] - Updated dependencies [5ab19ef] - Updated dependencies [6e16e06] @@ -786,7 +785,7 @@ ### Patch Changes -- add component +- add `EventContent` component ## 0.4.4 @@ -892,4 +891,4 @@ ### Patch Changes -- add default export +- add default export \ No newline at end of file diff --git a/mobile/docs/index.md b/mobile/docs/index.md deleted file mode 100644 index 79c465355..000000000 --- a/mobile/docs/index.md +++ /dev/null @@ -1,31 +0,0 @@ -# NDK Mobile - -A React Native/Expo implementation of [NDK (Nostr Development Kit)](https://github.com/nostr-dev-kit/ndk) that provides a complete toolkit for building Nostr applications on mobile platforms. - -## Features - -- 🔐 Multiple signer implementations supported via NDK Core (NIP-07, NIP-46, Private Key) and NDK Mobile (NIP-55). -- 💾 SQLite-based caching for offline support (`NDKCacheAdapterSqlite`). -- 🔄 Subscription management with automatic reconnection. -- 📱 React Native and Expo compatibility. -- 🪝 React hooks for easy state management (`useNDKStore`, `useNDKSessions`, `useSubscribe`, etc. via `@nostr-dev-kit/react`). -- 👛 Integrated wallet support (via `@nostr-dev-kit/ndk-wallet`). -- 🔄 **Persistent Sessions:** Automatically saves and loads user sessions and signers using `expo-secure-store`. - -## Installation - -```sh -# Install NDK Core, Hooks, Wallet, and Mobile -npm install @nostr-dev-kit/ndk @nostr-dev-kit/react @nostr-dev-kit/ndk-wallet @nostr-dev-kit/mobile expo-secure-store react-native-get-random-values @bacons/text-decoder expo-sqlite expo-crypto expo-file-system -# Ensure peer dependencies for expo-sqlite are met -npm install expo-sqlite/next -``` -*Note: Ensure all necessary peer dependencies for Expo modules like `expo-sqlite` are installed.* - -## Usage - -When using this library, you primarily interact with the core `NDK` instance and hooks from `@nostr-dev-kit/react`. `ndk-mobile` provides the `NDKCacheAdapterSqlite` for persistence, the `useSessionMonitor` hook for automatic session persistence, and the NIP-55 signer. - -## Example - -For a real application using this look at [Olas](https://github.com/pablof7z/olas). diff --git a/package.json b/package.json index fe9519960..d898ce6da 100755 --- a/package.json +++ b/package.json @@ -33,9 +33,9 @@ "cs:pub": "bun cs:check && changeset publish", "cs:ver": "changeset version && bunx syncpack fix-mismatches", "dev": "turbo dev --no-cache --continue", - "docs:build": "bash ./prepare-docs.sh && vitepress build docs", - "docs:dev": "bash ./prepare-docs.sh && npx vitepress dev docs", - "docs:preview": "vitepress preview", + "docs:build": "cd docs && vitepress build", + "docs:dev": "cd docs && npx vitepress dev", + "docs:preview": "cd docs && vitepress preview", "format": "biome format --write .", "lint": "biome check .", "release": "bun cs:check && turbo build --filter=docs^... && changeset publish", diff --git a/prepare-docs.sh b/prepare-docs.sh index 65a282c32..8a2dca93e 100755 --- a/prepare-docs.sh +++ b/prepare-docs.sh @@ -24,8 +24,7 @@ rm -rf \ "$DOCS_DIR/tutorial" \ "$DOCS_DIR/api-examples.md" \ "$DOCS_DIR/snippets" \ - "$DOCS_DIR/snippets" \ - "$DOCS_DIR/cache" \ +Re "$DOCS_DIR/cache" \ "$DOCS_DIR/mobile" \ "$DOCS_DIR/wallet" \ "$DOCS_DIR/wot" \ @@ -41,7 +40,6 @@ mkdir -p \ "$DOCS_DIR/mobile" \ "$DOCS_DIR/snippets/mobile" \ "$DOCS_DIR/wallet" \ - "$DOCS_DIR/wallet" \ "$DOCS_DIR/snippets/wallet" \ "$DOCS_DIR/wot" \ "$DOCS_DIR/wrappers" \ diff --git a/react/docs/index.md b/react/docs/getting-started.md similarity index 63% rename from react/docs/index.md rename to react/docs/getting-started.md index 40911655f..198aa2496 100644 --- a/react/docs/index.md +++ b/react/docs/getting-started.md @@ -1,82 +1,111 @@ -# NDK React Hooks +# NDK React -`@nostr-dev-kit/react` provides a set of React hooks and utilities to seamlessly integrate Nostr functionality into your React applications using the Nostr Development Kit (NDK). This library simplifies managing Nostr data, user sessions, and event subscriptions within your React components. +`@nostr-dev-kit/react` provides a set of React hooks and utilities to seamlessly integrate Nostr functionality into your +React applications using the Nostr Development Kit (NDK). -## Initialization +This library simplifies managing Nostr data, user sessions, and subscriptions within a React application. -The core of `@nostr-dev-kit/react` revolves around a shared NDK instance. Initialize it once at the root of your application using the `useNDKInit` hook. This ensures all hooks and stores use the same NDK configuration. +## Initialization -```tsx -// components/ndk.tsx -'use client'; +NDK React revolves around a shared (singleton) NDK instance. Initialize it at the root of your application using +the `useNDKInit` hook. -// Here we will initialize NDK and configure it to be available throughout the application -import NDK, { NDKNip07Signer, NDKPrivateKeySigner, NDKSigner } from "@nostr-dev-kit/ndk"; +This ensures all hooks and stores use the same/shared NDK instance and configuration. -// An optional in-browser cache adapter -import NDKCacheAdapterDexie from "@nostr-dev-kit/cache-dexie"; -import { NDKSessionLocalStorage, useNDKInit, useNDKSessionMonitor } from "@nostr-dev-kit/react"; -import { useEffect } from "react"; +### Provider Pattern -// Define explicit relays or use defaults -const explicitRelayUrls = ["wss://relay.primal.net", "wss://nos.lol", "wss://purplepag.es"]; +A common way to make react components available to all other components in your application is to make use of [the +provider pattern](https://www.patterns.dev/vanilla/provider-pattern/). -// Setup Dexie cache adapter (Client-side only) -let cacheAdapter: NDKCacheAdapterDexie | undefined; -if (typeof window !== "undefined") { - cacheAdapter = new NDKCacheAdapterDexie({ dbName: "your-app-name" }); -} +Create the provider component and put it somewhere close to your application layout: -// Create the singleton NDK instance -const ndk = new NDK({ explicitRelayUrls, cacheAdapter }); +```tsx +// app/NDKProvider.tsx +'use client'; -// Connect to relays on initialization (client-side) -if (typeof window !== "undefined") ndk.connect(); +import { useNDK, useNDKCurrentUser, useNDKInit } from "@nostr-dev-kit/react"; +import { useEffect } from "react"; -// Use the browser's localStorage for session storage -const sessionStorage = new NDKSessionLocalStorage(); +type ProvidersProps = { + children: React.ReactNode; +}; -/** - * Use an NDKHeadless component to initialize NDK in order to prevent application-rerenders - * when there are changes to the NDK or session state. - * - * Include this headless component in your app layout to initialize NDK correctly. - * @returns - */ -export default function NDKHeadless() { - const initNDK = useNDKInit(); +export function NDKProvider({ children }: ProvidersProps) { + + const initializeNDK = useNDKInit(); + const { ndk } = useNDK(); + + // Connect only when there is an active user + const shouldConnect = useNDKCurrentUser(); - useNDKSessionMonitor(sessionStorage, { - profile: true, // automatically fetch profile information for the active user - follows: true, // automatically fetch follows of the active user - }); + useEffect(() => { + initializeNDK({ + explicitRelayUrls: ['wss://relay.damus.io'], + }); + }, [initializeNDK]); useEffect(() => { - if (ndk) initNDK(ndk); - }, [initNDK]) - - return null; -} + if (!shouldConnect) return; + + // This will also reconnect when the instance changes + ndk?.connect(); + }, [ndk, shouldConnect]); + + return <>{children}; +} ``` +Then load that provider in the main application layout (can be server component): + ```tsx -// src/App.tsx -import React, { useEffect } from 'react'; -import { useNDKInit } from '@nostr-dev-kit/react'; -import { NDKSessionLocalStorage, useNDKSessionMonitor } from '@nostr-dev-kit/react'; -import YourMainApp from './YourMainApp'; // Your main application component -import NDKHeadless from "components/ndk.tsx"; +// src/Layout.tsx + +// Your main application component +import YourApp from './App'; +import NDKProvider from "./NDKProvider.tsx"; function App() { - return - - - + return ( + + + + + ); } export default App; ``` +### Headless Component + +```tsx +'use client'; + +import { useNDK, useNDKInit } from "@nostr-dev-kit/react"; + +/** + * Use an NDKHeadless component to initialize NDK to prevent application-rerenders + * when there are changes to the NDK or session state. Include this headless component in your app layout + * to initialize NDK correctly. + */ +export function NDKHeadless() { + const initializeNDK = useNDKInit(); + const { ndk } = useNDK(); + + useEffect(() => { + initializeNDK({ + explicitRelayUrls: [ 'wss://relay.damus.io'], + }); + }, [initializeNDK]); + + return null; +} +``` + +Notice that this NDK instance is not connected. [Read about connecting](/core/docs/fundamentals/connecting.html). + +## Using the NDK Instance + Once initialized, you can access the NDK instance anywhere in your component tree using the `useNDK` hook if needed, although many hooks use it implicitly. ```tsx @@ -89,34 +118,30 @@ function MyComponent() { } ``` -## Logging In -`@nostr-dev-kit/react` supports multiple-sessions. Use `useNDKSessionLogin()` to add a session. +## Logging in -Sessions can be read-only (logging by providing an `NDKUser`) or full-sessions with signing access (logging by providing an `NDKSigner`). +NDK can be initialised without [a signer](/core/docs/fundamentals/signers.html) as demonstrated +[in the first snippet](/react/docs/getting-started.html#headless-component). To log in with a signer the +`useNDKSessionLogin` +hook can be used which will: + +1. Create a new session +2. Activate [the signer](/core/docs/fundamentals/signers.html) +3. If successful, return and tell NDK about the current user ```tsx -import {useNDKSessionLogin, useNDKCurrentUser} from '@nostr-dev-kit/react'; -import {NDKPrivateKeySigner} from '@nostr-dev-kit/ndk'; +import { useNDK, useNDKSessionLogin } from '@nostr-dev-kit/react'; -function Signin() { +function MyComponent() { + const { ndk } = useNDK(); const login = useNDKSessionLogin(); - const nsec = "nsec1...."; // ask the user to enter their key or the preferred login method. - const currentUser = useNDKCurrentUser(); - const handleLogin = useCallback(async () => { - const signer = new NDKPrivateKeySigner(nsec); + const handleLoginWithNsec = async (nsec: string) => { + const signer = new NDKPrivateKeySigner(nsec); - await login(signer) - aloert("hello!") - }, []) - - useEffect(() => { - if (!currentUser) { - console.log('you are not logged in) - } else { - console.log('you are now logged in with user with pubkey', currentUser.pubkey) - } - }, [currentUser]) + await login(signer) + alert("User Signed in!") + }; } ``` @@ -219,17 +244,7 @@ function UserCard({ userIdentifier }: UserCardProps) { export default UserCard; ``` -## Managing User Sessions (Login & Multi-Account) - -`@nostr-dev-kit/react` provides robust session management, supporting both single and multiple user accounts. You can use the session monitoring functionality to automatically persist and restore user sessions across page reloads. - -The session monitor will: -1. Automatically restore sessions from storage when your app loads -2. Persist new sessions when users log in -3. Update storage when sessions change -4. Remove sessions from storage when users log out -You can use this alongside the other session management hooks like `useNDKSessionLogin`, `useNDKSessionLogout`, and `useNDKSessionSwitch`. ## Subscribing to Events diff --git a/react/docs/hooks.md b/react/docs/hooks.md new file mode 100644 index 000000000..d5d02850d --- /dev/null +++ b/react/docs/hooks.md @@ -0,0 +1,357 @@ +# NDK React + +`@nostr-dev-kit/react` provides a set of React hooks and utilities to seamlessly integrate Nostr functionality into your +React applications using the Nostr Development Kit (NDK). + +This library simplifies managing Nostr data, user sessions, and event subscriptions within a React application. + +## Initialization + +NDK React package revolves around a shared NDK instance. Initialize it once at the root of your application using the +`useNDKInit` hook. + +This ensures all hooks and stores use the same NDK configuration. + +### Headless Component + +```tsx +'use client'; + +import { useNDK, useNDKInit } from "@nostr-dev-kit/react"; + +/** + * Use an NDKHeadless component to initialize NDK to prevent application-rerenders + * when there are changes to the NDK or session state. Include this headless component in your app layout + * to initialize NDK correctly. + */ +export function NDKHeadless() { + const initializeNDK = useNDKInit(); + const { ndk } = useNDK(); + + useEffect(() => { + initializeNDK({ + explicitRelayUrls: [ 'wss://relay.damus.io'], + }); + }, [initializeNDK]); + + return null; +} +``` + +Notice that this NDK instance is not connected. [Read about connecting](/core/docs/fundamentals/connecting.html). + +### Provider Pattern + +A common way to make react components available to all other components in your application is to make use of [the +provider pattern](https://www.patterns.dev/vanilla/provider-pattern/). + +Create the provider component and put it somewhere close to your application layout: + +```tsx +// app/NDKProvider.tsx +'use client'; + +import { useNDK, useNDKCurrentUser, useNDKInit } from "@nostr-dev-kit/react"; +import { useEffect } from "react"; + +type ProvidersProps = { + children: React.ReactNode; +}; + +export function NDKProvider({ children }: ProvidersProps) { + + const initializeNDK = useNDKInit(); + const { ndk } = useNDK(); + + // Connect only when there is an active user + const shouldConnect = useNDKCurrentUser(); + + useEffect(() => { + initializeNDK({ + explicitRelayUrls: ['wss://relay.damus.io'], + }); + }, [initializeNDK]); + + useEffect(() => { + if (!shouldConnect) return; + + // This will also reconnect when the instance changes + ndk?.connect(); + }, [ndk, shouldConnect]); + + return <>{children}; +} +``` + +Then load that provider in the main application layout (can be server component): + +```tsx +// src/Layout.tsx + +// Your main application component +import YourApp from './App'; +import NDKProvider from "./NDKProvider.tsx"; + +function App() { + return ( + + + + + ); +} + +export default App; +``` + +## Using the NDK Instance + +Once initialized, you can access the NDK instance anywhere in your component tree using the `useNDK` hook if needed, +although many hooks use it implicitly. + +```tsx +import { useNDK } from '@nostr-dev-kit/react'; + +function MyComponent() { + const { ndk } = useNDK(); + + // Use ndk instance... +} +``` + +## Logging in + +NDK can be initialised without [a signer](/core/docs/fundamentals/signers.html) as demonstrated +[in the first snippet](/react/docs/getting-started.html#headless-component). To log in with a signer the +`useNDKSessionLogin` +hook can be used which will: + +1. Create a new session +2. Activate [the signer](/core/docs/fundamentals/signers.html) +3. If successful, return and tell NDK about the current user + +```tsx +import { useNDK, useNDKSessionLogin } from '@nostr-dev-kit/react'; + +function MyComponent() { + const { ndk } = useNDK(); + const login = useNDKSessionLogin(); + + const handleLoginWithNsec = async (nsec: string) => { + const signer = new NDKPrivateKeySigner(nsec); + + await login(signer) + alert("User Signed in!") + }; +} +``` + +## Working with Users + +### Using the `useUser` Hook + +The `useUser` hook resolves various user identifier formats into an NDKUser instance: + +```tsx +// Using hex pubkey +const user = useUser("abc123..."); + +// Using npub +const user = useUser("npub1..."); + +// Using nip05 +const user = useUser("alice@example.com"); + +// Using nprofile +const user = useUser("nprofile1..."); +``` + +### Fetching User Profiles + +Easily fetch and display Nostr user profiles using the `useProfileValue` hook. It handles caching and fetching logic +automatically. + +The `useProfileValue` hook accepts two parameters: + +- `userOrPubkey`: An NDKUser instance, public key string, null, or undefined +- `options`: An optional object with the following properties: + - `refresh`: A boolean indicating whether to force a refresh of the profile + - `subOpts`: NDKSubscriptionOptions to customize how the profile is fetched from relays + +```tsx +// Basic usage with pubkey +const profile = useProfileValue(pubkey); + +// Using with NDKUser from useUser hook +const user = useUser("alice@example.com"); +const profile = useProfileValue(user); + +// Force refresh the profile +const profile = useProfileValue(pubkey, { refresh: true }); + +// With subscription options +const profile = useProfileValue(user, { + refresh: false, + subOpts: { + closeOnEose: true, + // Other NDKSubscriptionOptions... + } +}); +``` + +The hook returns the user's profile (NDKUserProfile) or undefined if the profile is not available yet. + +```tsx +// src/components/UserCard.tsx +import React from 'react'; +import { useUser, useProfileValue } from '@nostr-dev-kit/react'; + +interface UserCardProps { + userIdentifier: string; // Can be pubkey, npub, nip05, or nprofile +} + +function UserCard({ userIdentifier }: UserCardProps) { + // Resolve user from any identifier format + const user = useUser(userIdentifier); + + // Fetch profile - will automatically fetch when user resolves + const profile = useProfileValue(user, { + refresh: false, // Whether to force a refresh of the profile + subOpts: { /* NDKSubscriptionOptions */ } // Options for the subscription + }); + + if (!user) { + return
Resolving user...
; + } + + if (!profile) { + return
Loading profile for {user.npub.substring(0, 12)}...
; + } + + return ( +
+ {profile.name +

{profile.displayName || profile.name || 'Anonymous'}

+

{profile.about}

+ {profile.nip05 &&

NIP-05: {profile.nip05}

} +
+ ); +} + +export default UserCard; +``` + +## Subscribing to Events + +Use the `useSubscribe` hook for subscribing to Nostr events based on filters. It returns the events found in a stable +set. + +```tsx +// src/components/NoteFeed.tsx +import React, { useState } from 'react'; +import { NDKFilter, NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; +import { useSubscribe } from '@nostr-dev-kit/react'; + +function NoteFeed() { + // there is no need to memoize filters + // Subscribe to events matching the filter + // `events` is a Set ordered by created_at (newest first by default) + const { events, eose } = useSubscribe( + [{ kinds: [NDKKind.Text] }], // no need to memoize filters, useSubscribe only depends on the explicit dependencies + { /* in case you need to pass options for the subscription */ }, // NDKSubscriptionOptions + [...dependencies] // in case you need to change the subscription + ); + + return ( +
+

Recent Notes

+ {events.length === 0 && eose &&

No notes found.

} +
    + {events.map((event: NDKEvent) => ( +
  • +

    {event.content}

    + By: + At: {new Date(event.created_at! * 1000).toLocaleString()} +
  • + ))} +
+
+ ); +} + +// Helper component (replace with actual implementation) +function UserProfileLink({ pubkey }: { pubkey: string }) { + const profile = useProfileValue(pubkey, { refresh: true }); + return {profile?.name || pubkey.substring(0, 8)}; +} + +export default NoteFeed; +``` + +### Fetching a Single Event with `useEvent` + +The `useEvent` hook allows you to fetch a single event by its ID or using a filter. This is useful when you need to +retrieve and display a specific event, such as an article, note, or any other Nostr content. + +```tsx +// src/components/EventViewer.tsx +import React from 'react'; +import { NDKEvent, NDKArticle } from '@nostr-dev-kit/ndk'; +import { useEvent } from '@nostr-dev-kit/react'; + +function EventViewer({ eventId }: { eventId: string }) { + // Fetch a single event by ID or filter + // Returns undefined while loading, null if not found, or the event if found + const event = useEvent( + eventId, + { wrap: true }, // Optional: UseSubscribeOptions + [] // Optional: dependencies array + ); + + if (event === undefined) return
Loading event...
; + if (event === null) return
Event not found
; + + return ( +
+

{event.title || 'Untitled'}

+

{event.content}

+ Published: {new Date(event.created_at! * 1000).toLocaleString()} +
+ ); +} + +export default EventViewer; +``` + +The `useEvent` hook accepts three parameters: + +- `idOrFilter`: A string ID, an NDKFilter object, or an array of NDKFilter objects to fetch the event +- `opts`: Optional UseSubscribeOptions to customize how the event is fetched +- `dependencies`: Optional array of dependencies that will trigger a refetch when changed + +The hook returns: + +- `undefined` while the event is being loaded +- `null` if the event was not found +- The event object if it was found + +This makes it easy to handle loading states and display appropriate UI for each case. + +## Other Useful Hooks + +`@nostr-dev-kit/react` provides several other specialized hooks: + +* `useFollows()`: Fetches the follow list of the active user. +* `useNDKWallet()`: Manages wallet connections (e.g., NWC) (via `import of "@nostr-dev-kit/react/wallet"`) +* `useNDKNutzapMonitor()`: Monitors for incoming zaps via Nutzap. (via `import of "@nostr-dev-kit/react/wallet"`) + +## Muting + +See [Muting Documentation](./muting.md) for details on how to mute, unmute, and check mute status for users, events, +hashtags, and words using NDK React hooks. \ No newline at end of file diff --git a/react/docs/hooks_old_docs_from_usage.md b/react/docs/hooks_old_docs_from_usage.md new file mode 100644 index 000000000..e244e7814 --- /dev/null +++ b/react/docs/hooks_old_docs_from_usage.md @@ -0,0 +1,57 @@ +## Usage with React Hooks (`ndk-hooks`) + +When using the `ndk-hooks` package in a React application, the initialization process involves creating the NDK instance +and then using the `useNDKInit` hook to make it available to the rest of your application via Zustand stores. + +This hook ensures that both the core NDK store and dependent stores (like the user profiles store) are properly +initialized with the NDK instance. + +It's recommended to create and connect your NDK instance outside of your React components, potentially in a dedicated +setup file or at the root of your application. Then, use the `useNDKInit` hook within your main App component or a +context provider to initialize the stores once the component mounts. + +```tsx +import React, {useEffect} from 'react'; // Removed useState +import NDK from '@nostr-dev-kit/ndk'; +import {useNDKInit} from '@nostr-dev-kit/ndk-hooks'; // Assuming package name + +// 1. Configure your NDK instance (e.g., in src/ndk.ts or similar) +const ndk = new NDK({ + explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.primal.net'], + // Add signer or cache adapter if needed +}); + +// 2. Connect the instance immediately +ndk.connect() + .then(() => console.log('NDK connected')) + .catch((e) => console.error('NDK connection error:', e)); + +// Example: App component or Context Provider that initializes NDK stores +function App() { + const initializeNDK = useNDKInit(); // Hook returns the function directly + + useEffect(() => { + // 3. Initialize stores once the component mounts + initializeNDK(ndk); + }, [initializeNDK]); // Dependency ensures this runs if initializeNDK changes, though unlikely + + // Your application components can now use other ndk-hooks + // No need to wait for connection state here, as hooks handle NDK readiness internally + return ( +
+ {/* ... Your app content using useProfile, useSubscribe, etc. ... */} +
+ ); +} + +export default App; +``` + +**Key Points:** + +* Create and configure your `NDK` instance globally or outside components. +* Call `ndk.connect()` immediately after creation. Connection happens in the background. +* In your main App or Provider component, get the `initializeNDK` function from `useNDKInit`. +* Use `useEffect` with an empty dependency array (or `[initializeNDK]`) to call `initializeNDK(ndk)` once on mount. +* This sets up the necessary Zustand stores. Other `ndk-hooks` will access the initialized `ndk` instance from the store + and handle its readiness internally. \ No newline at end of file diff --git a/react/docs/multi-account.md b/react/docs/multi-account.md new file mode 100644 index 000000000..97e4d112c --- /dev/null +++ b/react/docs/multi-account.md @@ -0,0 +1,47 @@ +## Sessions / Multi-Account + +`@nostr-dev-kit/react` supports multiple-sessions. Use `useNDKSessionLogin()` to add a session. + +Sessions can be read-only (logging by providing an `NDKUser`) or full-sessions with signing access (logging by providing +an `NDKSigner`). + +```tsx +import {useNDKSessionLogin, useNDKCurrentUser} from '@nostr-dev-kit/react'; +import {NDKPrivateKeySigner} from '@nostr-dev-kit/ndk'; + +function Signin() { + const login = useNDKSessionLogin(); + const nsec = "nsec1...."; // ask the user to enter their key or the preferred login method. + const currentUser = useNDKCurrentUser(); + + const handleLogin = useCallback(async () => { + const signer = new NDKPrivateKeySigner(nsec); + + await login(signer) + aloert("hello!") + }, []) + + useEffect(() => { + if (!currentUser) { + console.log('you are not logged in) + } else { + console.log('you are now logged in with user with pubkey', currentUser.pubkey) + } + }, [currentUser]) +} +``` + +## Managing User Sessions (Login & Multi-Account) + +`@nostr-dev-kit/react` provides robust session management, supporting both single and multiple user accounts. You can +use the session monitoring functionality to automatically persist and restore user sessions across page reloads. + +The session monitor will: + +1. Automatically restore sessions from storage when your app loads +2. Persist new sessions when users log in +3. Update storage when sessions change +4. Remove sessions from storage when users log out + +You can use this alongside the other session management hooks like `useNDKSessionLogin`, `useNDKSessionLogout`, and +`useNDKSessionSwitch`. \ No newline at end of file diff --git a/sessions/docs/advanced.md b/sessions/docs/advanced.md new file mode 100644 index 000000000..8042342b0 --- /dev/null +++ b/sessions/docs/advanced.md @@ -0,0 +1,99 @@ +## React to Changes + +Subscribe to session changes: + +```typescript +const unsubscribe = sessions.subscribe((state) => { + console.log('Active user:', state.activePubkey); + console.log('Total sessions:', state.sessions.size); + + // Update your UI... +}); + +// Later, cleanup +unsubscribe(); +``` + +## Read-Only Sessions + +Create a read-only session without a signer. Configure `fetches` in the constructor: + +```typescript +const sessions = new NDKSessionManager(ndk, { + fetches: { + follows: true, + relayList: true + } +}); + +const user = ndk.getUser({pubkey: somePubkey}); +await sessions.login(user); + +// Data is fetched and cached, but user can't sign events +``` + +## Using with NIP-07 (Browser Extensions) + +```typescript +import {NDKNip07Signer} from '@nostr-dev-kit/ndk'; + +const sessions = new NDKSessionManager(ndk, { + fetches: { + follows: true, + mutes: true + } +}); + +const signer = new NDKNip07Signer(); +await sessions.login(signer); +``` + +## CLI Example + +Complete example for a Node.js CLI tool: + +```typescript +#!/usr/bin/env node +import NDK from '@nostr-dev-kit/ndk'; +import {NDKSessionManager, FileStorage} from '@nostr-dev-kit/sessions'; +import {NDKPrivateKeySigner} from '@nostr-dev-kit/ndk'; + +const ndk = new NDK({explicitRelayUrls: ['wss://relay.damus.io']}); +await ndk.connect(); + +const sessions = new NDKSessionManager(ndk, { + storage: new FileStorage('./.ndk-sessions.json'), + autoSave: true, + fetches: { + follows: true + } +}); + +// Restore previous session +await sessions.restore(); + +if (!sessions.activeUser) { + // First time - login + const nsec = process.env.NOSTR_NSEC; + if (!nsec) throw new Error('NOSTR_NSEC not set'); + + const signer = new NDKPrivateKeySigner(nsec); + await sessions.login(signer); + + console.log('Logged in as', sessions.activeUser.npub); +} else { + console.log('Welcome back', sessions.activeUser.npub); +} + +// Use the active session to publish +const event = new NDKEvent(ndk, { + kind: 1, + content: 'Hello from CLI!' +}); + +await event.publish(); +console.log('Published:', event.id); + +// Cleanup +sessions.destroy(); +``` \ No newline at end of file diff --git a/sessions/docs/api.md b/sessions/docs/api.md index 6ac8c5dc6..e5790d936 100644 --- a/sessions/docs/api.md +++ b/sessions/docs/api.md @@ -507,49 +507,4 @@ try { } catch (error) { console.error('Failed to restore sessions:', error); } -``` - -## Best Practices - -### 1. Always Call destroy() - -```typescript -// In your cleanup code -sessions.destroy(); -``` - -### 2. Use autoSave - -```typescript -const sessions = new NDKSessionManager(ndk, { - autoSave: true, - saveDebounceMs: 500 -}); -``` - -### 3. Handle No Active Session - -```typescript -if (!sessions.activeUser) { - // Show login UI -} -``` - -### 4. Subscribe to Changes - -```typescript -const unsubscribe = sessions.subscribe((state) => { - // Update UI when sessions change -}); -``` - -### 5. Security - -```typescript -// ⚠️ NEVER commit .ndk-sessions.json to git! -// Add to .gitignore: -// .ndk-sessions.json - -// Use environment variables for sensitive keys -const nsec = process.env.NOSTR_NSEC; -``` +``` \ No newline at end of file diff --git a/sessions/docs/best-practices.md b/sessions/docs/best-practices.md new file mode 100644 index 000000000..b3ad7c634 --- /dev/null +++ b/sessions/docs/best-practices.md @@ -0,0 +1,44 @@ +# Best Practices + +## Always Call destroy() + +```typescript +// In your cleanup code +sessions.destroy(); +``` + +## Use autoSave + +```typescript +const sessions = new NDKSessionManager(ndk, { + autoSave: true, + saveDebounceMs: 500 +}); +``` + +## Handle No Active Session + +```typescript +if (!sessions.activeUser) { + // Show login UI +} +``` + +## Subscribe to Changes + +```typescript +const unsubscribe = sessions.subscribe((state) => { + // Update UI when sessions change +}); +``` + +## Security + +```typescript +// ⚠️ NEVER commit .ndk-sessions.json to git! +// Add to .gitignore: +// .ndk-sessions.json + +// Use environment variables for sensitive keys +const nsec = process.env.NOSTR_NSEC; +``` diff --git a/sessions/docs/index.md b/sessions/docs/index.md deleted file mode 100644 index be5502d52..000000000 --- a/sessions/docs/index.md +++ /dev/null @@ -1,141 +0,0 @@ -# Sessions - -`@nostr-dev-kit/sessions` is a framework-agnostic session management library for NDK that provides multi-account support, automatic data fetching, and flexible persistence. - -## Why Sessions? - -Managing user authentication and session state in Nostr applications can be complex. The sessions package simplifies: - -- **Multi-account management** - Let users switch between multiple Nostr identities seamlessly -- **Automatic data fetching** - Automatically fetch and cache follows, mutes, relay lists, and more -- **Persistence** - Save and restore sessions across app restarts -- **Framework agnostic** - Works with React, Svelte, Vue, vanilla JS, Node.js, etc. - -## Key Features - -### 🔐 Multiple Account Support - -Users can log in with multiple Nostr accounts and switch between them instantly. Perfect for: -- Personal and business accounts -- Testing with multiple identities -- Content creators managing multiple personas - -### 💾 Flexible Storage - -Built-in storage adapters for: -- **LocalStorage** - Browser-based persistence -- **FileStorage** - Node.js/CLI applications -- **MemoryStorage** - Testing or temporary sessions -- **Custom** - Implement your own storage backend - -### 🔄 Auto-Fetch User Data - -On login, automatically fetch: -- Contact list (kind 3 follows) -- Mute lists (kind 10000) -- Relay lists (kind 10002) -- Blocked relay lists (kind 10001) -- NIP-60 wallet data (kind 17375) -- Any custom replaceable event kinds - -### 🎯 Framework Integration - -Works seamlessly with: -- React (via `@nostr-dev-kit/react`) -- Svelte 5 (via `@nostr-dev-kit/svelte`) -- Mobile (React Native via `@nostr-dev-kit/mobile`) -- Vanilla JavaScript -- Node.js/CLI applications - -## Installation - -```bash -npm install @nostr-dev-kit/sessions -# or -bun add @nostr-dev-kit/sessions -``` - -## Quick Example - -```typescript -import NDK from '@nostr-dev-kit/ndk'; -import { NDKSessionManager, LocalStorage } from '@nostr-dev-kit/sessions'; -import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; - -const ndk = new NDK({ explicitRelayUrls: ['wss://relay.damus.io'] }); -await ndk.connect(); - -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage(), - autoSave: true, - fetches: { - follows: true, - mutes: true, - relayList: true - } -}); - -// Restore previous sessions -await sessions.restore(); - -// Login with auto-fetch -const signer = new NDKPrivateKeySigner(nsec); -await sessions.login(signer); - -console.log('Active user:', sessions.activeUser); -console.log('Follows:', sessions.activeSession?.followSet?.size); -``` - -## Next Steps - -- [Quick Start Guide](./quick-start) - Get up and running -- [API Reference](./api) - Complete API documentation -- [Migration Guide](./migration) - Migrating from ndk-hooks - -## Use Cases - -### Browser Applications -Perfect for web apps that need: -- User login/logout -- Multi-account switching -- Persistent sessions across page reloads -- Automatic relay and follow list management - -### Node.js/CLI Tools -Ideal for command-line tools that need: -- Saved credentials -- Multiple identity management -- Automated publishing with saved accounts - -### Mobile Applications -Great for React Native apps needing: -- Secure session storage -- Multi-account support -- Offline-first data caching - -## Architecture - -The sessions package is built on three core components: - -1. **NDKSessionManager** - Main API for managing sessions -2. **SessionStorage** - Pluggable storage backends -3. **NDKSession** - Individual session state and data - -All session state changes are observable via the subscribe pattern, making it easy to integrate with any reactive framework. - -## Security Considerations - -⚠️ **Important:** Session serialization stores private keys. In production: - -1. Use encrypted storage when possible -2. Never commit session files to version control -3. Use environment variables for sensitive keys -4. Consider NIP-07 (browser extensions) or NIP-46 (remote signers) for better security - -## Framework-Specific Documentation - -For framework-specific implementations using sessions: - -- **React** - See [`@nostr-dev-kit/react` hooks documentation](/hooks/session-management) -- **Svelte 5** - See [`@nostr-dev-kit/svelte` documentation](/wrappers/svelte) -- **Mobile** - See [`@nostr-dev-kit/mobile` documentation](/mobile/session) diff --git a/sessions/docs/introduction.md b/sessions/docs/introduction.md new file mode 100644 index 000000000..e27f18a9e --- /dev/null +++ b/sessions/docs/introduction.md @@ -0,0 +1,34 @@ +# @nostr-dev-kit/sessions + +Framework-agnostic session management for NDK with multi-account support and persistence. + +## Features + +- 🔐 **Multi-account support** - Manage multiple Nostr accounts simultaneously +- 💾 **Flexible persistence** - Built-in localStorage, filesystem, and memory storage +- 🔄 **Auto-sync** - Automatically fetch follows, mutes, relays, and events +- 🎯 **Framework-agnostic** - Works with React, Svelte, Vue, vanilla JS, Node.js, etc. +- 🔌 **Minimal boilerplate** - Simple, intuitive API +- 🎨 **Full TypeScript** - Complete type safety + +## Installation + +::: code-group + +```sh [npm] +npm i @nostr-dev-kit/sessions +``` + +```sh [pnpm] +pnpm add @nostr-dev-kit/sessions +``` + +```sh [yarn] +yarn add @nostr-dev-kit/sessions +``` + +```sh [bun] +bun add @nostr-dev-kit/sessions +``` + +::: \ No newline at end of file diff --git a/sessions/docs/multi-account.md b/sessions/docs/multi-account.md new file mode 100644 index 000000000..a22f6afdc --- /dev/null +++ b/sessions/docs/multi-account.md @@ -0,0 +1,17 @@ +# Multi-Account Management + +The library supports multiple accounts (called sessions) through [different signers](/core/docs/fundamentals/signers). + +## Adding a Session + +Each time you use `sessions.login` NDK will create a new session if [that signer](/core/docs/fundamentals/signers) isn't +already an active session. + +<<< @/sessions/docs/snippets/adding_sessions.ts + +## Switch Between Accounts + +Sessions can be listed with `getSessions()` which will return a `Map`. +Switching sessions is as simple as passing in the pubkey: + +<<< @/sessions/docs/snippets/session_switch.ts \ No newline at end of file diff --git a/sessions/docs/quick-start.md b/sessions/docs/quick-start.md index 58290d2b5..4d9e5466e 100644 --- a/sessions/docs/quick-start.md +++ b/sessions/docs/quick-start.md @@ -4,145 +4,58 @@ Get started with NDK Sessions in minutes. ## Installation -```bash -npm install @nostr-dev-kit/sessions @nostr-dev-kit/ndk -# or -bun add @nostr-dev-kit/sessions @nostr-dev-kit/ndk -``` - -## Basic Setup - -### 1. Initialize NDK - -First, create and connect your NDK instance: +::: code-group -```typescript -import NDK from '@nostr-dev-kit/ndk'; - -const ndk = new NDK({ - explicitRelayUrls: [ - 'wss://relay.damus.io', - 'wss://nos.lol', - 'wss://relay.nostr.band' - ] -}); - -await ndk.connect(); +```sh [npm] +npm i @nostr-dev-kit/sessions ``` -### 2. Create Session Manager - -Create a session manager with your preferred storage: - -```typescript -import { NDKSessionManager, LocalStorage } from '@nostr-dev-kit/sessions'; - -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage(), - autoSave: true, // Automatically save changes - saveDebounceMs: 500 // Debounce auto-saves -}); +```sh [pnpm] +pnpm add @nostr-dev-kit/sessions ``` -### 3. Restore Previous Sessions - -Restore any previously saved sessions: - -```typescript -await sessions.restore(); - -if (sessions.activeUser) { - console.log('Welcome back!', sessions.activeUser.npub); -} +```sh [yarn] +yarn add @nostr-dev-kit/sessions ``` -### 4. Login - -Login with a signer. To automatically fetch user data, configure `fetches` in the constructor: - -```typescript -import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; - -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage(), - autoSave: true, - fetches: { - follows: true, // Fetch contact list - mutes: true, // Fetch mute list - relayList: true, // Fetch relay list - wallet: true // Fetch NIP-60 wallet - } -}); - -const signer = new NDKPrivateKeySigner(nsecKey); -await sessions.login(signer); - -// Access session data -console.log('Following:', sessions.activeSession?.followSet?.size, 'users'); -console.log('Muted:', sessions.activeSession?.muteSet?.size, 'items'); +```sh [bun] +bun add @nostr-dev-kit/sessions ``` -## Storage Options +::: -### Browser (LocalStorage) +## Basic Setup -```typescript -import { LocalStorage } from '@nostr-dev-kit/sessions'; +### 1. Initialize NDK -const sessions = new NDKSessionManager(ndk, { - storage: new LocalStorage('my-app-sessions') // Custom key -}); -``` +First, create and [initialise your NDK instance](/core/docs/getting-started/usage#instantiate-ndk). -### Node.js (FileStorage) +### 2. Create Session Manager -```typescript -import { FileStorage } from '@nostr-dev-kit/sessions'; +Create a session manager with your preferred storage: -const sessions = new NDKSessionManager(ndk, { - storage: new FileStorage('./.ndk-sessions.json') -}); -``` +<<< @/sessions/docs/snippets/init_sessions_local_storage.ts -### Temporary (MemoryStorage) +More about [storage options](/sessions/docs/storage-options.html). -```typescript -import { MemoryStorage } from '@nostr-dev-kit/sessions'; +### 3. Restore Previous Sessions -const sessions = new NDKSessionManager(ndk, { - storage: new MemoryStorage(), // No persistence - autoSave: false -}); -``` +Restore any previously saved sessions: -## Multi-Account Management +<<< @/sessions/docs/snippets/sessions_restore.ts -### Login Multiple Accounts +### 4. Login (and auto-fetch) -```typescript -// Login first account (automatically active) -const signer1 = new NDKPrivateKeySigner(nsec1); -const pubkey1 = await sessions.login(signer1); +Login with [a signer](/core/docs/fundamentals/signers). To automatically fetch user data, configure `fetches` in the +constructor: -// Login second account -const signer2 = new NDKPrivateKeySigner(nsec2); -const pubkey2 = await sessions.login(signer2, { setActive: false }); +<<< @/sessions/docs/snippets/session_fetch_user_data.ts -console.log('Accounts:', sessions.getSessions().size); -``` +## Read-only Sessions -### Switch Between Accounts +TODO -> Write -```typescript -// Switch to different account -sessions.switchTo(pubkey2); -console.log('Active:', sessions.activePubkey); - -// Switch back -sessions.switchTo(pubkey1); -``` - -### Logout +## Logging out ```typescript // Logout specific account @@ -151,110 +64,3 @@ sessions.logout(pubkey1); // Or logout current active account sessions.logout(); ``` - -## React to Changes - -Subscribe to session changes: - -```typescript -const unsubscribe = sessions.subscribe((state) => { - console.log('Active user:', state.activePubkey); - console.log('Total sessions:', state.sessions.size); - - // Update your UI... -}); - -// Later, cleanup -unsubscribe(); -``` - -## Read-Only Sessions - -Create a read-only session without a signer. Configure `fetches` in the constructor: - -```typescript -const sessions = new NDKSessionManager(ndk, { - fetches: { - follows: true, - relayList: true - } -}); - -const user = ndk.getUser({ pubkey: somePubkey }); -await sessions.login(user); - -// Data is fetched and cached, but user can't sign events -``` - -## Using with NIP-07 (Browser Extensions) - -```typescript -import { NDKNip07Signer } from '@nostr-dev-kit/ndk'; - -const sessions = new NDKSessionManager(ndk, { - fetches: { - follows: true, - mutes: true - } -}); - -const signer = new NDKNip07Signer(); -await sessions.login(signer); -``` - -## CLI Example - -Complete example for a Node.js CLI tool: - -```typescript -#!/usr/bin/env node -import NDK from '@nostr-dev-kit/ndk'; -import { NDKSessionManager, FileStorage } from '@nostr-dev-kit/sessions'; -import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; - -const ndk = new NDK({ explicitRelayUrls: ['wss://relay.damus.io'] }); -await ndk.connect(); - -const sessions = new NDKSessionManager(ndk, { - storage: new FileStorage('./.ndk-sessions.json'), - autoSave: true, - fetches: { - follows: true - } -}); - -// Restore previous session -await sessions.restore(); - -if (!sessions.activeUser) { - // First time - login - const nsec = process.env.NOSTR_NSEC; - if (!nsec) throw new Error('NOSTR_NSEC not set'); - - const signer = new NDKPrivateKeySigner(nsec); - await sessions.login(signer); - - console.log('Logged in as', sessions.activeUser.npub); -} else { - console.log('Welcome back', sessions.activeUser.npub); -} - -// Use the active session to publish -const event = new NDKEvent(ndk, { - kind: 1, - content: 'Hello from CLI!' -}); - -await event.publish(); -console.log('Published:', event.id); - -// Cleanup -sessions.destroy(); -``` - -## Next Steps - -- [API Reference](./api) - Complete API documentation -- [Migration Guide](./migration) - Migrating from ndk-hooks -- [React Hooks](/hooks/session-management) - Using with React -- [Svelte](/wrappers/svelte) - Using with Svelte diff --git a/sessions/docs/snippets/adding_sessions.ts b/sessions/docs/snippets/adding_sessions.ts new file mode 100644 index 000000000..0247ef11b --- /dev/null +++ b/sessions/docs/snippets/adding_sessions.ts @@ -0,0 +1,9 @@ +// Login first account (automatically active) +const signer1 = new NDKPrivateKeySigner(nsec1); +const pubkey1 = await sessions.login(signer1); + +// Login second account +const signer2 = new NDKPrivateKeySigner(nsec2); +const pubkey2 = await sessions.login(signer2, { setActive: false }); + +console.log("Accounts:", sessions.getSessions().size); diff --git a/sessions/docs/snippets/init_sessions_local_storage.ts b/sessions/docs/snippets/init_sessions_local_storage.ts new file mode 100644 index 000000000..fecc2b2e5 --- /dev/null +++ b/sessions/docs/snippets/init_sessions_local_storage.ts @@ -0,0 +1,13 @@ +import NDK from "@nostr-dev-kit/ndk"; +import { LocalStorage, NDKSessionManager } from "@nostr-dev-kit/sessions"; + +// Create a new NDK instance with explicit relays +const ndk = new NDK({ + explicitRelayUrls: ["wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"], +}); + +const sessions = new NDKSessionManager(ndk, { + storage: new LocalStorage(), + autoSave: true, // Automatically save changes + saveDebounceMs: 500, // Debounce auto-saves +}); diff --git a/sessions/docs/snippets/session_fetch_user_data.ts b/sessions/docs/snippets/session_fetch_user_data.ts new file mode 100644 index 000000000..ee8008b77 --- /dev/null +++ b/sessions/docs/snippets/session_fetch_user_data.ts @@ -0,0 +1,19 @@ +import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; + +const sessions = new NDKSessionManager(ndk, { + storage: new LocalStorage(), + autoSave: true, + fetches: { + follows: true, // Fetch contact list + mutes: true, // Fetch mute list + relayList: true, // Fetch relay list + wallet: true, // Fetch NIP-60 wallet + }, +}); + +const signer = new NDKPrivateKeySigner(nsecKey); +await sessions.login(signer); + +// Access session data +console.log("Following:", sessions.activeSession?.followSet?.size, "users"); +console.log("Muted:", sessions.activeSession?.muteSet?.size, "items"); diff --git a/sessions/docs/snippets/session_switch.ts b/sessions/docs/snippets/session_switch.ts new file mode 100644 index 000000000..25780cf74 --- /dev/null +++ b/sessions/docs/snippets/session_switch.ts @@ -0,0 +1,6 @@ +// Switch to different account +sessions.switchTo(pubkey2); +console.log("Active:", sessions.activePubkey); + +// Switch back +sessions.switchTo(pubkey1); diff --git a/sessions/docs/snippets/sessions_restore.ts b/sessions/docs/snippets/sessions_restore.ts new file mode 100644 index 000000000..f1b7ea69b --- /dev/null +++ b/sessions/docs/snippets/sessions_restore.ts @@ -0,0 +1,5 @@ +await sessions.restore(); + +if (sessions.activeUser) { + console.log("Welcome back!", sessions.activeUser.npub); +} diff --git a/sessions/docs/storage-options.md b/sessions/docs/storage-options.md new file mode 100644 index 000000000..84688ab00 --- /dev/null +++ b/sessions/docs/storage-options.md @@ -0,0 +1,40 @@ +# Storage Options + +To make sure NDK persists sessions, you need to provide one of the following storage options. + +## Local Storage + +Suited for browser/web-app applications. + +```typescript +import { LocalStorage } from '@nostr-dev-kit/sessions'; + +const sessions = new NDKSessionManager(ndk, { + storage: new LocalStorage('my-app-sessions') // Custom key +}); +``` + +## File Storage + +Common used for NodeJS application or CLI scripts. + +```typescript +import { FileStorage } from '@nostr-dev-kit/sessions'; + +const sessions = new NDKSessionManager(ndk, { + storage: new FileStorage('./.ndk-sessions.json') +}); +``` + +## Memory Storage + +Temporary, mostly used for testing or short-lived application logic. + +```typescript +import { MemoryStorage } from '@nostr-dev-kit/sessions'; + +const sessions = new NDKSessionManager(ndk, { + storage: new MemoryStorage(), // No persistence + autoSave: false +}); +``` \ No newline at end of file diff --git a/sync/README.md b/sync/README.md index 60a829034..e69de29bb 100644 --- a/sync/README.md +++ b/sync/README.md @@ -1,459 +0,0 @@ -# @nostr-dev-kit/sync - -NIP-77 Negentropy sync protocol implementation for NDK. - -Efficient event synchronization using set reconciliation to minimize bandwidth usage when syncing events between clients and relays. - -## Features - -- **Bandwidth Efficient**: Uses Negentropy protocol to identify differences without transferring full event data -- **Automatic Fallback**: Falls back to standard `fetchEvents` for relays without NIP-77 support -- **Capability Tracking**: Caches which relays support Negentropy to optimize future syncs -- **Cache Integration**: Automatically populates NDK cache with synced events -- **Sequential Multi-Relay**: Syncs with multiple relays for optimal efficiency -- **Clean API**: Type-safe class-based interface - -## Installation - -```bash -npm install @nostr-dev-kit/sync -# or -bun add @nostr-dev-kit/sync -``` - -## Requirements - -- `@nostr-dev-kit/ndk` (workspace dependency) -- An NDK cache adapter must be configured - -## Usage - -### Recommended: NDKSync Class - -The `NDKSync` class provides a clean, stateful API with automatic relay capability tracking: - -```typescript -import NDK from '@nostr-dev-kit/ndk'; -import { NDKSync } from '@nostr-dev-kit/sync'; - -const ndk = new NDK({ - explicitRelayUrls: ['wss://relay.damus.io'], - cacheAdapter: myCacheAdapter // Required! -}); - -await ndk.connect(); - -// Create sync instance (caches relay capabilities) -const sync = new NDKSync(ndk); - -// Sync recent notes from a user -const result = await sync.sync({ - kinds: [1], - authors: [pubkey], - since: Math.floor(Date.now() / 1000) - 86400 // Last 24h -}); - -console.log(`Synced ${result.events.length} events`); -console.log(`Needed ${result.need.size} events from relays`); -console.log(`Have ${result.have.size} events relays don't`); -``` - -### Sync + Subscribe (Recommended) - -The `syncAndSubscribe` method combines efficient syncing with live subscriptions, ensuring you don't miss any events during the sync process: - -```typescript -import { NDKSync } from '@nostr-dev-kit/sync'; - -const sync = new NDKSync(ndk); - -const sub = await sync.syncAndSubscribe( - { kinds: [1], authors: [pubkey] }, - { - onEvent: (event) => { - console.log('Event:', event.content); - }, - onRelaySynced: (relay, count) => { - console.log(`✓ Synced ${count} events from ${relay.url}`); - }, - onSyncComplete: () => { - console.log('✓ All relays synced!'); - } - } -); - -// Subscription is already receiving events -// Background sync continues for historical events -``` - -**How it works:** -1. Immediately starts a subscription with `limit: 0` to catch new events -2. Returns the subscription right away (non-blocking) -3. Background: Syncs historical events from each relay - - Checks capability cache to determine if relay supports Negentropy - - Uses Negentropy where available (efficient) - - Falls back to `fetchEvents` for non-Negentropy relays -4. All synced events automatically flow to the subscription - -**Perfect for:** -- Wallet syncing (kind 7375, 7376, 5) -- Feed loading -- DM synchronization -- Any scenario where you need complete event coverage - -### Static Methods - -If you don't need persistent capability tracking, use static methods: - -```typescript -import { NDKSync } from '@nostr-dev-kit/sync'; - -// One-off sync -const result = await NDKSync.sync(ndk, { kinds: [1], limit: 100 }); - -// One-off sync and subscribe -const sub = await NDKSync.syncAndSubscribe(ndk, { kinds: [1] }); -``` - -### Checking Relay Capabilities - -The `NDKSync` class automatically tracks which relays support Negentropy: - -```typescript -const sync = new NDKSync(ndk); - -// Check if a relay supports Negentropy -const relay = ndk.pool.relays.get("wss://relay.example.com"); -const supported = await sync.checkRelaySupport(relay); - -// Get all relays that support Negentropy -const negentropyRelays = await sync.getNegentropyRelays(); - -// Get cached capability info -const capability = sync.getRelayCapability("wss://relay.example.com"); -console.log(capability?.supportsNegentropy); -console.log(capability?.lastChecked); - -// Clear cache for a specific relay (e.g., after relay update) -sync.clearCapabilityCache("wss://relay.example.com"); - -// Clear all capability cache -sync.clearCapabilityCache(); -``` - -### Sync Options - -```typescript -// Sync with specific relays -const result = await sync.sync(filters, { - relayUrls: ['wss://relay.nostr.band', 'wss://nos.lol'] -}); - -// Sync without auto-fetch -const result = await sync.sync(filters, { - autoFetch: false -}); - -// Manually fetch if needed -if (result.need.size > 100) { - console.log('Too many to fetch now, schedule for later'); -} else { - await ndk.fetchEvents({ ids: Array.from(result.need) }); -} -``` - -### Background Cache Warming - -```typescript -// Good for background sync to populate cache -await sync.sync(filters, { - autoFetch: true // Fetch and cache events -}); - -// Later, subscriptions will be instant from cache -const sub = ndk.subscribe(filters); -``` - -## Utility Functions - -For checking relay support without creating an `NDKSync` instance: - -```typescript -import { supportsNegentropy, getRelayCapabilities, filterNegentropyRelays } from '@nostr-dev-kit/sync'; - -// Check if a relay supports NIP-77 -const supported = await supportsNegentropy("wss://relay.example.com"); - -// Get detailed relay capabilities -const caps = await getRelayCapabilities("wss://relay.damus.io"); -console.log(`Negentropy: ${caps.supportsNegentropy}`); -console.log(`Software: ${caps.software} ${caps.version}`); -console.log(`Supported NIPs: ${caps.supportedNips.join(", ")}`); - -// Filter relays to only those with NIP-77 support -const allRelays = ["wss://relay1.com", "wss://relay2.com", "wss://relay3.com"]; -const syncRelays = await filterNegentropyRelays(allRelays); -``` - -## API Reference - -### `NDKSync` Class - -#### Constructor - -```typescript -new NDKSync(ndk: NDK) -``` - -Creates a new sync instance with relay capability tracking. - -#### Methods - -##### `sync(filters, options?)` - -Performs NIP-77 sync with relays. - -**Parameters:** -- `filters`: NDKFilter | NDKFilter[] - Filters to sync -- `options?`: NDKSyncOptions - Sync options - -**Returns:** Promise - -##### `syncAndSubscribe(filters, options?)` - -Combines sync with live subscription for complete event coverage. - -**Parameters:** -- `filters`: NDKFilter | NDKFilter[] - Filters to sync and subscribe -- `options?`: SyncAndSubscribeOptions - Subscription options with sync callbacks - -**Returns:** Promise - -##### `checkRelaySupport(relay)` - -Check if a relay supports Negentropy (uses cache when available). - -**Parameters:** -- `relay`: NDKRelay - Relay to check - -**Returns:** Promise - -##### `getNegentropyRelays(relays?)` - -Get all relays that support Negentropy. - -**Parameters:** -- `relays?`: NDKRelay[] - Optional specific relays to check (defaults to all NDK relays) - -**Returns:** Promise - -##### `getRelayCapability(relayUrl)` - -Get cached capability info for a relay. - -**Parameters:** -- `relayUrl`: string - Relay URL - -**Returns:** RelayCapability | undefined - -##### `clearCapabilityCache(relayUrl?)` - -Clear capability cache. - -**Parameters:** -- `relayUrl?`: string - Optional specific relay URL (clears all if omitted) - -#### Static Methods - -##### `NDKSync.sync(ndk, filters, options?)` - -Static convenience method for one-off syncs. - -##### `NDKSync.syncAndSubscribe(ndk, filters, options?)` - -Static convenience method for one-off sync+subscribe. - -### Types - -#### `NDKSyncOptions` - -```typescript -interface NDKSyncOptions { - // Relay selection - relaySet?: NDKRelaySet; // Explicit relay set - relayUrls?: string[]; // Or explicit relay URLs - - // Behavior - autoFetch?: boolean; // Auto-fetch events (default: true) - frameSizeLimit?: number; // Message size limit (default: 50000) -} -``` - -#### `NDKSyncResult` - -```typescript -interface NDKSyncResult { - events: NDKEvent[]; // Fetched events (if autoFetch: true) - need: Set; // Event IDs we needed - have: Set; // Event IDs we have -} -``` - -#### `SyncAndSubscribeOptions` - -```typescript -interface SyncAndSubscribeOptions extends NDKSubscriptionOptions { - onRelaySynced?: (relay: NDKRelay, eventCount: number) => void; - onSyncComplete?: () => void; - relaySet?: NDKRelaySet; - relayUrls?: string[]; -} -``` - -#### `RelayCapability` - -```typescript -interface RelayCapability { - supportsNegentropy: boolean; - lastChecked: number; - lastError?: string; -} -``` - -## How It Works - -1. **Cache Query**: Queries NDK cache for events matching filters -2. **Storage Build**: Builds Negentropy storage from cached events -3. **Capability Check**: Checks if relay supports NIP-77 (cached for 1 hour) -4. **Sync Session**: For Negentropy relays, exchanges compact messages to identify differences -5. **Fallback**: For non-Negentropy relays, uses standard `fetchEvents` -6. **Event Fetch**: Automatically fetches missing events (if autoFetch: true) -7. **Cache Update**: Saves fetched events to cache for future use - -### Sequential Multi-Relay Sync - -When syncing with multiple relays: - -```typescript -const result = await sync.sync(filters, { - relayUrls: ['wss://relay1.com', 'wss://relay2.com'] -}); -``` - -1. Sync with relay1, fetch events, cache them -2. Sync with relay2 (now includes relay1's events in storage) -3. Fetch any new events from relay2, cache them -4. Return merged results - -This approach is bandwidth-efficient: later relays see events from earlier relays and won't re-request them. - -## Error Handling - -```typescript -try { - const result = await sync.sync(filters); -} catch (error) { - if (error.message.includes('cache adapter')) { - console.error('Sync requires a cache adapter'); - } else { - console.error('Sync failed:', error); - } -} -``` - -Note: Relays without NIP-77 support automatically fall back to `fetchEvents` - no error is thrown. - -## Advanced Usage - -### Manual Negentropy - -For advanced use cases, you can use the Negentropy classes directly: - -```typescript -import { Negentropy, NegentropyStorage } from '@nostr-dev-kit/sync'; - -// Build storage from events -const storage = NegentropyStorage.fromEvents(events); - -// Create negentropy instance -const neg = new Negentropy(storage, 50000); - -// Generate initial message -const initialMsg = await neg.initiate(); - -// Process responses -const { nextMessage, have, need } = await neg.reconcile(response); -``` - - -## Protocol Details - -This package implements [NIP-77](https://nips.nostr.com/77) - Negentropy Protocol for set reconciliation. - -**Key Features:** -- Uses range-based set reconciliation -- XOR-based fingerprinting for efficient comparison -- Variable-length encoding for compact messages -- Frame size limiting to prevent oversized messages -- Automatic fallback to standard REQ/EVENT for non-supporting relays - -## Performance - -Negentropy is extremely bandwidth-efficient when relays support it: - -- **Small differences**: ~1-2 KB of messages to sync 1000s of events -- **Large differences**: Scales logarithmically with set size -- **No differences**: Single round-trip with ~100 bytes - -Compared to traditional REQ/EVENT syncing, Negentropy can reduce bandwidth by 10-100x when sets are mostly synchronized. - -## Development - -```bash -# Install dependencies -bun install - -# Build -bun run build - -# Watch mode -bun run dev - -# E2E Test (requires NIP-77 compatible relay) -bun run e2e - -# E2E test for syncAndSubscribe -bun run e2e:sync-subscribe - -# Lint -bun run lint -``` - -### E2E Examples - -**Test syncAndSubscribe pattern:** -```bash -# Using your own pubkey -bun run e2e:sync-subscribe npub1... - -# Or with hex pubkey -bun run e2e:sync-subscribe 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d -``` - -This will: -- Connect to multiple relays -- Start a live subscription immediately (non-blocking) -- Sync historical events in the background -- Show progress for each relay (Negentropy vs fallback) -- Display live events as they arrive in real-time -- Keep running to demonstrate live subscription - -**Note on Testing**: Most Nostr relays don't support NIP-77 yet, so the basic E2E test will timeout. The syncAndSubscribe E2E test works with any relay (falls back to fetchEvents). See [TESTING.md](./TESTING.md) for details on testing approaches and relay compatibility. - -## License - -MIT - -## Credits - -Based on the [Negentropy protocol](https://github.com/hoytech/negentropy) by Doug Hoyte, implementing the range-based set reconciliation algorithm by Aljoscha Meyer. diff --git a/docs/sync/index.md b/sync/docs/index.md similarity index 100% rename from docs/sync/index.md rename to sync/docs/index.md diff --git a/wallet/README.md b/wallet/README.md index 67641c0e9..068076774 100644 --- a/wallet/README.md +++ b/wallet/README.md @@ -127,17 +127,24 @@ For more detailed documentation on specific components: - [Nutzap Monitor State Store](./docs/nutzap-monitor-state-store.md) - [NWC Client](./docs/nwc-client.md) -## Installation +## Install -```bash -npm install @nostr-dev-kit/ndk-wallet +::: code-group + +```sh [npm] +npm i @nostr-dev-kit/ndk-wallet ``` -## Requirements +```sh [pnpm] +pnpm add @nostr-dev-kit/ndk-wallet +``` -- `@nostr-dev-kit/ndk`: Peer dependency -- Modern browser or Node.js environment +```sh [yarn] +yarn add @nostr-dev-kit/ndk-wallet +``` -## License +```sh [bun] +bun add @nostr-dev-kit/ndk-wallet +``` +::: -MIT diff --git a/wallet/docs/nip60-configuration.md b/wallet/docs/NDKCashuWallet.md similarity index 100% rename from wallet/docs/nip60-configuration.md rename to wallet/docs/NDKCashuWallet.md diff --git a/wallet/docs/NDKNWCWallet.md b/wallet/docs/NDKNWCWallet.md new file mode 100644 index 000000000..6fc932348 --- /dev/null +++ b/wallet/docs/NDKNWCWallet.md @@ -0,0 +1,181 @@ +# NWC Client (`NDKWalletNWC`) + +The `NDKWalletNWC` implements the [NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md) specification and +allows you to zap using [Nostr +Web Connect (NWC)](https://nwc.dev/). + +## Usage + +The NWC wallet can be used to: + +- Connect to NWC Wallets +- Send payment requests +- Query wallet information +- Handle payment responses + +## Install + +To initialise WebLN ensure you have the ndk-wallet installed + +::: code-group + +```sh [npm] +npm i @nostr-dev-kit/ndk-wallet +``` + +```sh [pnpm] +pnpm add @nostr-dev-kit/ndk-wallet +``` + +```sh [yarn] +yarn add @nostr-dev-kit/ndk-wallet +``` + +```sh [bun] +bun add @nostr-dev-kit/ndk-wallet +``` +::: + +## Initialising + + +```typescript +import NDK from "@nostr-dev-kit/ndk"; +import { NDKWalletNWC } from "@nostr-dev-kit/ndk-wallet"; + +const ndk = new NDK(); + +// Create an WebLN wallet +const wallet = new NDKWalletNWC(ndk); + +ndk.wallet = wallet; +``` + +## Pay an invoice + +To pay an invoice you can use the wallet instance directly: + +```typescript +// Generate payment request +const paymentRequest = { pr: "lnbc..."} as LnPaymentInfo; + +// Pay an invoice +await wallet.pay(paymentRequest); +``` + +Or using the NDK instance: + +```typescript +// Generate payment request +const paymentRequest = { pr: "lnbc..."} as LnPaymentInfo; + +// Pay an invoice +await ndk.wallet.pay(paymentRequest); +``` + +## Retrieve + +To retrieve the balance you can use the wallet instance directly or using the NDK instance; + +```typescript +console.log(wallet.balance); + +console.log(ndk.wallet.balance); +``` + +## Update balance + +To refresh the balance from the linked wallet use the wallet instance directly or using the NDK instance; + +```typescript +await wallet.updateBalance; + +await ndk.wallet.balance; +``` + +Make sure to await the promise to fully refresh the balance. + +## Nostr Zaps + +Lightning Zaps using NWC are described in [NIP-47](https://nostr-nips.com/nip-47). + +The full protocol (Step 1 to 9) is described in the respective docs. In the below example we will +refer to the steps described in the Nostr Implementation Protocol (NIP). + +```typescript + +import { + generateZapRequest, + getNip57ZapSpecFromLud, + NDKUser, + NDKZapper +} from "@nostr-dev-kit/ndk"; + +// Step 01: Calculate lnurl +const lud06 = ''; +const lud16 = 'pablof7z@primal.net'; + +// retrieve lightning callback data for user +const lnMeta = await getNip57ZapSpecFromLud( + { + lud16, + lud06, + }, + ndk, // pass NDK instance +); + +// Step 03: General Zap request +const target = new NDKUser({ npub: '' }); // User you want to zap +const amount = 1000; // amount in MSats + +// generate zap request, this event is not published to relays +const zapRequest = await generateZapRequest( + target, + ndk, + lnMeta, + ndk.activeUser.npub, + amount, + relays, // optional relays to send zapReceipt to + message, // message +); + +// Step 04 to 07: Retrieve invoice + +// create zapper instance and get lightning invoice +const zapper = new NDKZapper(target, amount, "msat", { ndk }); + +// retrieve the lightning invoice +const invoice = await zapper.getLnInvoice(zapRequest, amount, lnMeta); + +// pay the invoice +await wallet.lnPay({ + pr: invoice, +}); + +// extract the timestamp from the invoice +const invoiceDecoded = decode(invoice); +const timestampSection = invoiceDecoded.sections.filter( + (section) => section.name === "timestamp", +); + +const invoiceTimestamp = timestampSection[0] + ? Number(timestampSection[0].value) + : Math.floor(Date.now() / 1000); + + +// Step 08: Publish zap receipt +const zapReceiptEvent = new NDKEvent(ndk); +zapReceiptEvent.content = ""; +zapReceiptEvent.kind = NDKKind.Zap; +zapReceiptEvent.created_at = invoiceTimestamp; +zapReceiptEvent.tags.push(["p", hexpubkey]); +zapReceiptEvent.tags.push(["bolt11", invoice]); +zapReceiptEvent.tags.push(["client", "asknostr.site"]); +zapReceiptEvent.tags.push([ + "description", + JSON.stringify(zapRequest?.rawEvent()), +]); + +await zapReceiptEvent.publish(); + +``` \ No newline at end of file diff --git a/wallet/docs/NDKWebLNWallet.md b/wallet/docs/NDKWebLNWallet.md new file mode 100644 index 000000000..ff04080ef --- /dev/null +++ b/wallet/docs/NDKWebLNWallet.md @@ -0,0 +1,180 @@ +# WebLN Client (`NDKWebLNWallet`) + +The `NDKWebLNWallet` implements the NIP-57 specification and allows you to zap using WebLN. + +## Usage + +The WebLN wallet can be used to: + +- Connect to WebLn-compatible wallets +- Send payment requests +- Query wallet information +- Handle payment responses + +## Install + +To initialise WebLN ensure you have the ndk-wallet installed + +::: code-group + +```sh [npm] +npm i @nostr-dev-kit/ndk-wallet +``` + +```sh [pnpm] +pnpm add @nostr-dev-kit/ndk-wallet +``` + +```sh [yarn] +yarn add @nostr-dev-kit/ndk-wallet +``` + +```sh [bun] +bun add @nostr-dev-kit/ndk-wallet +``` +::: + +## Initialising + + +```typescript +import NDK from "@nostr-dev-kit/ndk"; +import { NDKWebLNWallet } from "@nostr-dev-kit/ndk-wallet"; + +const ndk = new NDK(); + +// Create an WebLN wallet +const wallet = new NDKWebLNWallet(ndk); + +ndk.wallet = wallet; +``` + +## Pay an invoice + +To pay an invoice you can use the wallet instance directly: + +```typescript +// Generate payment request +const paymentRequest = { pr: "lnbc..."} as LnPaymentInfo; + +// Pay an invoice +await wallet.pay(paymentRequest); +``` + +Or using the NDK instance: + +```typescript +// Generate payment request +const paymentRequest = { pr: "lnbc..."} as LnPaymentInfo; + +// Pay an invoice +await ndk.wallet.pay(paymentRequest); +``` + +## Retrieve + +To retrieve the balance you can use the wallet instance directly or using the NDK instance; + +```typescript +console.log(wallet.balance); + +console.log(ndk.wallet.balance); +``` + +## Update balance + +To refresh the balance from the linked WebLN entity use the WebLN wallet instance directly or using the NDK instance; + +```typescript +await wallet.updateBalance; + +await ndk.wallet.balance; +``` + +Make sure to await the promise to fully refresh the balance. + +## Nostr Zaps + +Lightning Zaps on the Nostr network are described in [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md). + +The full protocol (Step 1 to 9) is described in the respective docs. In the below example we will +refer to the steps described in the Nostr Implementation Protocol (NIP). + +```typescript + +import { + generateZapRequest, + getNip57ZapSpecFromLud, + NDKUser, + NDKZapper +} from "@nostr-dev-kit/ndk"; + +// Step 01: Calculate lnurl +const lud06 = ''; +const lud16 = 'pablof7z@primal.net'; + +// retrieve lightning callback data for user +const lnMeta = await getNip57ZapSpecFromLud( + { + lud16, + lud06, + }, + ndk, // pass NDK instance +); + +// Step 03: General Zap request +const target = new NDKUser({ npub: '' }); // User you want to zap +const amount = 1000; // amount in MSats + +// generate zap request, this event is not published to relays +const zapRequest = await generateZapRequest( + target, + ndk, + lnMeta, + ndk.activeUser.npub, + amount, + relays, // optional relays to send zapReceipt to + message, // message +); + +// Step 04 to 07: Retrieve invoice + +// create zapper instance and get lightning invoice +const zapper = new NDKZapper(target, amount, "msat", { ndk }); + +// retrieve the lightning invoice +const invoice = await zapper.getLnInvoice(zapRequest, amount, lnMeta); + +// pay the invoice +await wallet.lnPay({ + pr: invoice, +}); + +// extract the timestamp from the invoice +const invoiceDecoded = decode(invoice); +const timestampSection = invoiceDecoded.sections.filter( + (section) => section.name === "timestamp", +); + +const invoiceTimestamp = timestampSection[0] + ? Number(timestampSection[0].value) + : Math.floor(Date.now() / 1000); + + +// Step 08: Publish zap receipt +const zapReceiptEvent = new NDKEvent(ndk); +zapReceiptEvent.content = ""; +zapReceiptEvent.kind = NDKKind.Zap; +zapReceiptEvent.created_at = invoiceTimestamp; +zapReceiptEvent.tags.push(["p", hexpubkey]); +zapReceiptEvent.tags.push(["bolt11", invoice]); +zapReceiptEvent.tags.push(["client", "asknostr.site"]); +zapReceiptEvent.tags.push([ + "description", + JSON.stringify(zapRequest?.rawEvent()), +]); + +await zapReceiptEvent.publish(); + +``` + diff --git a/wallet/docs/index.md b/wallet/docs/index.md deleted file mode 100644 index 6b582294f..000000000 --- a/wallet/docs/index.md +++ /dev/null @@ -1,27 +0,0 @@ -# Wallet - -NDK provides an optional `@nostr-dev-kit/ndk-wallet` package, which provides common interfaces and functionalities to interface with different wallet adapters. - -Currently ndk-wallet supports: - -- NIP-60 wallets (nutsacks) -- NIP-47 connectors (NWC) -- WebLN (when available) - -## Connecting NDK with a wallet - -As a developer, the first thing you need to do to use a wallet in your app is to choose how you will connect to your wallet by using one of the wallet adapters. - -Once you instantiate the desired wallet, you simply pass it to ndk. - -```ts -const wallet = new NDKNWCWallet(ndk, { timeout: 5000, pairingCode: "nostr+walletconnect:...." }); -ndk.wallet = wallet; -wallet.on("timeout", (method: string) => console.log('Unable to complete the operation in time', { method })) -``` - -Now whenever you want to pay something, the wallet will be called. Refer to the Nutsack adapter to see more details of the interface. - -## Configuration - -- [NIP-60 Wallet Configuration](./nip60-configuration.md) - How to add/remove mints and relays diff --git a/wallet/docs/nutsack.md b/wallet/docs/nutsack.md index 64f10b6a7..4a0f4037b 100644 --- a/wallet/docs/nutsack.md +++ b/wallet/docs/nutsack.md @@ -1,4 +1,4 @@ -# NIP-60 (Nutack) wallets +# NIP-60 (Nutsack) wallets NIP-60 provides wallets that are available to any nostr application immediately; the goal of NIP-60 is to provide the same seamless experience nostr users expect from their apps with regards to the immediate aailability of their data, to their money. @@ -58,7 +58,6 @@ ndk.wallet = wallet; Now that we have a wallet, some funds, and we have ndk prepared to use that wallet, we'll send a zap. NDK provides a convenient `wallet` setter that allows ```ts -// have an ndk instance bound to `ndk`, you'll need to pass it in below const user = await NDKUser.fromNip05("_@f7z.io", ndk); const zapper = new NDKZapper(user, 1, "sat", { comment: "hello from my wallet!",