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 +76 -14
- package/build/__tests__/dm-tools.test.js +192 -0
- package/build/__tests__/error-handling.test.js +145 -0
- package/build/__tests__/event-tools.test.js +64 -0
- package/build/__tests__/format-conversion.test.js +137 -0
- package/build/__tests__/integration.test.js +2 -2
- package/build/__tests__/nip42-auth.test.js +53 -0
- package/build/__tests__/nips-search.test.js +109 -0
- package/build/__tests__/relay-specification.test.js +136 -0
- package/build/__tests__/relay-tools.test.js +73 -0
- package/build/__tests__/search-nips-simple.test.js +96 -0
- package/build/__tests__/social-tools.test.js +75 -0
- package/build/__tests__/websocket-integration.test.js +3 -3
- package/build/dm/dm-tools.js +300 -0
- package/build/event/event-tools.js +482 -0
- package/build/index.js +250 -16
- package/build/nips/nips-tools.js +567 -0
- package/build/nips-tools.js +421 -0
- package/build/note/note-tools.js +4 -0
- package/build/note-tools.js +53 -0
- package/build/relay/relay-tools.js +164 -0
- package/build/social/social-tools.js +297 -0
- package/build/utils/constants.js +21 -1
- package/build/utils/conversion.js +10 -5
- package/build/utils/ephemeral-relay.js +100 -16
- package/build/utils/formatting.js +40 -0
- package/build/utils/index.js +1 -0
- package/build/utils/keys.js +26 -0
- package/build/zap/zap-tools.js +6 -0
- package/build/zap-tools.js +989 -0
- package/package.json +2 -2
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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
-
|
|
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
|
+
});
|