nostr-mcp-server 2.1.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.
package/README.md CHANGED
@@ -9,7 +9,7 @@ https://github.com/user-attachments/assets/1d2d47d0-c61b-44e2-85be-5985d2a81c64
9
9
 
10
10
  ## Features
11
11
 
12
- This server implements 17 tools for interacting with the Nostr network:
12
+ This server implements 40 tools for interacting with the Nostr network:
13
13
 
14
14
  ### Reading & Querying Tools
15
15
  1. `getProfile`: Fetches a user's profile information by public key
@@ -18,25 +18,54 @@ This server implements 17 tools for interacting with the Nostr network:
18
18
  4. `getReceivedZaps`: Fetches zaps received by a user, including detailed payment information
19
19
  5. `getSentZaps`: Fetches zaps sent by a user, including detailed payment information
20
20
  6. `getAllZaps`: Fetches both sent and received zaps for a user, clearly labeled with direction and totals
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)
21
25
 
22
26
  ### Identity & Profile Management Tools
23
- 7. `createKeypair`: Generate new Nostr keypairs in hex and/or npub/nsec format
24
- 8. `createProfile`: Create a new Nostr profile (kind 0 event) with metadata
25
- 9. `updateProfile`: Update an existing Nostr profile with new metadata
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
26
30
 
27
31
  ### Note Creation & Publishing Tools
28
- 10. `createNote`: Create unsigned kind 1 note events with specified content and tags
29
- 11. `signNote`: Sign note events with a private key, generating cryptographically valid signatures
30
- 12. `publishNote`: Publish signed notes to specified Nostr relays
31
- 13. `postNote`: All-in-one authenticated note posting using an existing private key (nsec/hex)
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)
32
61
 
33
62
  ### Anonymous Tools
34
- 14. `sendAnonymousZap`: Prepare an anonymous zap to a profile or event, generating a lightning invoice for payment
35
- 15. `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
36
65
 
37
66
  ### NIP-19 Entity Tools
38
- 16. `convertNip19`: Convert between different NIP-19 entity formats (hex, npub, nsec, note, nprofile, nevent, naddr)
39
- 17. `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
40
69
 
41
70
  All tools fully support both hex public keys and npub format, with user-friendly display of Nostr identifiers.
42
71
 
@@ -243,6 +272,24 @@ Once configured, you can ask Claude to use the Nostr tools by making requests li
243
272
  - "Publish this signed note to wss://relay.damus.io and wss://nos.lol"
244
273
  - "Post a note saying 'GM Nostr! ☀️' using my private key nsec1xyz..."
245
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
+
246
293
  ### Anonymous Operations
247
294
  - "Send an anonymous zap of 100 sats to npub1qny3tkh0acurzla8x3zy4nhrjz5zd8ne6dvrjehx9n9hr3lnj08qwuzwc8"
248
295
  - "Send 1000 sats to note1abcdef... with a comment saying 'Great post!'"
@@ -365,13 +412,14 @@ For zap queries, you can enable extra validation and debugging:
365
412
  ## Limitations
366
413
 
367
414
  - The server has a default 8-second timeout for queries to prevent hanging
368
- - 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
369
416
  - Only a subset of relays is used by default
370
417
 
371
418
  ## Implementation Details
372
419
 
373
420
  - Built with **[snstr](https://github.com/austinkelsay/snstr)** - a lightweight, modern TypeScript library for Nostr protocol implementation
374
421
  - Native support for npub format using NIP-19 encoding/decoding
422
+ - Relay authentication support for NIP-42 where relays require AUTH events
375
423
  - NIP-57 compliant zap receipt detection with direction-awareness (sent/received/self)
376
424
  - Advanced bolt11 invoice parsing with payment amount extraction
377
425
  - Smart caching system for improved performance with large volumes of zaps
@@ -424,16 +472,21 @@ To modify or extend this server:
424
472
  - `index.ts`: Main server and tool registration
425
473
  - `profile/profile-tools.ts`: Identity management, keypair generation, profile creation ([Documentation](./profile/README.md))
426
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
427
479
  - `zap/zap-tools.ts`: Zap-related functionality ([Documentation](./zap/README.md))
428
480
  - `utils/`: Shared utility functions
429
481
  - `constants.ts`: Global constants and relay configurations
430
482
  - `conversion.ts`: NIP-19 entity conversion utilities (hex/npub/nprofile/nevent/naddr)
431
483
  - `formatting.ts`: Output formatting helpers
484
+ - `keys.ts`: Shared private key normalization and auth key helpers
432
485
  - `nip19-tools.ts`: NIP-19 entity conversion and analysis tools
433
486
  - `pool.ts`: Nostr connection pool management
434
487
  - `ephemeral-relay.ts`: In-memory Nostr relay for testing
435
488
 
436
- 2. Run `bun run build` (or `npm run build`) to compile
489
+ 2. Run `bun run build` (or `npm run build`) to compile.
437
490
 
438
491
  3. Restart Claude for Desktop or Cursor to pick up your changes
439
492
 
@@ -456,15 +509,20 @@ The test suite includes:
456
509
 
457
510
  ### Unit Tests
458
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
459
514
  - `profile-notes-simple.test.ts` - Tests profile and note data structures
460
515
  - `profile-tools.test.ts` - Tests keypair generation, profile creation, and identity management
461
516
  - `note-creation.test.ts` - Tests note creation, signing, and publishing workflows
462
517
  - `note-tools-functions.test.ts` - Tests note formatting, creation, signing, and publishing functions
463
518
  - `note-tools-unit.test.ts` - Unit tests for note formatting functions
464
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
465
522
  - `zap-tools-simple.test.ts` - Tests zap processing and anonymous zap preparation
466
523
  - `zap-tools-tests.test.ts` - Tests zap validation, parsing, and direction determination
467
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
468
526
 
469
527
  ### Integration Tests
470
528
  - `integration.test.ts` - Tests interaction with an ephemeral Nostr relay including:
@@ -496,6 +554,10 @@ The codebase is organized into modules:
496
554
  - Specialized functionality in dedicated directories:
497
555
  - [`profile/`](./profile/README.md): Identity management, keypair generation, and profile creation
498
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
499
561
  - [`zap/`](./zap/README.md): Zap handling and anonymous zapping
500
562
  - Common utilities in the `utils/` directory
501
563
 
@@ -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,145 @@
1
+ import { describe, expect, test, jest, beforeEach } from '@jest/globals';
2
+ // Mock the actual Nostr pool functionality
3
+ jest.mock('../utils/pool.js', () => {
4
+ return {
5
+ getRelayPool: jest.fn(() => ({
6
+ connect: jest.fn(),
7
+ close: jest.fn(),
8
+ subscribeMany: jest.fn(),
9
+ })),
10
+ };
11
+ });
12
+ // Mock setTimeout for testing timeouts
13
+ jest.useFakeTimers();
14
+ // Helper function for creating timed out promises
15
+ const createTimedOutPromise = () => new Promise((resolve, reject) => {
16
+ setTimeout(() => reject(new Error('Request timed out')), 10000);
17
+ });
18
+ // Mock getProfile that simulates various error scenarios
19
+ const mockGetProfile = jest.fn();
20
+ // Helpers for common error scenarios
21
+ const simulateTimeoutError = () => {
22
+ mockGetProfile.mockImplementationOnce(() => createTimedOutPromise());
23
+ };
24
+ const simulateNetworkError = () => {
25
+ mockGetProfile.mockImplementationOnce(() => Promise.reject(new Error('Failed to connect to relay')));
26
+ };
27
+ const simulateInvalidPubkey = () => {
28
+ mockGetProfile.mockImplementationOnce(() => Promise.reject(new Error('Invalid pubkey format')));
29
+ };
30
+ const simulateMalformedEvent = () => {
31
+ mockGetProfile.mockImplementationOnce(() => Promise.resolve({
32
+ error: 'Malformed event',
33
+ details: 'Event missing required signature'
34
+ }));
35
+ };
36
+ describe('Error Handling and Edge Cases', () => {
37
+ beforeEach(() => {
38
+ mockGetProfile.mockReset();
39
+ });
40
+ test('timeout handling', async () => {
41
+ simulateTimeoutError();
42
+ try {
43
+ jest.useFakeTimers();
44
+ const profilePromise = mockGetProfile('valid-pubkey');
45
+ // Fast-forward time to trigger timeout
46
+ jest.advanceTimersByTime(10000);
47
+ await profilePromise;
48
+ fail('Expected promise to reject with timeout error');
49
+ }
50
+ catch (error) {
51
+ expect(error.message).toContain('timed out');
52
+ }
53
+ });
54
+ test('invalid pubkey format handling', async () => {
55
+ simulateInvalidPubkey();
56
+ try {
57
+ await mockGetProfile('invalid-pubkey-format');
58
+ fail('Expected promise to reject with invalid pubkey error');
59
+ }
60
+ catch (error) {
61
+ expect(error.message).toContain('Invalid pubkey');
62
+ }
63
+ });
64
+ test('network error handling', async () => {
65
+ simulateNetworkError();
66
+ try {
67
+ await mockGetProfile('valid-pubkey');
68
+ fail('Expected promise to reject with network error');
69
+ }
70
+ catch (error) {
71
+ expect(error.message).toContain('Failed to connect');
72
+ }
73
+ });
74
+ test('malformed event handling', async () => {
75
+ simulateMalformedEvent();
76
+ const result = await mockGetProfile('valid-pubkey');
77
+ expect(result.error).toBeDefined();
78
+ expect(result.error).toContain('Malformed event');
79
+ });
80
+ test('empty pubkey handling', async () => {
81
+ // Empty string pubkey
82
+ simulateInvalidPubkey();
83
+ try {
84
+ await mockGetProfile('');
85
+ fail('Expected promise to reject with invalid pubkey error');
86
+ }
87
+ catch (error) {
88
+ expect(error.message).toContain('Invalid pubkey');
89
+ }
90
+ });
91
+ test('extremely long pubkey handling', async () => {
92
+ // Extremely long input should be rejected
93
+ simulateInvalidPubkey();
94
+ const veryLongPubkey = 'a'.repeat(1000);
95
+ try {
96
+ await mockGetProfile(veryLongPubkey);
97
+ fail('Expected promise to reject with invalid pubkey error');
98
+ }
99
+ catch (error) {
100
+ expect(error.message).toContain('Invalid pubkey');
101
+ }
102
+ });
103
+ test('special character handling in pubkey', async () => {
104
+ // Pubkey with special characters
105
+ simulateInvalidPubkey();
106
+ try {
107
+ await mockGetProfile('npub1<script>alert("xss")</script>');
108
+ fail('Expected promise to reject with invalid pubkey error');
109
+ }
110
+ catch (error) {
111
+ expect(error.message).toContain('Invalid pubkey');
112
+ }
113
+ });
114
+ test('null or undefined pubkey handling', async () => {
115
+ // Null pubkey
116
+ simulateInvalidPubkey();
117
+ try {
118
+ await mockGetProfile(null);
119
+ fail('Expected promise to reject with invalid pubkey error');
120
+ }
121
+ catch (error) {
122
+ expect(error.message).toContain('Invalid pubkey');
123
+ }
124
+ // Undefined pubkey
125
+ simulateInvalidPubkey();
126
+ try {
127
+ await mockGetProfile(undefined);
128
+ fail('Expected promise to reject with invalid pubkey error');
129
+ }
130
+ catch (error) {
131
+ expect(error.message).toContain('Invalid pubkey');
132
+ }
133
+ });
134
+ test('all relays failing scenario', async () => {
135
+ // Simulate all relays failing
136
+ simulateNetworkError();
137
+ try {
138
+ await mockGetProfile('valid-pubkey', { relays: ['wss://relay1.example.com', 'wss://relay2.example.com'] });
139
+ fail('Expected promise to reject when all relays fail');
140
+ }
141
+ catch (error) {
142
+ expect(error.message).toContain('Failed to connect');
143
+ }
144
+ });
145
+ });
@@ -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
+ });