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 +72 -121
- package/dist/__tests__/crypto.test.d.ts +1 -0
- package/dist/__tests__/crypto.test.js +186 -0
- package/dist/__tests__/guards.test.d.ts +1 -0
- package/dist/__tests__/guards.test.js +80 -0
- package/dist/__tests__/integration.test.d.ts +1 -0
- package/dist/__tests__/integration.test.js +148 -0
- package/dist/__tests__/nip_compliance.test.d.ts +1 -0
- package/dist/__tests__/nip_compliance.test.js +121 -0
- package/dist/__tests__/transport.test.d.ts +1 -0
- package/dist/__tests__/transport.test.js +113 -0
- package/dist/__tests__/types.test.d.ts +1 -0
- package/dist/__tests__/types.test.js +227 -0
- package/dist/__tests__/validation.test.d.ts +1 -0
- package/dist/__tests__/validation.test.js +118 -0
- package/dist/constants.d.ts +99 -0
- package/dist/constants.js +93 -0
- package/dist/index.d.ts +24 -37
- package/dist/index.js +98 -59
- package/dist/integration.d.ts +77 -0
- package/dist/integration.js +192 -0
- package/dist/types/base.d.ts +64 -0
- package/dist/types/base.js +4 -0
- package/dist/types/guards.d.ts +26 -0
- package/dist/types/guards.js +89 -0
- package/dist/types/index.d.ts +6 -12
- package/dist/types/index.js +6 -1
- package/dist/types/protocol.d.ts +125 -0
- package/dist/types/protocol.js +56 -0
- package/dist/validation.d.ts +22 -0
- package/dist/validation.js +139 -0
- package/package.json +17 -8
package/README.md
CHANGED
|
@@ -1,101 +1,90 @@
|
|
|
1
1
|
# @humanjavaenterprises/nostr-crypto-utils
|
|
2
2
|
|
|
3
|
-
A comprehensive cryptographic utilities
|
|
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
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@humanjavaenterprises/nostr-crypto-utils)
|
|
6
|
+
[](http://www.typescriptlang.org/)
|
|
7
|
+
[](https://github.com/HumanjavaEnterprises/nostr-crypto-utils/blob/main/LICENSE)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
⚠️ **Important Security Notice**
|
|
8
10
|
|
|
9
|
-
|
|
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
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
|
25
|
+
npm install @humanjavaenterprises/nostr-crypto-utils
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
##
|
|
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,
|
|
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
|
-
|
|
37
|
+
console.log('Public Key:', keyPair.publicKey);
|
|
38
|
+
console.log('Private Key:', keyPair.privateKey);
|
|
65
39
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
40
|
+
// Get public key from private key
|
|
41
|
+
const pubKey = getPublicKey(privateKey);
|
|
42
|
+
```
|
|
69
43
|
|
|
70
|
-
|
|
71
|
-
const publicKey = await derivePublicKey(keyPair.privateKey);
|
|
44
|
+
### Event Operations
|
|
72
45
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
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
|
-
###
|
|
66
|
+
### Message Handling
|
|
80
67
|
|
|
81
68
|
```typescript
|
|
82
|
-
import {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
##
|
|
110
|
+
## Protocol Compliance
|
|
122
111
|
|
|
123
|
-
|
|
112
|
+
This library implements the following Nostr Implementation Possibilities (NIPs):
|
|
124
113
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
121
|
+
## Contributing
|
|
148
122
|
|
|
149
|
-
|
|
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
|
-
##
|
|
125
|
+
## Security
|
|
174
126
|
|
|
175
|
-
|
|
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
|
-
|
|
133
|
+
---
|
|
182
134
|
|
|
183
|
-
|
|
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
|
+
});
|