nostr-crypto-utils 0.1.4 → 0.1.6

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
@@ -1,101 +1,90 @@
1
1
  # @humanjavaenterprises/nostr-crypto-utils
2
2
 
3
- A comprehensive cryptographic utilities package for NOSTR applications, designed to work seamlessly with [@humanjavaenterprises/nostr-nsec-seedphrase](https://github.com/humanjavaenterprises/nostr-nsec-seedphrase).
3
+ A comprehensive TypeScript library providing cryptographic utilities and protocol-compliant message handling for Nostr applications, designed to work seamlessly with [@humanjavaenterprises/nostr-nsec-seedphrase](https://github.com/HumanjavaEnterprises/nostr-nsec-seedphrase).
4
4
 
5
- ⚠️ **Important Security Notice**
5
+ [![npm version](https://badge.fury.io/js/%40humanjavaenterprises%2Fnostr-crypto-utils.svg)](https://www.npmjs.com/package/@humanjavaenterprises/nostr-crypto-utils)
6
+ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
7
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/HumanjavaEnterprises/nostr-crypto-utils/blob/main/LICENSE)
6
8
 
7
- This library handles cryptographic keys and seed phrases that are critical for securing your Nostr identity and data. Just like Bitcoin, any seed phrase or private key (`nsec`) generated by this library must be stored with the utmost security and care.
9
+ ⚠️ **Important Security Notice**
8
10
 
9
- Developers using this library must inform their users about the critical nature of managing seed phrases, `nsec`, and hex keys. It is the user's responsibility to securely store and manage these keys. The library and its authors disclaim any responsibility or liability for lost keys, seed phrases, or data resulting from mismanagement.
11
+ This library handles cryptographic keys and operations that are critical for securing your Nostr identity and data. All cryptographic operations, including key generation, signing, and encryption, must be handled with appropriate security measures.
10
12
 
11
13
  ## Features
12
14
 
13
- - 🔑 Complete key pair management (generation, validation, public key derivation)
14
- - 📝 Event signing and verification
15
- - 🔒 NIP-04 encryption and decryption
16
- - 🌱 Seed phrase support via integration with nostr-nsec-seedphrase
17
- - 📦 Modern ESM package with full TypeScript support
18
- - ⚡️ Built on established crypto libraries (noble-curves, noble-hashes)
19
- - 🤝 Compatible with nostr-tools as a peer dependency
15
+ - **Complete NIP Compliance**: Implements all required cryptographic operations according to Nostr Implementation Possibilities (NIPs)
16
+ - **Type Safety**: Full TypeScript support with comprehensive type definitions
17
+ - **Event Handling**: Create, sign, and validate Nostr events
18
+ - **Message Formatting**: Protocol-compliant message formatting for relay communication
19
+ - **Encryption**: Secure encryption and decryption for direct messages (NIP-04)
20
+ - **Validation**: Comprehensive validation for events, filters, and subscriptions
20
21
 
21
22
  ## Installation
22
23
 
23
24
  ```bash
24
- npm install @humanjavaenterprises/nostr-crypto-utils @humanjavaenterprises/nostr-nsec-seedphrase nostr-tools
25
+ npm install @humanjavaenterprises/nostr-crypto-utils
25
26
  ```
26
27
 
27
- ## Architecture Overview
28
-
29
- This library serves as a crucial middleware layer in NOSTR applications:
30
-
31
- ```
32
- ┌─────────────────────────────────────────────────────┐
33
- │ Your NOSTR App │
34
- └───────────────────────┬─────────────────────────────┘
35
-
36
- ┌───────────────────────▼─────────────────────────────┐
37
- │ @humanjavaenterprises/nostr-crypto-utils │
38
- │ │
39
- │ ┌─────────────────┐ ┌──────────────────┐ │
40
- │ │ Key Manager │ │ Event Handler │ │
41
- │ └────────┬────────┘ └────────┬─────────┘ │
42
- │ │ │ │
43
- │ ┌────────▼────────┐ ┌───────▼─────────┐ │
44
- │ │ nostr-nsec- │ │ nostr-tools │ │
45
- │ │ seedphrase │ │ │ │
46
- │ └─────────────────┘ └─────────────────┘ │
47
- └─────────────────────────────────────────────────────┘
48
-
49
- ┌───────────────────────▼─────────────────────────────┐
50
- │ NOSTR Protocol / Relays │
51
- └─────────────────────────────────────────────────────┘
52
- ```
53
-
54
- ## Usage Examples
28
+ ## Usage
55
29
 
56
30
  ### Key Management
57
31
 
58
32
  ```typescript
59
- import { generateKeyPair, derivePublicKey, validateKeyPair } from '@humanjavaenterprises/nostr-crypto-utils';
33
+ import { generateKeyPair, getPublicKey } from '@humanjavaenterprises/nostr-crypto-utils';
60
34
 
61
35
  // Generate a new key pair
62
36
  const keyPair = await generateKeyPair();
63
- console.log(keyPair);
64
- // { privateKey: '...', publicKey: '...' }
37
+ console.log('Public Key:', keyPair.publicKey);
38
+ console.log('Private Key:', keyPair.privateKey);
65
39
 
66
- // Generate from seed phrase
67
- const seedPhrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
68
- const keyPairFromSeed = await generateKeyPair(seedPhrase);
40
+ // Get public key from private key
41
+ const pubKey = getPublicKey(privateKey);
42
+ ```
69
43
 
70
- // Derive public key from private key
71
- const publicKey = await derivePublicKey(keyPair.privateKey);
44
+ ### Event Operations
72
45
 
73
- // Validate a key pair
74
- const validation = await validateKeyPair(keyPair.publicKey, keyPair.privateKey);
75
- console.log(validation);
76
- // { isValid: true, error: undefined }
46
+ ```typescript
47
+ import { createEvent, signEvent, validateEvent } from '@humanjavaenterprises/nostr-crypto-utils';
48
+
49
+ // Create and sign an event
50
+ const event = createEvent({
51
+ kind: NostrEventKind.TEXT_NOTE,
52
+ content: 'Hello Nostr!',
53
+ tags: []
54
+ });
55
+ const signedEvent = await signEvent(event, privateKey);
56
+
57
+ // Validate an event
58
+ const validation = validateEvent(event);
59
+ if (validation.isValid) {
60
+ console.log('Event is valid');
61
+ } else {
62
+ console.log('Validation errors:', validation.errors);
63
+ }
77
64
  ```
78
65
 
79
- ### Event Operations
66
+ ### Message Handling
80
67
 
81
68
  ```typescript
82
- import { signEvent, verifySignature } from '@humanjavaenterprises/nostr-crypto-utils';
83
-
84
- // Create and sign a NOSTR event
85
- const event = {
86
- kind: 1,
87
- created_at: Math.floor(Date.now() / 1000),
88
- tags: [],
89
- content: 'Hello NOSTR!'
69
+ import {
70
+ formatEventForRelay,
71
+ formatSubscriptionForRelay,
72
+ parseNostrMessage
73
+ } from '@humanjavaenterprises/nostr-crypto-utils';
74
+
75
+ // Format event for relay
76
+ const eventMessage = formatEventForRelay(signedEvent);
77
+
78
+ // Format subscription request
79
+ const subscription = {
80
+ id: 'sub1',
81
+ filters: [{ kinds: [1], limit: 10 }]
90
82
  };
83
+ const subMessage = formatSubscriptionForRelay(subscription);
91
84
 
92
- const signedEvent = await signEvent(event, keyPair.privateKey);
93
- console.log(signedEvent);
94
- // { id: '...', pubkey: '...', sig: '...', ...event }
95
-
96
- // Verify an event signature
97
- const isValid = await verifySignature(signedEvent);
98
- console.log(isValid); // true
85
+ // Parse incoming messages
86
+ const message = ['EVENT', signedEvent];
87
+ const parsed = parseNostrMessage(message);
99
88
  ```
100
89
 
101
90
  ### Encryption (NIP-04)
@@ -118,67 +107,29 @@ const decrypted = await decrypt(
118
107
  );
119
108
  ```
120
109
 
121
- ## Integration Examples
110
+ ## Protocol Compliance
122
111
 
123
- ### Authentication Flow
112
+ This library implements the following Nostr Implementation Possibilities (NIPs):
124
113
 
125
- ```typescript
126
- import { generateKeyPair, signEvent } from '@humanjavaenterprises/nostr-crypto-utils';
127
-
128
- async function authenticateUser(seedPhrase?: string) {
129
- // Generate or recover keys
130
- const keyPair = await generateKeyPair(seedPhrase);
131
-
132
- // Create auth event
133
- const authEvent = {
134
- kind: 22242,
135
- created_at: Math.floor(Date.now() / 1000),
136
- tags: [['challenge', 'authentication-challenge']],
137
- content: 'Authenticating...'
138
- };
139
-
140
- // Sign the event
141
- const signedAuthEvent = await signEvent(authEvent, keyPair.privateKey);
142
-
143
- return signedAuthEvent;
144
- }
145
- ```
114
+ - NIP-01: Basic protocol flow description
115
+ - NIP-02: Contact List and Petnames
116
+ - NIP-04: Encrypted Direct Message
117
+ - NIP-09: Event Deletion
118
+ - NIP-25: Reactions
119
+ - NIP-28: Public Chat Channels
146
120
 
147
- ### Secure Messaging
121
+ ## Contributing
148
122
 
149
- ```typescript
150
- import { generateKeyPair, encrypt, decrypt } from '@humanjavaenterprises/nostr-crypto-utils';
151
-
152
- async function secureMessaging() {
153
- // Generate keys for both parties
154
- const alice = await generateKeyPair();
155
- const bob = await generateKeyPair();
156
-
157
- // Alice encrypts a message for Bob
158
- const encrypted = await encrypt(
159
- 'Hey Bob!',
160
- bob.publicKey,
161
- alice.privateKey
162
- );
163
-
164
- // Bob decrypts Alice's message
165
- const decrypted = await decrypt(
166
- encrypted,
167
- alice.publicKey,
168
- bob.privateKey
169
- );
170
- }
171
- ```
123
+ Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
172
124
 
173
- ## Contributing
125
+ ## Security
174
126
 
175
- Contributions are welcome! Please feel free to submit a Pull Request.
127
+ If you discover a security vulnerability within this library, please send an e-mail to security@humanjavaenterprises.com. All security vulnerabilities will be promptly addressed.
176
128
 
177
129
  ## License
178
130
 
179
- MIT
131
+ This project is licensed under the MIT License - see the [LICENSE](https://github.com/HumanjavaEnterprises/nostr-crypto-utils/blob/main/LICENSE) file for details.
180
132
 
181
- ## Related Projects
133
+ ---
182
134
 
183
- - [@humanjavaenterprises/nostr-nsec-seedphrase](https://github.com/humanjavaenterprises/nostr-nsec-seedphrase) - Seed phrase management for NOSTR
184
- - [nostr-tools](https://github.com/nbd-wtf/nostr-tools) - Core NOSTR functionality
135
+ Built with ❤️ by [Human Java Enterprises](https://github.com/HumanjavaEnterprises)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,186 @@
1
+ /// <reference types="jest" />
2
+ import { generateKeyPair, getPublicKey, validateKeyPair, signEvent, verifySignature, encrypt, decrypt } from '../index';
3
+ describe('NOSTR Crypto Utils', () => {
4
+ describe('Key Management', () => {
5
+ it('should generate valid key pairs', async () => {
6
+ const keyPair = await generateKeyPair();
7
+ expect(keyPair.privateKey).toBeDefined();
8
+ expect(keyPair.publicKey).toBeDefined();
9
+ expect(keyPair.privateKey).toHaveLength(64);
10
+ expect(keyPair.publicKey).toHaveLength(64);
11
+ });
12
+ it('should derive the correct public key', async () => {
13
+ const keyPair = await generateKeyPair();
14
+ const derivedPubKey = await getPublicKey(keyPair.privateKey);
15
+ expect(derivedPubKey).toBe(keyPair.publicKey);
16
+ });
17
+ it('should validate key pairs', async () => {
18
+ const keyPair = await generateKeyPair();
19
+ const result = await validateKeyPair(keyPair.publicKey, keyPair.privateKey);
20
+ expect(result.isValid).toBe(true);
21
+ expect(result.error).toBeUndefined();
22
+ });
23
+ it('should generate consistent key pairs from seed phrase', async () => {
24
+ const seedPhrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
25
+ const keyPair1 = await generateKeyPair(seedPhrase);
26
+ const keyPair2 = await generateKeyPair(seedPhrase);
27
+ expect(keyPair1.privateKey).toBe(keyPair2.privateKey);
28
+ expect(keyPair1.publicKey).toBe(keyPair2.publicKey);
29
+ });
30
+ });
31
+ describe('Event Operations', () => {
32
+ it('should sign and verify events', async () => {
33
+ const keyPair = await generateKeyPair();
34
+ const event = {
35
+ kind: 1,
36
+ created_at: Math.floor(Date.now() / 1000),
37
+ tags: [],
38
+ content: 'Hello NOSTR!'
39
+ };
40
+ const signedEvent = await signEvent(event, keyPair.privateKey);
41
+ expect(signedEvent.sig).toBeDefined();
42
+ expect(signedEvent.id).toBeDefined();
43
+ expect(signedEvent.pubkey).toBe(keyPair.publicKey);
44
+ const isValid = await verifySignature(signedEvent);
45
+ expect(isValid).toBe(true);
46
+ });
47
+ });
48
+ describe('Encryption/Decryption', () => {
49
+ it('should encrypt and decrypt messages', async () => {
50
+ const alice = await generateKeyPair();
51
+ const bob = await generateKeyPair();
52
+ const message = 'Secret message for Bob';
53
+ const encrypted = await encrypt(message, bob.publicKey, alice.privateKey);
54
+ const decrypted = await decrypt(encrypted, alice.publicKey, bob.privateKey);
55
+ expect(decrypted).toBe(message);
56
+ });
57
+ });
58
+ describe('verifySignature', () => {
59
+ it('should verify a valid signature', async () => {
60
+ const event = {
61
+ kind: 1,
62
+ created_at: Math.floor(Date.now() / 1000),
63
+ tags: [],
64
+ content: 'Hello, World!'
65
+ };
66
+ const privateKey = await generateKeyPair().privateKey;
67
+ const signedEvent = await signEvent(event, privateKey);
68
+ expect(await verifySignature(signedEvent)).toBe(true);
69
+ });
70
+ it('should reject an invalid signature', async () => {
71
+ const event = {
72
+ kind: 1,
73
+ created_at: Math.floor(Date.now() / 1000),
74
+ tags: [],
75
+ content: 'Hello, World!'
76
+ };
77
+ const privateKey = await generateKeyPair().privateKey;
78
+ const signedEvent = await signEvent(event, privateKey);
79
+ // Tamper with the signature
80
+ signedEvent.sig = signedEvent.sig.replace('a', 'b');
81
+ expect(await verifySignature(signedEvent)).toBe(false);
82
+ });
83
+ it('should reject if event hash does not match', async () => {
84
+ const event = {
85
+ kind: 1,
86
+ created_at: Math.floor(Date.now() / 1000),
87
+ tags: [],
88
+ content: 'Hello, World!'
89
+ };
90
+ const privateKey = await generateKeyPair().privateKey;
91
+ const signedEvent = await signEvent(event, privateKey);
92
+ // Tamper with the content which affects the hash
93
+ signedEvent.content = 'Modified content';
94
+ expect(await verifySignature(signedEvent)).toBe(false);
95
+ });
96
+ it('should handle invalid hex in signature', async () => {
97
+ const event = {
98
+ kind: 1,
99
+ created_at: Math.floor(Date.now() / 1000),
100
+ tags: [],
101
+ content: 'Hello, World!'
102
+ };
103
+ const privateKey = await generateKeyPair().privateKey;
104
+ const signedEvent = await signEvent(event, privateKey);
105
+ // Add invalid hex character
106
+ signedEvent.sig = 'XYZ' + signedEvent.sig.slice(3);
107
+ expect(await verifySignature(signedEvent)).toBe(false);
108
+ });
109
+ });
110
+ describe('validateKeyPair', () => {
111
+ it('should validate a correct key pair', async () => {
112
+ const privateKey = await generateKeyPair().privateKey;
113
+ const publicKey = await getPublicKey(privateKey);
114
+ const result = await validateKeyPair(publicKey, privateKey);
115
+ expect(result.isValid).toBe(true);
116
+ expect(result.error).toBeUndefined();
117
+ });
118
+ it('should reject mismatched key pair', async () => {
119
+ const privateKey1 = await generateKeyPair().privateKey;
120
+ const privateKey2 = await generateKeyPair().privateKey;
121
+ const publicKey = await getPublicKey(privateKey1);
122
+ const result = await validateKeyPair(publicKey, privateKey2);
123
+ expect(result.isValid).toBe(false);
124
+ expect(result.error).toBe('Public key does not match private key');
125
+ });
126
+ it('should handle invalid private key', async () => {
127
+ const publicKey = await getPublicKey(await generateKeyPair().privateKey);
128
+ const result = await validateKeyPair(publicKey, 'invalid-private-key');
129
+ expect(result.isValid).toBe(false);
130
+ expect(result.error).toBe('Invalid key pair');
131
+ });
132
+ });
133
+ describe('signEvent', () => {
134
+ it('should sign an event with all fields', async () => {
135
+ const event = {
136
+ kind: 1,
137
+ created_at: Math.floor(Date.now() / 1000),
138
+ tags: [['p', '1234']],
139
+ content: 'Hello, World!'
140
+ };
141
+ const privateKey = await generateKeyPair().privateKey;
142
+ const signedEvent = await signEvent(event, privateKey);
143
+ expect(signedEvent.sig).toBeTruthy();
144
+ expect(signedEvent.id).toBeTruthy();
145
+ expect(signedEvent.pubkey).toBeTruthy();
146
+ });
147
+ it('should handle missing optional fields', async () => {
148
+ const event = {
149
+ kind: 1,
150
+ created_at: 1734127200, // 2024-12-14 18:14:51 PST
151
+ content: 'Hello, World!',
152
+ tags: [] // Adding the required tags property
153
+ };
154
+ const privateKey = await generateKeyPair().privateKey;
155
+ const signedEvent = await signEvent(event, privateKey);
156
+ expect(signedEvent.sig).toBeTruthy();
157
+ expect(signedEvent.id).toBeTruthy();
158
+ expect(signedEvent.pubkey).toBeTruthy();
159
+ expect(signedEvent.created_at).toBeDefined();
160
+ expect(signedEvent.tags).toEqual([]);
161
+ });
162
+ it('should handle undefined tags', async () => {
163
+ const event = {
164
+ kind: 1,
165
+ created_at: Math.floor(Date.now() / 1000),
166
+ content: 'Hello, World!',
167
+ tags: [] // Add empty tags array to satisfy NostrEvent interface
168
+ };
169
+ const privateKey = await generateKeyPair().privateKey;
170
+ const signedEvent = await signEvent(event, privateKey);
171
+ expect(signedEvent.tags).toEqual([]);
172
+ });
173
+ it('should handle undefined created_at', async () => {
174
+ const event = {
175
+ kind: 1,
176
+ content: 'Hello, World!',
177
+ tags: [],
178
+ created_at: Math.floor(Date.now() / 1000)
179
+ };
180
+ const privateKey = await generateKeyPair().privateKey;
181
+ const signedEvent = await signEvent(event, privateKey);
182
+ expect(signedEvent.created_at).toBeDefined();
183
+ expect(typeof signedEvent.created_at).toBe('number');
184
+ });
185
+ });
186
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ import { isNostrEvent, isSignedNostrEvent } from '../types/guards';
2
+ describe('Type Guards', () => {
3
+ describe('isNostrEvent', () => {
4
+ it('should return true for valid NostrEvent with all fields', () => {
5
+ const event = {
6
+ kind: 1,
7
+ created_at: Math.floor(Date.now() / 1000),
8
+ content: 'Hello, Nostr!',
9
+ tags: [['p', '1234']],
10
+ pubkey: '0123456789abcdef'
11
+ };
12
+ expect(isNostrEvent(event)).toBe(true);
13
+ });
14
+ it('should return true for valid NostrEvent with only required fields', () => {
15
+ const event = {
16
+ kind: 1,
17
+ content: 'Minimal event',
18
+ created_at: Math.floor(Date.now() / 1000),
19
+ tags: []
20
+ };
21
+ expect(isNostrEvent(event)).toBe(true);
22
+ });
23
+ it('should return true for valid NostrEvent with empty tags', () => {
24
+ const event = {
25
+ kind: 1,
26
+ content: 'Empty tags',
27
+ created_at: Math.floor(Date.now() / 1000),
28
+ tags: []
29
+ };
30
+ expect(isNostrEvent(event)).toBe(true);
31
+ });
32
+ it('should return false for invalid NostrEvent missing required fields', () => {
33
+ const event = {
34
+ created_at: Math.floor(Date.now() / 1000),
35
+ content: 'Missing kind'
36
+ };
37
+ expect(isNostrEvent(event)).toBe(false);
38
+ });
39
+ it('should return false for invalid NostrEvent with wrong field types', () => {
40
+ const event = {
41
+ kind: '1', // should be number
42
+ content: 123, // should be string
43
+ tags: ['not an array of arrays']
44
+ };
45
+ expect(isNostrEvent(event)).toBe(false);
46
+ });
47
+ });
48
+ describe('isSignedNostrEvent', () => {
49
+ it('should return true for valid signed NostrEvent', () => {
50
+ const event = {
51
+ kind: 1,
52
+ created_at: Math.floor(Date.now() / 1000),
53
+ content: 'Signed event',
54
+ tags: [],
55
+ pubkey: '0123456789abcdef',
56
+ id: '0123456789abcdef',
57
+ sig: '0123456789abcdef'
58
+ };
59
+ expect(isSignedNostrEvent(event)).toBe(true);
60
+ });
61
+ it('should return false for unsigned NostrEvent', () => {
62
+ const event = {
63
+ kind: 1,
64
+ created_at: Math.floor(Date.now() / 1000),
65
+ content: 'Unsigned event',
66
+ tags: [],
67
+ pubkey: '0123456789abcdef'
68
+ };
69
+ expect(isSignedNostrEvent(event)).toBe(false);
70
+ });
71
+ it('should return false for signed event with missing required fields', () => {
72
+ const event = {
73
+ content: 'Missing fields',
74
+ id: '0123456789abcdef',
75
+ sig: '0123456789abcdef'
76
+ };
77
+ expect(isSignedNostrEvent(event)).toBe(false);
78
+ });
79
+ });
80
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,148 @@
1
+ import { formatEventForRelay, formatSubscriptionForRelay, formatCloseForRelay, formatAuthForRelay, parseNostrMessage, createMetadataEvent, createTextNoteEvent, createDirectMessageEvent, createChannelMessageEvent, extractReferencedEvents, extractMentionedPubkeys, createKindFilter, createAuthorFilter, createReplyFilter } from '../integration';
2
+ import { NOSTR_KIND } from '../constants';
3
+ describe('Integration Utilities', () => {
4
+ const mockSignedEvent = {
5
+ id: '123',
6
+ pubkey: '456',
7
+ created_at: 1234567890,
8
+ kind: 1,
9
+ tags: [],
10
+ content: 'test',
11
+ sig: '789'
12
+ };
13
+ describe('Message Formatting', () => {
14
+ it('should format event message', () => {
15
+ const formatted = formatEventForRelay(mockSignedEvent);
16
+ expect(formatted).toEqual(['EVENT', mockSignedEvent]);
17
+ });
18
+ it('should format subscription message', () => {
19
+ const subscription = {
20
+ id: 'sub1',
21
+ filters: [{ kinds: [1], limit: 10 }]
22
+ };
23
+ const formatted = formatSubscriptionForRelay(subscription);
24
+ expect(formatted).toEqual(['REQ', 'sub1', { kinds: [1], limit: 10 }]);
25
+ });
26
+ it('should format close message', () => {
27
+ const formatted = formatCloseForRelay('sub1');
28
+ expect(formatted).toEqual(['CLOSE', 'sub1']);
29
+ });
30
+ it('should format auth message', () => {
31
+ const formatted = formatAuthForRelay(mockSignedEvent);
32
+ expect(formatted).toEqual(['AUTH', mockSignedEvent]);
33
+ });
34
+ });
35
+ describe('Message Parsing', () => {
36
+ it('should parse EVENT message', () => {
37
+ const message = ['EVENT', mockSignedEvent];
38
+ const parsed = parseNostrMessage(message);
39
+ expect(parsed.type).toBe('EVENT');
40
+ expect(parsed.payload).toEqual(mockSignedEvent);
41
+ });
42
+ it('should parse NOTICE message', () => {
43
+ const message = ['NOTICE', 'test message'];
44
+ const parsed = parseNostrMessage(message);
45
+ expect(parsed.type).toBe('NOTICE');
46
+ expect(parsed.payload).toBe('test message');
47
+ });
48
+ it('should parse OK message', () => {
49
+ const message = ['OK', 'event1', true, 'success'];
50
+ const parsed = parseNostrMessage(message);
51
+ expect(parsed.type).toBe('OK');
52
+ expect(parsed.payload).toEqual(['event1', true, 'success']);
53
+ });
54
+ it('should throw error for invalid message', () => {
55
+ expect(() => parseNostrMessage('invalid')).toThrow('Invalid relay message: not an array');
56
+ expect(() => parseNostrMessage([123])).toThrow('Invalid relay message: type must be a string');
57
+ expect(() => parseNostrMessage(['UNKNOWN'])).toThrow('Unknown message type: UNKNOWN');
58
+ });
59
+ });
60
+ describe('Event Creation', () => {
61
+ const now = Math.floor(Date.now() / 1000);
62
+ it('should create metadata event', () => {
63
+ const metadata = { name: 'test', about: 'test user' };
64
+ const event = createMetadataEvent(metadata);
65
+ expect(event.kind).toBe(NOSTR_KIND.METADATA);
66
+ expect(JSON.parse(event.content)).toEqual(metadata);
67
+ expect(event.created_at).toBeGreaterThanOrEqual(now);
68
+ expect(event.tags).toEqual([]);
69
+ });
70
+ it('should create text note event', () => {
71
+ const content = 'Hello, Nostr!';
72
+ const replyTo = 'event123';
73
+ const mentions = ['pubkey123'];
74
+ const event = createTextNoteEvent(content, replyTo, mentions);
75
+ expect(event.kind).toBe(NOSTR_KIND.TEXT_NOTE);
76
+ expect(event.content).toBe(content);
77
+ expect(event.created_at).toBeGreaterThanOrEqual(now);
78
+ expect(event.tags).toHaveLength(2);
79
+ expect(event.tags).toContainEqual(['e', replyTo]);
80
+ expect(event.tags).toContainEqual(['p', mentions[0]]);
81
+ });
82
+ it('should create direct message event', () => {
83
+ const recipientPubkey = 'pubkey123';
84
+ const content = 'Secret message';
85
+ const event = createDirectMessageEvent(recipientPubkey, content);
86
+ expect(event.kind).toBe(NOSTR_KIND.ENCRYPTED_DIRECT_MESSAGE);
87
+ expect(event.content).toBe(content);
88
+ expect(event.created_at).toBeGreaterThanOrEqual(now);
89
+ expect(event.tags).toHaveLength(1);
90
+ expect(event.tags[0]).toEqual(['p', recipientPubkey]);
91
+ });
92
+ it('should create channel message event', () => {
93
+ const channelId = 'channel123';
94
+ const content = 'Channel message';
95
+ const replyTo = 'event123';
96
+ const event = createChannelMessageEvent(channelId, content, replyTo);
97
+ expect(event.kind).toBe(NOSTR_KIND.CHANNEL_MESSAGE);
98
+ expect(event.content).toBe(content);
99
+ expect(event.created_at).toBeGreaterThanOrEqual(now);
100
+ expect(event.tags).toHaveLength(2);
101
+ expect(event.tags).toContainEqual(['e', channelId, '', 'root']);
102
+ expect(event.tags).toContainEqual(['e', replyTo, '', 'reply']);
103
+ });
104
+ });
105
+ describe('Event Analysis', () => {
106
+ const mockEvent = {
107
+ kind: 1,
108
+ content: 'test',
109
+ created_at: 1234567890,
110
+ tags: [
111
+ ['e', 'event1'],
112
+ ['e', 'event2'],
113
+ ['p', 'pubkey1'],
114
+ ['p', 'pubkey2']
115
+ ]
116
+ };
117
+ it('should extract referenced events', () => {
118
+ const refs = extractReferencedEvents(mockEvent);
119
+ expect(refs).toEqual(['event1', 'event2']);
120
+ });
121
+ it('should extract mentioned pubkeys', () => {
122
+ const mentions = extractMentionedPubkeys(mockEvent);
123
+ expect(mentions).toEqual(['pubkey1', 'pubkey2']);
124
+ });
125
+ });
126
+ describe('Filter Creation', () => {
127
+ it('should create kind filter', () => {
128
+ const filter = createKindFilter(1, 10);
129
+ expect(filter).toEqual({ kinds: [1], limit: 10 });
130
+ });
131
+ it('should create author filter', () => {
132
+ const filter = createAuthorFilter('pubkey123', [1, 2], 10);
133
+ expect(filter).toEqual({
134
+ authors: ['pubkey123'],
135
+ kinds: [1, 2],
136
+ limit: 10
137
+ });
138
+ });
139
+ it('should create reply filter', () => {
140
+ const filter = createReplyFilter('event123', 10);
141
+ expect(filter).toEqual({
142
+ '#e': ['event123'],
143
+ kinds: [NOSTR_KIND.TEXT_NOTE, NOSTR_KIND.CHANNEL_MESSAGE],
144
+ limit: 10
145
+ });
146
+ });
147
+ });
148
+ });