nostr-mcp-server 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +101 -35
  2. package/build/__tests__/basic.test.js +2 -2
  3. package/build/__tests__/dm-tools.test.js +192 -0
  4. package/build/__tests__/event-tools.test.js +64 -0
  5. package/build/__tests__/integration.test.js +4 -3
  6. package/build/__tests__/mocks.js +0 -10
  7. package/build/__tests__/nip19-conversion.test.js +1 -0
  8. package/build/__tests__/nip42-auth.test.js +53 -0
  9. package/build/__tests__/note-creation.test.js +1 -0
  10. package/build/__tests__/note-tools-functions.test.js +10 -5
  11. package/build/__tests__/note-tools-unit.test.js +1 -0
  12. package/build/__tests__/profile-notes-simple.test.js +1 -1
  13. package/build/__tests__/profile-postnote.test.js +1 -0
  14. package/build/__tests__/profile-tools.test.js +1 -0
  15. package/build/__tests__/relay-tools.test.js +73 -0
  16. package/build/__tests__/social-tools.test.js +75 -0
  17. package/build/__tests__/websocket-integration.test.js +5 -4
  18. package/build/__tests__/zap-tools-simple.test.js +1 -1
  19. package/build/__tests__/zap-tools-tests.test.js +1 -0
  20. package/build/bun.setup.js +3 -0
  21. package/build/dm/dm-tools.js +300 -0
  22. package/build/event/event-tools.js +482 -0
  23. package/build/index.js +247 -54
  24. package/build/note/note-tools.js +4 -0
  25. package/build/relay/relay-tools.js +164 -0
  26. package/build/social/social-tools.js +297 -0
  27. package/build/utils/constants.js +21 -1
  28. package/build/utils/conversion.js +10 -5
  29. package/build/utils/ephemeral-relay.js +100 -16
  30. package/build/utils/formatting.js +40 -0
  31. package/build/utils/index.js +1 -0
  32. package/build/utils/keys.js +26 -0
  33. package/build/zap/zap-tools.js +6 -1
  34. package/package.json +13 -16
package/README.md CHANGED
@@ -1,12 +1,15 @@
1
1
  # Nostr MCP Server
2
2
 
3
+ [![npm version](https://badge.fury.io/js/nostr-mcp-server.svg)](https://www.npmjs.com/package/nostr-mcp-server)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
3
6
  A Model Context Protocol (MCP) server that provides Nostr capabilities to LLMs like Claude.
4
7
 
5
8
  https://github.com/user-attachments/assets/1d2d47d0-c61b-44e2-85be-5985d2a81c64
6
9
 
7
10
  ## Features
8
11
 
9
- This server implements 18 tools for interacting with the Nostr network:
12
+ This server implements 40 tools for interacting with the Nostr network:
10
13
 
11
14
  ### Reading & Querying Tools
12
15
  1. `getProfile`: Fetches a user's profile information by public key
@@ -15,26 +18,54 @@ This server implements 18 tools for interacting with the Nostr network:
15
18
  4. `getReceivedZaps`: Fetches zaps received by a user, including detailed payment information
16
19
  5. `getSentZaps`: Fetches zaps sent by a user, including detailed payment information
17
20
  6. `getAllZaps`: Fetches both sent and received zaps for a user, clearly labeled with direction and totals
18
- 7. `searchNips`: Search through Nostr Implementation Possibilities (NIPs) with relevance scoring
21
+ 7. `queryEvents`: Generic event query tool supporting kinds/authors/ids/tags and timestamps
22
+ 8. `getContactList`: Fetches a user's contact list (kind 3) and followed pubkeys
23
+ 9. `getFollowing`: Alias of `getContactList`
24
+ 10. `getRelayList`: Fetches a user's relay list metadata (NIP-65 kind 10002)
19
25
 
20
26
  ### Identity & Profile Management Tools
21
- 8. `createKeypair`: Generate new Nostr keypairs in hex and/or npub/nsec format
22
- 9. `createProfile`: Create a new Nostr profile (kind 0 event) with metadata
23
- 10. `updateProfile`: Update an existing Nostr profile with new metadata
24
-
25
- ### Note Creation & Publishing Tools
26
- 11. `createNote`: Create unsigned kind 1 note events with specified content and tags
27
- 12. `signNote`: Sign note events with a private key, generating cryptographically valid signatures
28
- 13. `publishNote`: Publish signed notes to specified Nostr relays
29
- 14. `postNote`: All-in-one authenticated note posting using an existing private key (nsec/hex)
27
+ 11. `createKeypair`: Generate new Nostr keypairs in hex and/or npub/nsec format
28
+ 12. `createProfile`: Create a new Nostr profile (kind 0 event) with metadata
29
+ 13. `updateProfile`: Update an existing Nostr profile with new metadata
30
+
31
+ ### Note Creation & Publishing Tools
32
+ 14. `createNote`: Create unsigned kind 1 note events with specified content and tags
33
+ 15. `signNote`: Sign note events with a private key, generating cryptographically valid signatures
34
+ 16. `publishNote`: Publish signed notes to specified Nostr relays
35
+ 17. `postNote`: All-in-one authenticated note posting using an existing private key (nsec/hex)
36
+
37
+ ### Generic Event Tools
38
+ 18. `createNostrEvent`: Create unsigned Nostr events of any kind (provides low-level building block for NIPs beyond notes/profiles)
39
+ 19. `signNostrEvent`: Sign any unsigned Nostr event with a private key
40
+ 20. `publishNostrEvent`: Publish any signed Nostr event to relays
41
+
42
+ ### Social Tools
43
+ 21. `setRelayList`: Publish your relay list metadata (NIP-65 kind 10002)
44
+ 22. `follow`: Follow a pubkey by updating your contact list (kind 3)
45
+ 23. `unfollow`: Unfollow a pubkey by updating your contact list (kind 3)
46
+ 24. `reactToEvent`: React to an event (kind 7)
47
+ 25. `repostEvent`: Repost an event (kind 6)
48
+ 26. `deleteEvent`: Delete one or more events (kind 5 deletion request)
49
+ 27. `replyToEvent`: Reply to an event with correct NIP-10 thread tags (kind 1)
50
+
51
+ ### Messaging Tools
52
+ 28. `encryptNip04`: Encrypt plaintext using NIP-04 (AES-CBC) for direct messages
53
+ 29. `decryptNip04`: Decrypt ciphertext using NIP-04 (AES-CBC) for direct messages
54
+ 30. `sendDmNip04`: Send a NIP-04 encrypted DM (kind 4)
55
+ 31. `getDmConversationNip04`: Fetch and optionally decrypt a NIP-04 DM conversation (kind 4)
56
+ 32. `encryptNip44`: Encrypt plaintext using NIP-44 (ChaCha20 + HMAC)
57
+ 33. `decryptNip44`: Decrypt ciphertext using NIP-44 (ChaCha20 + HMAC)
58
+ 34. `sendDmNip44`: Send a NIP-44 encrypted DM using NIP-17 gift wrap (kind 1059)
59
+ 35. `decryptDmNip44`: Decrypt a NIP-17 gift wrapped DM (kind 1059)
60
+ 36. `getDmInboxNip44`: Fetch and decrypt your NIP-44 DM inbox (NIP-17 gift wraps, kind 1059)
30
61
 
31
62
  ### Anonymous Tools
32
- 15. `sendAnonymousZap`: Prepare an anonymous zap to a profile or event, generating a lightning invoice for payment
33
- 16. `postAnonymousNote`: Post an anonymous note using a randomly generated one-time keypair
63
+ 37. `sendAnonymousZap`: Prepare an anonymous zap to a profile or event, generating a lightning invoice for payment
64
+ 38. `postAnonymousNote`: Post an anonymous note using a randomly generated one-time keypair
34
65
 
35
66
  ### NIP-19 Entity Tools
36
- 17. `convertNip19`: Convert between different NIP-19 entity formats (hex, npub, nsec, note, nprofile, nevent, naddr)
37
- 18. `analyzeNip19`: Analyze and decode any NIP-19 entity to understand its type and contents
67
+ 39. `convertNip19`: Convert between different NIP-19 entity formats (hex, npub, nsec, note, nprofile, nevent, naddr)
68
+ 40. `analyzeNip19`: Analyze and decode any NIP-19 entity to understand its type and contents
38
69
 
39
70
  All tools fully support both hex public keys and npub format, with user-friendly display of Nostr identifiers.
40
71
 
@@ -46,7 +77,21 @@ All tools fully support both hex public keys and npub format, with user-friendly
46
77
  npm install -g nostr-mcp-server
47
78
  ```
48
79
 
49
- ### Option 2: Install from source
80
+ ### Option 2: Install from source (using Bun - Recommended)
81
+
82
+ ```bash
83
+ # Clone the repository
84
+ git clone https://github.com/austinkelsay/nostr-mcp-server.git
85
+ cd nostr-mcp-server
86
+
87
+ # Install dependencies
88
+ bun install
89
+
90
+ # Build the project
91
+ bun run build
92
+ ```
93
+
94
+ ### Option 3: Install from source (using npm)
50
95
 
51
96
  ```bash
52
97
  # Clone the repository
@@ -214,9 +259,6 @@ Once configured, you can ask Claude to use the Nostr tools by making requests li
214
259
  - "How many zaps has npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8 received?"
215
260
  - "Show me the zaps sent by npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8"
216
261
  - "Show me all zaps (both sent and received) for npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8"
217
- - "Search for NIPs about zaps"
218
- - "What NIPs are related to long-form content?"
219
- - "Show me NIP-23 with full content"
220
262
 
221
263
  ### Identity & Profile Management
222
264
  - "Generate a new Nostr keypair for me"
@@ -230,6 +272,24 @@ Once configured, you can ask Claude to use the Nostr tools by making requests li
230
272
  - "Publish this signed note to wss://relay.damus.io and wss://nos.lol"
231
273
  - "Post a note saying 'GM Nostr! ☀️' using my private key nsec1xyz..."
232
274
 
275
+ ### Generic Event Operations
276
+ - "Query kind 7 reaction events from this pubkey from the last 24 hours"
277
+ - "Create a kind 7 reaction event to this note ID with content '+'"
278
+ - "Sign this generic Nostr event with my private key and publish it"
279
+
280
+ ### Social & Relay Management
281
+ - "Follow this pubkey using my private key nsec1xyz..."
282
+ - "Unfollow this pubkey using my private key"
283
+ - "Reply to this event ID with 'Great point' using my private key"
284
+ - "Set my relay list to wss://relay.damus.io and wss://nos.lol"
285
+ - "Fetch the relay list for this npub"
286
+
287
+ ### Direct Messaging (NIP-04 / NIP-44)
288
+ - "Encrypt 'hey' with NIP-04 for this recipient pubkey"
289
+ - "Send a NIP-04 DM saying 'hello' to this pubkey using my private key"
290
+ - "Send a NIP-44 DM saying 'check this out' to this pubkey"
291
+ - "Fetch and decrypt my NIP-44 inbox using my private key"
292
+
233
293
  ### Anonymous Operations
234
294
  - "Send an anonymous zap of 100 sats to npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8"
235
295
  - "Send 1000 sats to note1abcdef... with a comment saying 'Great post!'"
@@ -349,22 +409,17 @@ For zap queries, you can enable extra validation and debugging:
349
409
 
350
410
  - "Show me all zaps for npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8 with validation and debug enabled"
351
411
 
352
- For NIP searches, you can control the number of results and include full content:
353
-
354
- - "Search for NIPs about zaps with full content"
355
- - "Show me the top 5 NIPs about relays"
356
- - "What NIPs are related to encryption? Show me 15 results"
357
-
358
412
  ## Limitations
359
413
 
360
414
  - The server has a default 8-second timeout for queries to prevent hanging
361
- - Only public keys in hex format or npub format are supported
415
+ - When a tool expects a public key, both hex and npub formats are supported
362
416
  - Only a subset of relays is used by default
363
417
 
364
418
  ## Implementation Details
365
419
 
366
420
  - Built with **[snstr](https://github.com/austinkelsay/snstr)** - a lightweight, modern TypeScript library for Nostr protocol implementation
367
421
  - Native support for npub format using NIP-19 encoding/decoding
422
+ - Relay authentication support for NIP-42 where relays require AUTH events
368
423
  - NIP-57 compliant zap receipt detection with direction-awareness (sent/received/self)
369
424
  - Advanced bolt11 invoice parsing with payment amount extraction
370
425
  - Smart caching system for improved performance with large volumes of zaps
@@ -417,49 +472,57 @@ To modify or extend this server:
417
472
  - `index.ts`: Main server and tool registration
418
473
  - `profile/profile-tools.ts`: Identity management, keypair generation, profile creation ([Documentation](./profile/README.md))
419
474
  - `note/note-tools.ts`: Note creation, signing, publishing, and reading functionality ([Documentation](./note/README.md))
475
+ - `event/event-tools.ts`: Generic event querying/creation/signing/publishing
476
+ - `social/social-tools.ts`: Follow/unfollow/reaction/repost/delete/reply flows
477
+ - `relay/relay-tools.ts`: Relay list (NIP-65) read/write and NIP-42 auth support
478
+ - `dm/dm-tools.ts`: NIP-04 and NIP-44/NIP-17 direct message tooling
420
479
  - `zap/zap-tools.ts`: Zap-related functionality ([Documentation](./zap/README.md))
421
- - `nips/nips-tools.ts`: Functions for searching NIPs ([Documentation](./nips/README.md))
422
480
  - `utils/`: Shared utility functions
423
481
  - `constants.ts`: Global constants and relay configurations
424
482
  - `conversion.ts`: NIP-19 entity conversion utilities (hex/npub/nprofile/nevent/naddr)
425
483
  - `formatting.ts`: Output formatting helpers
484
+ - `keys.ts`: Shared private key normalization and auth key helpers
426
485
  - `nip19-tools.ts`: NIP-19 entity conversion and analysis tools
427
486
  - `pool.ts`: Nostr connection pool management
428
487
  - `ephemeral-relay.ts`: In-memory Nostr relay for testing
429
488
 
430
- 2. Run `npm run build` to compile
489
+ 2. Run `bun run build` (or `npm run build`) to compile.
431
490
 
432
491
  3. Restart Claude for Desktop or Cursor to pick up your changes
433
492
 
434
493
  ## Testing
435
494
 
436
- We've implemented a comprehensive test suite using Jest to test both basic functionality and integration with the Nostr protocol:
495
+ We've implemented a comprehensive test suite using Bun's native test runner to test both basic functionality and integration with the Nostr protocol:
437
496
 
438
497
  ```bash
439
- # Run all tests
440
- npm test
498
+ # Run all tests (using Bun - Recommended)
499
+ bun test
441
500
 
442
501
  # Run a specific test file
443
- npm test -- __tests__/basic.test.ts
502
+ bun test __tests__/basic.test.ts
444
503
 
445
504
  # Run integration tests
446
- npm test -- __tests__/integration.test.ts
505
+ bun test __tests__/integration.test.ts
447
506
  ```
448
507
 
449
508
  The test suite includes:
450
509
 
451
510
  ### Unit Tests
452
511
  - `basic.test.ts` - Tests simple profile formatting and zap receipt processing
512
+ - `dm-tools.test.ts` - Tests NIP-04/NIP-44 direct message encryption/decryption and message workflows
513
+ - `event-tools.test.ts` - Tests generic event query/create/sign/publish helpers
453
514
  - `profile-notes-simple.test.ts` - Tests profile and note data structures
454
515
  - `profile-tools.test.ts` - Tests keypair generation, profile creation, and identity management
455
516
  - `note-creation.test.ts` - Tests note creation, signing, and publishing workflows
456
517
  - `note-tools-functions.test.ts` - Tests note formatting, creation, signing, and publishing functions
457
518
  - `note-tools-unit.test.ts` - Unit tests for note formatting functions
458
519
  - `profile-postnote.test.ts` - Tests authenticated note posting with existing private keys
520
+ - `relay-tools.test.ts` - Tests relay list tooling (NIP-65) and authenticated relay interactions
521
+ - `social-tools.test.ts` - Tests follow/unfollow, reactions, reposts, deletes, and replies
459
522
  - `zap-tools-simple.test.ts` - Tests zap processing and anonymous zap preparation
460
523
  - `zap-tools-tests.test.ts` - Tests zap validation, parsing, and direction determination
461
- - `search-nips-simple.test.ts` - Tests NIPs search functionality with relevance scoring
462
524
  - `nip19-conversion.test.ts` - Tests NIP-19 entity conversion and analysis (28 test cases)
525
+ - `nip42-auth.test.ts` - Tests NIP-42 relay authentication flow and key handling
463
526
 
464
527
  ### Integration Tests
465
528
  - `integration.test.ts` - Tests interaction with an ephemeral Nostr relay including:
@@ -491,8 +554,11 @@ The codebase is organized into modules:
491
554
  - Specialized functionality in dedicated directories:
492
555
  - [`profile/`](./profile/README.md): Identity management, keypair generation, and profile creation
493
556
  - [`note/`](./note/README.md): Note creation, signing, publishing, and reading functionality
557
+ - `event/`: Generic event query/create/sign/publish functionality
558
+ - `social/`: Social interactions (follow/unfollow/reactions/reposts/deletes/replies)
559
+ - `relay/`: Relay list tooling and relay auth support
560
+ - `dm/`: NIP-04 and NIP-44/NIP-17 direct messaging tools
494
561
  - [`zap/`](./zap/README.md): Zap handling and anonymous zapping
495
- - [`nips/`](./nips/README.md): NIPs search and caching functionality
496
562
  - Common utilities in the `utils/` directory
497
563
 
498
564
  This modular structure makes the codebase more maintainable, reduces duplication, and enables easier feature extensions. For detailed information about each module's features and implementation, see their respective documentation.
@@ -1,6 +1,6 @@
1
- import { jest } from '@jest/globals';
1
+ import { describe, test, expect, mock } from 'bun:test';
2
2
  // Mock the formatProfile function
3
- const mockFormatProfile = jest.fn((profile) => {
3
+ const mockFormatProfile = mock((profile) => {
4
4
  const content = typeof profile.content === 'string'
5
5
  ? JSON.parse(profile.content)
6
6
  : profile.content;
@@ -0,0 +1,192 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
+ import { schnorr } from "@noble/curves/secp256k1";
3
+ import { NostrRelay } from "../utils/ephemeral-relay.js";
4
+ import { QUERY_TIMEOUT } from "../utils/constants.js";
5
+ import { encryptNip04, decryptNip04, sendDmNip04, getDmConversationNip04, encryptNip44, decryptNip44, sendDmNip44, getDmInboxNip44, } from "../dm/dm-tools.js";
6
+ describe("dm-tools", () => {
7
+ let relay;
8
+ let relayUrl;
9
+ let authRelay;
10
+ let authRelayUrl;
11
+ // Fixed test keys (32 bytes hex). Not used outside this test context.
12
+ const alicePriv = "0000000000000000000000000000000000000000000000000000000000000001";
13
+ const bobPriv = "0000000000000000000000000000000000000000000000000000000000000002";
14
+ const alicePub = Buffer.from(schnorr.getPublicKey(alicePriv)).toString("hex");
15
+ const bobPub = Buffer.from(schnorr.getPublicKey(bobPriv)).toString("hex");
16
+ beforeAll(async () => {
17
+ relay = new NostrRelay(0);
18
+ await relay.start();
19
+ relayUrl = relay.url;
20
+ authRelay = new NostrRelay(0, undefined, true);
21
+ await authRelay.start();
22
+ authRelayUrl = authRelay.url;
23
+ });
24
+ afterAll(async () => {
25
+ await relay.close();
26
+ await authRelay.close();
27
+ });
28
+ test("NIP-04 encrypt/decrypt roundtrip", async () => {
29
+ const msg = `hello-${Date.now()}`;
30
+ const enc = await encryptNip04({ privateKey: alicePriv, recipientPubkey: bobPub, plaintext: msg });
31
+ expect(enc.success).toBe(true);
32
+ expect(enc.ciphertext).toBeTruthy();
33
+ const dec = await decryptNip04({ privateKey: bobPriv, senderPubkey: alicePub, ciphertext: enc.ciphertext });
34
+ expect(dec.success).toBe(true);
35
+ expect(dec.plaintext).toBe(msg);
36
+ });
37
+ test("NIP-44 encrypt/decrypt roundtrip", async () => {
38
+ const msg = `hello44-${Date.now()}`;
39
+ const enc = await encryptNip44({ privateKey: alicePriv, recipientPubkey: bobPub, plaintext: msg });
40
+ expect(enc.success).toBe(true);
41
+ expect(enc.ciphertext).toBeTruthy();
42
+ const dec = await decryptNip44({ privateKey: bobPriv, senderPubkey: alicePub, ciphertext: enc.ciphertext });
43
+ expect(dec.success).toBe(true);
44
+ expect(dec.plaintext).toBe(msg);
45
+ });
46
+ test("sendDmNip04 + getDmConversationNip04 decrypts both directions", async () => {
47
+ const a1 = await sendDmNip04({
48
+ privateKey: alicePriv,
49
+ recipientPubkey: bobPub,
50
+ content: "a->b",
51
+ relays: [relayUrl],
52
+ });
53
+ expect(a1.success).toBe(true);
54
+ const b1 = await sendDmNip04({
55
+ privateKey: bobPriv,
56
+ recipientPubkey: alicePub,
57
+ content: "b->a",
58
+ relays: [relayUrl],
59
+ });
60
+ expect(b1.success).toBe(true);
61
+ // Poll until both are queryable (relay OK may arrive before store).
62
+ const deadline = Date.now() + QUERY_TIMEOUT;
63
+ let convo = null;
64
+ while (Date.now() < deadline) {
65
+ convo = await getDmConversationNip04({
66
+ privateKey: alicePriv,
67
+ peerPubkey: bobPub,
68
+ relays: [relayUrl],
69
+ limit: 10,
70
+ decrypt: true,
71
+ });
72
+ if (convo.success && (convo.messages ?? []).length >= 2)
73
+ break;
74
+ await new Promise((r) => setTimeout(r, 25));
75
+ }
76
+ expect(convo?.success).toBe(true);
77
+ const contents = (convo.messages ?? []).map((m) => m.content);
78
+ expect(contents).toContain("a->b");
79
+ expect(contents).toContain("b->a");
80
+ });
81
+ test("sendDmNip44 + getDmInboxNip44 decrypts gift wrapped message", async () => {
82
+ const msg = `gift-${Date.now()}`;
83
+ const sent = await sendDmNip44({ privateKey: alicePriv, recipientPubkey: bobPub, content: msg, relays: [relayUrl] });
84
+ expect(sent.success).toBe(true);
85
+ const deadline = Date.now() + QUERY_TIMEOUT;
86
+ let inbox = null;
87
+ while (Date.now() < deadline) {
88
+ inbox = await getDmInboxNip44({ privateKey: bobPriv, relays: [relayUrl], limit: 25 });
89
+ if (inbox.success && (inbox.messages ?? []).some((m) => m.content === msg))
90
+ break;
91
+ await new Promise((r) => setTimeout(r, 25));
92
+ }
93
+ expect(inbox?.success).toBe(true);
94
+ const found = (inbox.messages ?? []).find((m) => m.content === msg);
95
+ expect(found).toBeTruthy();
96
+ expect(found.from).toBe(alicePub);
97
+ });
98
+ test("DM tools pass authPrivateKey through for NIP-42 relays", async () => {
99
+ const nip04Msg = `auth-nip04-${Date.now()}`;
100
+ const noAuth04 = await sendDmNip04({
101
+ privateKey: alicePriv,
102
+ recipientPubkey: bobPub,
103
+ content: nip04Msg,
104
+ relays: [authRelayUrl],
105
+ });
106
+ expect(noAuth04.success).toBe(false);
107
+ const withAuth04 = await sendDmNip04({
108
+ privateKey: alicePriv,
109
+ recipientPubkey: bobPub,
110
+ content: nip04Msg,
111
+ relays: [authRelayUrl],
112
+ authPrivateKey: alicePriv,
113
+ });
114
+ expect(withAuth04.success).toBe(true);
115
+ const noAuth04Query = await getDmConversationNip04({
116
+ privateKey: bobPriv,
117
+ peerPubkey: alicePub,
118
+ relays: [authRelayUrl],
119
+ limit: 10,
120
+ });
121
+ expect(noAuth04Query.success).toBe(false);
122
+ const deadline04 = Date.now() + QUERY_TIMEOUT;
123
+ let convo04 = null;
124
+ while (Date.now() < deadline04) {
125
+ convo04 = await getDmConversationNip04({
126
+ privateKey: bobPriv,
127
+ peerPubkey: alicePub,
128
+ relays: [authRelayUrl],
129
+ authPrivateKey: bobPriv,
130
+ limit: 10,
131
+ decrypt: true,
132
+ });
133
+ if (convo04.success && (convo04.messages ?? []).some((m) => m.content === nip04Msg))
134
+ break;
135
+ await new Promise((r) => setTimeout(r, 25));
136
+ }
137
+ expect(convo04?.success).toBe(true);
138
+ expect((convo04.messages ?? []).some((m) => m.content === nip04Msg)).toBe(true);
139
+ const nip44Msg = `auth-nip44-${Date.now()}`;
140
+ const noAuth44 = await sendDmNip44({
141
+ privateKey: alicePriv,
142
+ recipientPubkey: bobPub,
143
+ content: nip44Msg,
144
+ relays: [authRelayUrl],
145
+ });
146
+ expect(noAuth44.success).toBe(false);
147
+ const withAuth44 = await sendDmNip44({
148
+ privateKey: alicePriv,
149
+ recipientPubkey: bobPub,
150
+ content: nip44Msg,
151
+ relays: [authRelayUrl],
152
+ authPrivateKey: alicePriv,
153
+ });
154
+ expect(withAuth44.success).toBe(true);
155
+ const noAuth44Query = await getDmInboxNip44({
156
+ privateKey: bobPriv,
157
+ relays: [authRelayUrl],
158
+ limit: 25,
159
+ });
160
+ expect(noAuth44Query.success).toBe(false);
161
+ const deadline44 = Date.now() + QUERY_TIMEOUT;
162
+ let inbox44 = null;
163
+ while (Date.now() < deadline44) {
164
+ inbox44 = await getDmInboxNip44({
165
+ privateKey: bobPriv,
166
+ relays: [authRelayUrl],
167
+ authPrivateKey: bobPriv,
168
+ limit: 25,
169
+ });
170
+ if (inbox44.success && (inbox44.messages ?? []).some((m) => m.content === nip44Msg))
171
+ break;
172
+ await new Promise((r) => setTimeout(r, 25));
173
+ }
174
+ expect(inbox44?.success).toBe(true);
175
+ expect((inbox44.messages ?? []).some((m) => m.content === nip44Msg)).toBe(true);
176
+ });
177
+ test("DM conversation/inbox return structured error for invalid private keys", async () => {
178
+ const convo = await getDmConversationNip04({
179
+ privateKey: "bad-key",
180
+ peerPubkey: bobPub,
181
+ relays: [relayUrl],
182
+ });
183
+ expect(convo.success).toBe(false);
184
+ expect(convo.message).toBe("Invalid private key format.");
185
+ const inbox = await getDmInboxNip44({
186
+ privateKey: "bad-key",
187
+ relays: [relayUrl],
188
+ });
189
+ expect(inbox.success).toBe(false);
190
+ expect(inbox.message).toBe("Invalid private key format.");
191
+ });
192
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
+ import { NostrRelay } from "../utils/ephemeral-relay.js";
3
+ import { DEFAULT_RELAYS, KINDS, QUERY_TIMEOUT } from "../utils/constants.js";
4
+ import { createNostrEvent, signNostrEvent, publishNostrEvent, queryEvents, } from "../event/event-tools.js";
5
+ describe("event-tools", () => {
6
+ let relay;
7
+ let relayUrl;
8
+ beforeAll(async () => {
9
+ relay = new NostrRelay(0);
10
+ await relay.start();
11
+ relayUrl = relay.url;
12
+ });
13
+ afterAll(async () => {
14
+ await relay.close();
15
+ });
16
+ test("create/sign/publish/query a generic kind 7 reaction event", async () => {
17
+ // Fixed test key (32 bytes hex). Not used outside this test context.
18
+ const privateKey = "0000000000000000000000000000000000000000000000000000000000000001";
19
+ const created = await createNostrEvent({
20
+ kind: KINDS.REACTION,
21
+ content: "+",
22
+ tags: [["e", "deadbeef".repeat(8)]],
23
+ privateKey,
24
+ });
25
+ expect(created.success).toBe(true);
26
+ expect(created.event?.kind).toBe(KINDS.REACTION);
27
+ expect(created.event?.content).toBe("+");
28
+ const signed = await signNostrEvent({
29
+ privateKey,
30
+ event: created.event,
31
+ });
32
+ expect(signed.success).toBe(true);
33
+ expect(signed.signedEvent?.id).toBeTruthy();
34
+ expect(signed.signedEvent?.sig).toBeTruthy();
35
+ // Prefer the ephemeral relay for test determinism; fall back to defaults if it's unavailable.
36
+ const relays = relayUrl ? [relayUrl] : DEFAULT_RELAYS;
37
+ const published = await publishNostrEvent({
38
+ signedEvent: signed.signedEvent,
39
+ relays,
40
+ });
41
+ if (!published.success) {
42
+ throw new Error(published.message);
43
+ }
44
+ expect(published.success).toBe(true);
45
+ // Poll queryEvents until it shows up (relay sends OK before storing).
46
+ const deadline = Date.now() + QUERY_TIMEOUT;
47
+ let last = null;
48
+ while (Date.now() < deadline) {
49
+ last = await queryEvents({
50
+ relays,
51
+ kinds: [KINDS.REACTION],
52
+ authors: [signed.signedEvent.pubkey],
53
+ limit: 25,
54
+ });
55
+ if (last.success && (last.events ?? []).some((e) => e.id === signed.signedEvent.id))
56
+ break;
57
+ await new Promise((r) => setTimeout(r, 25));
58
+ }
59
+ if (!last?.success) {
60
+ throw new Error(last?.message || "queryEvents failed");
61
+ }
62
+ expect((last.events ?? []).some((e) => e.id === signed.signedEvent.id)).toBe(true);
63
+ });
64
+ });
@@ -1,6 +1,7 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
1
2
  import { NostrRelay } from '../utils/ephemeral-relay.js';
2
3
  import { schnorr } from '@noble/curves/secp256k1';
3
- import { randomBytes } from 'crypto';
4
+ import { randomBytes } from 'node:crypto';
4
5
  import { sha256 } from '@noble/hashes/sha256';
5
6
  // Generate a keypair for testing
6
7
  function generatePrivateKey() {
@@ -34,14 +35,14 @@ function createSignedEvent(privateKey, kind, content, tags = []) {
34
35
  }
35
36
  describe('Nostr Integration Tests', () => {
36
37
  let relay;
37
- const testPort = 9700;
38
38
  let privateKey;
39
39
  let publicKey;
40
40
  beforeAll(async () => {
41
41
  privateKey = generatePrivateKey();
42
42
  publicKey = getPublicKey(privateKey);
43
43
  // Start the ephemeral relay
44
- relay = new NostrRelay(testPort);
44
+ // Use port 0 to avoid CI/local port conflicts.
45
+ relay = new NostrRelay(0);
45
46
  await relay.start();
46
47
  });
47
48
  afterAll(async () => {
@@ -1,5 +1,3 @@
1
- // Mock Nostr events and utility functions for testing
2
- import { jest } from '@jest/globals';
3
1
  export const MOCK_HEX_PUBKEY = '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e';
4
2
  export const MOCK_NPUB = 'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6';
5
3
  export const mockProfile = {
@@ -64,14 +62,6 @@ export const mockZapReceipt = {
64
62
  content: '',
65
63
  sig: 'mock_signature'
66
64
  };
67
- // Mock pool functions
68
- export const mockPool = {
69
- get: jest.fn(),
70
- querySync: jest.fn(),
71
- close: jest.fn()
72
- };
73
- // Mock for getFreshPool function
74
- export const getFreshPoolMock = jest.fn().mockReturnValue(mockPool);
75
65
  // Mock response for lightning service for anonymous zaps
76
66
  export const mockLightningServiceResponse = {
77
67
  callback: 'https://example.com/callback',
@@ -1,3 +1,4 @@
1
+ import { describe, it, expect, beforeAll } from 'bun:test';
1
2
  import { convertNip19, analyzeNip19 } from '../utils/nip19-tools.js';
2
3
  import { generateKeypair, encodePublicKey, encodePrivateKey, encodeNoteId, encodeProfile, encodeEvent, encodeAddress } from 'snstr';
3
4
  describe('NIP-19 Conversion Tools', () => {
@@ -0,0 +1,53 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
+ import { NostrRelay } from "../utils/ephemeral-relay.js";
3
+ import { createNostrEvent, signNostrEvent, publishNostrEvent, queryEvents } from "../event/event-tools.js";
4
+ import { KINDS } from "../utils/constants.js";
5
+ describe("NIP-42 AUTH", () => {
6
+ let relay;
7
+ let relayUrl;
8
+ // Fixed test key (32 bytes hex). Not used outside this test context.
9
+ const privateKey = "0000000000000000000000000000000000000000000000000000000000000001";
10
+ beforeAll(async () => {
11
+ relay = new NostrRelay(0, undefined, true);
12
+ await relay.start();
13
+ relayUrl = relay.url;
14
+ });
15
+ afterAll(async () => {
16
+ await relay.close();
17
+ });
18
+ test("publish/query fail without authPrivateKey and succeed with it", async () => {
19
+ const created = await createNostrEvent({
20
+ kind: KINDS.TEXT,
21
+ content: `auth-${Date.now()}`,
22
+ tags: [],
23
+ privateKey,
24
+ });
25
+ expect(created.success).toBe(true);
26
+ const signed = await signNostrEvent({ privateKey, event: created.event });
27
+ expect(signed.success).toBe(true);
28
+ const noAuthPub = await publishNostrEvent({ signedEvent: signed.signedEvent, relays: [relayUrl] });
29
+ expect(noAuthPub.success).toBe(false);
30
+ const withAuthPub = await publishNostrEvent({
31
+ signedEvent: signed.signedEvent,
32
+ relays: [relayUrl],
33
+ authPrivateKey: privateKey,
34
+ });
35
+ expect(withAuthPub.success).toBe(true);
36
+ const noAuthQuery = await queryEvents({
37
+ relays: [relayUrl],
38
+ kinds: [KINDS.TEXT],
39
+ authors: [signed.signedEvent.pubkey],
40
+ limit: 10,
41
+ });
42
+ expect(noAuthQuery.success).toBe(false);
43
+ const withAuthQuery = await queryEvents({
44
+ relays: [relayUrl],
45
+ authPrivateKey: privateKey,
46
+ kinds: [KINDS.TEXT],
47
+ authors: [signed.signedEvent.pubkey],
48
+ limit: 10,
49
+ });
50
+ expect(withAuthQuery.success).toBe(true);
51
+ expect((withAuthQuery.events ?? []).some((e) => e.id === signed.signedEvent.id)).toBe(true);
52
+ });
53
+ });
@@ -1,3 +1,4 @@
1
+ import { describe, it, expect } from 'bun:test';
1
2
  import { createNote, signNote, publishNote } from '../note/note-tools.js';
2
3
  import { createKeypair } from '../profile/profile-tools.js';
3
4
  describe('Note Creation Tools', () => {
@@ -1,16 +1,16 @@
1
- import { jest } from '@jest/globals';
1
+ import { mock, describe, it, expect, beforeAll, beforeEach } from 'bun:test';
2
2
  import { generateKeypair } from 'snstr';
3
3
  // Mock the pool to prevent real WebSocket connections
4
4
  const mockPool = {
5
- close: jest.fn(),
6
- publish: jest.fn().mockReturnValue([
5
+ close: mock(() => { }),
6
+ publish: mock(() => [
7
7
  Promise.resolve({ success: true }),
8
8
  Promise.resolve({ success: true })
9
9
  ])
10
10
  };
11
11
  // Mock the pool module directly
12
- jest.mock('../utils/pool.js', () => ({
13
- getFreshPool: jest.fn(() => mockPool)
12
+ mock.module('../utils/pool.js', () => ({
13
+ getFreshPool: mock(() => mockPool)
14
14
  }));
15
15
  // Now import the functions that use the mocked module
16
16
  import { formatProfile, formatNote, createNote, signNote, publishNote } from '../note/note-tools.js';
@@ -19,6 +19,11 @@ describe('Note Tools Functions', () => {
19
19
  beforeAll(async () => {
20
20
  testKeys = await generateKeypair();
21
21
  });
22
+ beforeEach(() => {
23
+ // Reset mock state between tests
24
+ mockPool.close.mockClear();
25
+ mockPool.publish.mockClear();
26
+ });
22
27
  describe('formatProfile', () => {
23
28
  it('should format a complete profile', () => {
24
29
  const profileEvent = {
@@ -1,3 +1,4 @@
1
+ import { describe, it, expect, beforeAll } from 'bun:test';
1
2
  import { generateKeypair } from 'snstr';
2
3
  import { formatProfile, formatNote, } from '../note/note-tools.js';
3
4
  describe('Note Tools Unit Tests', () => {