nostr-crypto-utils 0.1.5 → 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 -39
- package/dist/index.js +38 -39
- 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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { generateKeyPair, signEvent, encrypt, decrypt, createEvent, validateEvent, validateSignedEvent, NostrEventKind, formatSubscriptionForRelay, formatEventForRelay, formatCloseForRelay } from '../index';
|
|
2
|
+
describe('NIP-01: Basic Protocol Flow', () => {
|
|
3
|
+
let keyPair;
|
|
4
|
+
beforeAll(async () => {
|
|
5
|
+
keyPair = await generateKeyPair();
|
|
6
|
+
});
|
|
7
|
+
test('Event Creation and Signing Flow', async () => {
|
|
8
|
+
// Create a text note (kind 1) as specified in NIP-01
|
|
9
|
+
const event = createEvent({
|
|
10
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
11
|
+
content: 'Hello Nostr!',
|
|
12
|
+
tags: [],
|
|
13
|
+
created_at: Math.floor(Date.now() / 1000)
|
|
14
|
+
});
|
|
15
|
+
// Sign the event
|
|
16
|
+
const signedEvent = await signEvent(event, keyPair.privateKey);
|
|
17
|
+
// Verify event structure according to NIP-01
|
|
18
|
+
const validation = validateSignedEvent(signedEvent);
|
|
19
|
+
expect(validation.isValid).toBe(true);
|
|
20
|
+
expect(signedEvent.pubkey).toBe(keyPair.publicKey);
|
|
21
|
+
expect(typeof signedEvent.created_at).toBe('number');
|
|
22
|
+
expect(Array.isArray(signedEvent.tags)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
test('Client-Relay Message Format', async () => {
|
|
25
|
+
// Test REQ message format
|
|
26
|
+
const reqMessage = formatSubscriptionForRelay({ id: 'test_sub', filters: [{ kinds: [1], limit: 10 }] });
|
|
27
|
+
expect(reqMessage[0]).toBe('REQ');
|
|
28
|
+
expect(Array.isArray(reqMessage)).toBe(true);
|
|
29
|
+
expect(reqMessage.length).toBeGreaterThan(2);
|
|
30
|
+
// Test EVENT message format
|
|
31
|
+
const event2 = createEvent({
|
|
32
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
33
|
+
content: 'test message',
|
|
34
|
+
tags: []
|
|
35
|
+
});
|
|
36
|
+
const signedEvent = await signEvent(event2, keyPair.privateKey);
|
|
37
|
+
const eventMessage = formatEventForRelay(signedEvent);
|
|
38
|
+
expect(eventMessage[0]).toBe('EVENT');
|
|
39
|
+
expect(eventMessage[1]).toMatchObject({
|
|
40
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
41
|
+
content: 'test message'
|
|
42
|
+
});
|
|
43
|
+
// Test CLOSE message format
|
|
44
|
+
const closeMessage = JSON.stringify(formatCloseForRelay('subscription_id'));
|
|
45
|
+
const closeParsed = JSON.parse(closeMessage);
|
|
46
|
+
expect(closeParsed[0]).toBe('CLOSE');
|
|
47
|
+
expect(closeParsed[1]).toBe('subscription_id');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('NIP-02: Contact List', () => {
|
|
51
|
+
test('Contact List Event Structure', async () => {
|
|
52
|
+
const keyPair = await generateKeyPair();
|
|
53
|
+
const contacts = createEvent({
|
|
54
|
+
kind: NostrEventKind.CONTACTS,
|
|
55
|
+
content: JSON.stringify([
|
|
56
|
+
{
|
|
57
|
+
pubkey: '0123456789abcdef',
|
|
58
|
+
relay: 'wss://relay.example.com',
|
|
59
|
+
petname: 'friend'
|
|
60
|
+
}
|
|
61
|
+
]),
|
|
62
|
+
tags: [],
|
|
63
|
+
created_at: Math.floor(Date.now() / 1000)
|
|
64
|
+
});
|
|
65
|
+
const validation = validateEvent(contacts);
|
|
66
|
+
expect(validation.isValid).toBe(true);
|
|
67
|
+
expect(contacts.kind).toBe(NostrEventKind.CONTACTS);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('NIP-04: Encrypted Direct Messages', () => {
|
|
71
|
+
test('Message Encryption and Decryption', async () => {
|
|
72
|
+
const alice = await generateKeyPair();
|
|
73
|
+
const bob = await generateKeyPair();
|
|
74
|
+
const message = 'Secret message for testing';
|
|
75
|
+
// Encrypt message from Alice to Bob
|
|
76
|
+
const encrypted = await encrypt(message, bob.publicKey, alice.privateKey);
|
|
77
|
+
expect(encrypted).not.toBe(message);
|
|
78
|
+
// Bob decrypts message from Alice
|
|
79
|
+
const decrypted = await decrypt(encrypted, alice.publicKey, bob.privateKey);
|
|
80
|
+
expect(decrypted).toBe(message);
|
|
81
|
+
});
|
|
82
|
+
test('Encrypted DM Event Structure', async () => {
|
|
83
|
+
const sender = await generateKeyPair();
|
|
84
|
+
const recipient = await generateKeyPair();
|
|
85
|
+
const message = 'Secret message';
|
|
86
|
+
const encrypted = await encrypt(message, recipient.publicKey, sender.privateKey);
|
|
87
|
+
const dmEvent = createEvent({
|
|
88
|
+
kind: NostrEventKind.ENCRYPTED_DIRECT_MESSAGE,
|
|
89
|
+
content: encrypted,
|
|
90
|
+
tags: [['p', recipient.publicKey]],
|
|
91
|
+
created_at: Math.floor(Date.now() / 1000)
|
|
92
|
+
});
|
|
93
|
+
const validation = validateEvent(dmEvent);
|
|
94
|
+
expect(validation.isValid).toBe(true);
|
|
95
|
+
expect(dmEvent.kind).toBe(NostrEventKind.ENCRYPTED_DIRECT_MESSAGE);
|
|
96
|
+
expect(dmEvent.tags[0][0]).toBe('p');
|
|
97
|
+
expect(dmEvent.tags[0][1]).toBe(recipient.publicKey);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('NIP-09: Event Deletion', () => {
|
|
101
|
+
test('Event Deletion Structure', async () => {
|
|
102
|
+
const keyPair = await generateKeyPair();
|
|
103
|
+
const originalEvent = await signEvent(createEvent({
|
|
104
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
105
|
+
content: 'Original post',
|
|
106
|
+
tags: [],
|
|
107
|
+
created_at: Math.floor(Date.now() / 1000)
|
|
108
|
+
}), keyPair.privateKey);
|
|
109
|
+
const deleteEvent = createEvent({
|
|
110
|
+
kind: NostrEventKind.DELETE,
|
|
111
|
+
content: 'Deleted due to error',
|
|
112
|
+
tags: [['e', originalEvent.id]],
|
|
113
|
+
created_at: Math.floor(Date.now() / 1000)
|
|
114
|
+
});
|
|
115
|
+
const validation = validateEvent(deleteEvent);
|
|
116
|
+
expect(validation.isValid).toBe(true);
|
|
117
|
+
expect(deleteEvent.kind).toBe(NostrEventKind.DELETE);
|
|
118
|
+
expect(deleteEvent.tags[0][0]).toBe('e');
|
|
119
|
+
expect(deleteEvent.tags[0][1]).toBe(originalEvent.id);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { parseNostrMessage, NostrEventKind, createEvent, signEvent, generateKeyPair, validateSignedEvent, formatEventForRelay } from '../index';
|
|
2
|
+
// Mock WebSocket for testing
|
|
3
|
+
class MockWebSocket {
|
|
4
|
+
constructor(url) {
|
|
5
|
+
this.url = url;
|
|
6
|
+
this.listeners = {};
|
|
7
|
+
this.readyState = 1;
|
|
8
|
+
}
|
|
9
|
+
send(data) {
|
|
10
|
+
// Simulate relay behavior
|
|
11
|
+
const message = JSON.parse(data);
|
|
12
|
+
if (message[0] === 'REQ') {
|
|
13
|
+
// Simulate EOSE after subscription
|
|
14
|
+
this.emit('message', JSON.stringify(['EOSE', message[1]]));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
addEventListener(event, callback) {
|
|
18
|
+
if (!this.listeners[event]) {
|
|
19
|
+
this.listeners[event] = [];
|
|
20
|
+
}
|
|
21
|
+
this.listeners[event].push(callback);
|
|
22
|
+
}
|
|
23
|
+
emit(event, data) {
|
|
24
|
+
if (this.listeners[event]) {
|
|
25
|
+
this.listeners[event].forEach(callback => {
|
|
26
|
+
callback({ data });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
close() {
|
|
31
|
+
this.readyState = 3;
|
|
32
|
+
this.emit('close', null);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
describe('Transport Layer Integration', () => {
|
|
36
|
+
let keyPair;
|
|
37
|
+
let mockRelay;
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
keyPair = await generateKeyPair();
|
|
40
|
+
mockRelay = new MockWebSocket('wss://mock.relay');
|
|
41
|
+
});
|
|
42
|
+
test('Subscription Flow', done => {
|
|
43
|
+
// Create subscription request
|
|
44
|
+
const subId = 'test-sub';
|
|
45
|
+
const filter = { kinds: [1], limit: 10 };
|
|
46
|
+
const subMessage = JSON.stringify(['REQ', subId, filter]);
|
|
47
|
+
// Listen for EOSE
|
|
48
|
+
mockRelay.addEventListener('message', event => {
|
|
49
|
+
const message = JSON.parse(event.data);
|
|
50
|
+
if (message[0] === 'EOSE' && message[1] === subId) {
|
|
51
|
+
done();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// Send subscription
|
|
55
|
+
mockRelay.send(subMessage);
|
|
56
|
+
});
|
|
57
|
+
test('Event Publication Flow', async () => {
|
|
58
|
+
// Create and sign an event
|
|
59
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
60
|
+
const event = createEvent({
|
|
61
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
62
|
+
content: 'Test message',
|
|
63
|
+
tags: [],
|
|
64
|
+
created_at: timestamp,
|
|
65
|
+
pubkey: keyPair.publicKey
|
|
66
|
+
});
|
|
67
|
+
// Sign the event
|
|
68
|
+
const signedEvent = await signEvent(event, keyPair.privateKey);
|
|
69
|
+
// Validate event before sending
|
|
70
|
+
const validation = await validateSignedEvent(signedEvent);
|
|
71
|
+
expect(validation.isValid).toBe(true);
|
|
72
|
+
// Format event message
|
|
73
|
+
const [messageType, eventPayload] = formatEventForRelay(signedEvent);
|
|
74
|
+
expect(messageType).toBe('EVENT');
|
|
75
|
+
// Simulate sending to relay
|
|
76
|
+
mockRelay.send(JSON.stringify([messageType, eventPayload]));
|
|
77
|
+
});
|
|
78
|
+
test('Message Parsing', () => {
|
|
79
|
+
// Test EVENT message parsing
|
|
80
|
+
const eventMessage = ['EVENT', { id: 'test_id', pubkey: 'test_pubkey', kind: 1, content: 'test', created_at: 123, sig: 'test_sig', tags: [] }];
|
|
81
|
+
const eventResponse = parseNostrMessage(eventMessage);
|
|
82
|
+
expect(eventResponse.type).toBe('EVENT');
|
|
83
|
+
if (eventResponse.type === 'EVENT') {
|
|
84
|
+
expect(eventResponse.payload).toMatchObject({
|
|
85
|
+
id: 'test_id',
|
|
86
|
+
pubkey: 'test_pubkey',
|
|
87
|
+
kind: 1
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// Test NOTICE message parsing
|
|
91
|
+
const noticeMessage = ['NOTICE', 'test message'];
|
|
92
|
+
const noticeResponse = parseNostrMessage(noticeMessage);
|
|
93
|
+
expect(noticeResponse.type).toBe('NOTICE');
|
|
94
|
+
if (noticeResponse.type === 'NOTICE') {
|
|
95
|
+
expect(noticeResponse.payload).toBe('test message');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
test('Connection State Handling', () => {
|
|
99
|
+
expect(mockRelay.readyState).toBe(1); // Connected
|
|
100
|
+
mockRelay.close();
|
|
101
|
+
expect(mockRelay.readyState).toBe(3); // Closed
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('Transport Error Handling', () => {
|
|
105
|
+
test('Invalid Message Format', () => {
|
|
106
|
+
const invalidMessage = ['EVENT', { id: 'test_id', pubkey: 'test_pubkey', kind: 1, content: 'test', created_at: 123, sig: 'test_sig', tags: [] }];
|
|
107
|
+
expect(() => parseNostrMessage(invalidMessage)).not.toThrow();
|
|
108
|
+
});
|
|
109
|
+
test('Malformed JSON', () => {
|
|
110
|
+
const malformedMessage = ['EVENT', { id: 'test_id', pubkey: 'test_pubkey', kind: 1, content: 'test', created_at: 123, sig: 'test_sig', tags: [] }];
|
|
111
|
+
expect(() => parseNostrMessage(malformedMessage)).not.toThrow();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { isNostrEvent, isSignedNostrEvent, isNostrFilter, isNostrSubscription, NostrEventKind } from '../types';
|
|
2
|
+
describe('Type Guards', () => {
|
|
3
|
+
describe('isNostrEvent', () => {
|
|
4
|
+
it('should validate a valid NostrEvent', () => {
|
|
5
|
+
const validEvent = {
|
|
6
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
7
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
8
|
+
content: 'Hello, Nostr!',
|
|
9
|
+
tags: [['p', '1234'], ['t', 'test']],
|
|
10
|
+
pubkey: '0123456789abcdef'
|
|
11
|
+
};
|
|
12
|
+
expect(isNostrEvent(validEvent)).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
it('should validate a NostrEvent without optional pubkey', () => {
|
|
15
|
+
const validEvent = {
|
|
16
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
17
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
18
|
+
content: 'Hello, Nostr!',
|
|
19
|
+
tags: []
|
|
20
|
+
};
|
|
21
|
+
expect(isNostrEvent(validEvent)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it('should reject invalid NostrEvent objects', () => {
|
|
24
|
+
const invalidEvents = [
|
|
25
|
+
null,
|
|
26
|
+
undefined,
|
|
27
|
+
{},
|
|
28
|
+
{ kind: 'not a number' },
|
|
29
|
+
{ kind: 1, created_at: 'not a number' },
|
|
30
|
+
{ kind: 1, created_at: 123, content: 42 },
|
|
31
|
+
{ kind: 1, created_at: 123, content: 'valid', tags: 'not an array' },
|
|
32
|
+
{ kind: 1, created_at: 123, content: 'valid', tags: [['valid'], 42] },
|
|
33
|
+
{ kind: 1, created_at: 123, content: 'valid', tags: [], pubkey: 42 }
|
|
34
|
+
];
|
|
35
|
+
invalidEvents.forEach(event => {
|
|
36
|
+
expect(isNostrEvent(event)).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('should handle edge cases for NostrEvent', () => {
|
|
40
|
+
const edgeCases = [
|
|
41
|
+
// Large tag array
|
|
42
|
+
{
|
|
43
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
44
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
45
|
+
content: 'Test',
|
|
46
|
+
tags: Array(1000).fill(['p', '1234']),
|
|
47
|
+
},
|
|
48
|
+
// Unicode content
|
|
49
|
+
{
|
|
50
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
51
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
52
|
+
content: '🦄 Unicode test 漢字',
|
|
53
|
+
tags: [],
|
|
54
|
+
},
|
|
55
|
+
// Zero timestamp
|
|
56
|
+
{
|
|
57
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
58
|
+
created_at: 0,
|
|
59
|
+
content: 'Test',
|
|
60
|
+
tags: [],
|
|
61
|
+
},
|
|
62
|
+
// Reserved event kind
|
|
63
|
+
{
|
|
64
|
+
kind: 10000,
|
|
65
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
66
|
+
content: 'Test',
|
|
67
|
+
tags: [],
|
|
68
|
+
},
|
|
69
|
+
// Empty content
|
|
70
|
+
{
|
|
71
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
72
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
73
|
+
content: '',
|
|
74
|
+
tags: [],
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
edgeCases.forEach(event => {
|
|
78
|
+
expect(isNostrEvent(event)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('isSignedNostrEvent', () => {
|
|
83
|
+
it('should validate a valid SignedNostrEvent', () => {
|
|
84
|
+
const validEvent = {
|
|
85
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
86
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
87
|
+
content: 'Hello, Nostr!',
|
|
88
|
+
tags: [['p', '1234'], ['t', 'test']],
|
|
89
|
+
pubkey: '0123456789abcdef',
|
|
90
|
+
id: 'event_id_hash',
|
|
91
|
+
sig: 'valid_signature'
|
|
92
|
+
};
|
|
93
|
+
expect(isSignedNostrEvent(validEvent)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
it('should reject invalid SignedNostrEvent objects', () => {
|
|
96
|
+
const invalidEvents = [
|
|
97
|
+
null,
|
|
98
|
+
undefined,
|
|
99
|
+
{},
|
|
100
|
+
{ ...validNostrEvent }, // Missing id and sig
|
|
101
|
+
{ ...validNostrEvent, id: 42 }, // Wrong id type
|
|
102
|
+
{ ...validNostrEvent, id: 'valid', sig: 42 }, // Wrong sig type
|
|
103
|
+
{ ...validNostrEvent, id: 'valid', sig: 'valid', pubkey: undefined } // Missing required pubkey
|
|
104
|
+
];
|
|
105
|
+
invalidEvents.forEach(event => {
|
|
106
|
+
expect(isSignedNostrEvent(event)).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('isNostrFilter', () => {
|
|
111
|
+
it('should validate a valid NostrFilter', () => {
|
|
112
|
+
const validFilter = {
|
|
113
|
+
ids: ['1234', '5678'],
|
|
114
|
+
authors: ['abcd', 'efgh'],
|
|
115
|
+
kinds: [1, 2, 3],
|
|
116
|
+
'#e': ['event1', 'event2'],
|
|
117
|
+
'#p': ['pubkey1', 'pubkey2'],
|
|
118
|
+
since: 123456789,
|
|
119
|
+
until: 987654321,
|
|
120
|
+
limit: 100
|
|
121
|
+
};
|
|
122
|
+
expect(isNostrFilter(validFilter)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
it('should validate a NostrFilter with partial fields', () => {
|
|
125
|
+
const partialFilters = [
|
|
126
|
+
{ ids: ['1234'] },
|
|
127
|
+
{ authors: ['abcd'] },
|
|
128
|
+
{ kinds: [1] },
|
|
129
|
+
{ '#e': ['event1'] },
|
|
130
|
+
{ '#p': ['pubkey1'] },
|
|
131
|
+
{ since: 123456789 },
|
|
132
|
+
{ until: 987654321 },
|
|
133
|
+
{ limit: 100 }
|
|
134
|
+
];
|
|
135
|
+
partialFilters.forEach(filter => {
|
|
136
|
+
expect(isNostrFilter(filter)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
it('should reject invalid NostrFilter objects', () => {
|
|
140
|
+
const invalidFilters = [
|
|
141
|
+
null,
|
|
142
|
+
undefined,
|
|
143
|
+
{ ids: [42] }, // Wrong type in array
|
|
144
|
+
{ authors: ['valid', 42] }, // Mixed types in array
|
|
145
|
+
{ kinds: ['1'] }, // Wrong array type
|
|
146
|
+
{ '#e': [42] }, // Wrong type in array
|
|
147
|
+
{ '#p': [true] }, // Wrong type in array
|
|
148
|
+
{ since: '123' }, // Wrong type
|
|
149
|
+
{ until: false }, // Wrong type
|
|
150
|
+
{ limit: '100' } // Wrong type
|
|
151
|
+
];
|
|
152
|
+
invalidFilters.forEach(filter => {
|
|
153
|
+
expect(isNostrFilter(filter)).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
it('should handle edge cases for NostrFilter', () => {
|
|
157
|
+
const edgeCases = [
|
|
158
|
+
// Multiple tag types
|
|
159
|
+
{
|
|
160
|
+
'#e': ['1234'],
|
|
161
|
+
'#p': ['5678'],
|
|
162
|
+
'#t': ['test'], // Custom tag
|
|
163
|
+
},
|
|
164
|
+
// Time range edge cases
|
|
165
|
+
{
|
|
166
|
+
since: 0,
|
|
167
|
+
until: Number.MAX_SAFE_INTEGER,
|
|
168
|
+
},
|
|
169
|
+
// Large limit
|
|
170
|
+
{
|
|
171
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
172
|
+
},
|
|
173
|
+
// Multiple array fields
|
|
174
|
+
{
|
|
175
|
+
ids: ['1', '2', '3'],
|
|
176
|
+
authors: ['a', 'b', 'c'],
|
|
177
|
+
kinds: [1, 2, 3, 10000],
|
|
178
|
+
}
|
|
179
|
+
];
|
|
180
|
+
edgeCases.forEach(filter => {
|
|
181
|
+
expect(isNostrFilter(filter)).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('isNostrSubscription', () => {
|
|
186
|
+
it('should validate a valid NostrSubscription', () => {
|
|
187
|
+
const validSubscription = {
|
|
188
|
+
id: 'sub_123',
|
|
189
|
+
filters: [
|
|
190
|
+
{ kinds: [1], limit: 10 },
|
|
191
|
+
{ authors: ['abcd'], since: 123456789 }
|
|
192
|
+
]
|
|
193
|
+
};
|
|
194
|
+
expect(isNostrSubscription(validSubscription)).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
it('should validate a NostrSubscription with empty filters', () => {
|
|
197
|
+
const validSubscription = {
|
|
198
|
+
id: 'sub_123',
|
|
199
|
+
filters: []
|
|
200
|
+
};
|
|
201
|
+
expect(isNostrSubscription(validSubscription)).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
it('should reject invalid NostrSubscription objects', () => {
|
|
204
|
+
const invalidSubscriptions = [
|
|
205
|
+
null,
|
|
206
|
+
undefined,
|
|
207
|
+
{},
|
|
208
|
+
{ id: 123, filters: [] }, // Wrong id type
|
|
209
|
+
{ id: 'valid' }, // Missing filters
|
|
210
|
+
{ id: 'valid', filters: 'not an array' }, // Wrong filters type
|
|
211
|
+
{ id: 'valid', filters: [null] }, // Invalid filter
|
|
212
|
+
{ id: 'valid', filters: [{ invalid: true }] } // Invalid filter object
|
|
213
|
+
];
|
|
214
|
+
invalidSubscriptions.forEach(sub => {
|
|
215
|
+
expect(isNostrSubscription(sub)).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
// Helper for tests
|
|
221
|
+
const validNostrEvent = {
|
|
222
|
+
kind: NostrEventKind.TEXT_NOTE,
|
|
223
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
224
|
+
content: 'Hello, Nostr!',
|
|
225
|
+
tags: [['p', '1234'], ['t', 'test']],
|
|
226
|
+
pubkey: '0123456789abcdef'
|
|
227
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { validateEvent, validateSignedEvent, validateFilter, validateSubscription } from '../validation';
|
|
2
|
+
describe('Validation Functions', () => {
|
|
3
|
+
describe('validateEvent', () => {
|
|
4
|
+
it('should validate a valid event', () => {
|
|
5
|
+
const event = {
|
|
6
|
+
kind: 1,
|
|
7
|
+
content: 'Hello, Nostr!',
|
|
8
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
9
|
+
tags: [['p', '1234567890abcdef']],
|
|
10
|
+
pubkey: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
|
|
11
|
+
};
|
|
12
|
+
const result = validateEvent(event);
|
|
13
|
+
expect(result.isValid).toBe(true);
|
|
14
|
+
expect(result.errors).toHaveLength(0);
|
|
15
|
+
});
|
|
16
|
+
it('should reject an event with invalid kind', () => {
|
|
17
|
+
const event = {
|
|
18
|
+
kind: -1,
|
|
19
|
+
content: 'Hello, Nostr!',
|
|
20
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
21
|
+
tags: [],
|
|
22
|
+
pubkey: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
|
|
23
|
+
};
|
|
24
|
+
const result = validateEvent(event);
|
|
25
|
+
expect(result.isValid).toBe(false);
|
|
26
|
+
expect(result.errors).toContain('Event kind must be a non-negative integer');
|
|
27
|
+
});
|
|
28
|
+
it('should reject an event with future timestamp', () => {
|
|
29
|
+
const event = {
|
|
30
|
+
kind: 1,
|
|
31
|
+
content: 'Hello, Nostr!',
|
|
32
|
+
created_at: Math.floor(Date.now() / 1000) + 7200, // 2 hours in the future
|
|
33
|
+
tags: [],
|
|
34
|
+
pubkey: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
|
|
35
|
+
};
|
|
36
|
+
const result = validateEvent(event);
|
|
37
|
+
expect(result.isValid).toBe(false);
|
|
38
|
+
expect(result.errors).toContain('Event timestamp is too far in the future');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('validateSignedEvent', () => {
|
|
42
|
+
it('should validate a valid signed event', () => {
|
|
43
|
+
const event = {
|
|
44
|
+
id: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
45
|
+
sig: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
46
|
+
kind: 1,
|
|
47
|
+
content: 'Hello, Nostr!',
|
|
48
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
49
|
+
tags: [],
|
|
50
|
+
pubkey: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
|
|
51
|
+
};
|
|
52
|
+
const result = validateSignedEvent(event);
|
|
53
|
+
expect(result.isValid).toBe(true);
|
|
54
|
+
expect(result.errors).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
it('should reject an event with invalid signature format', () => {
|
|
57
|
+
const event = {
|
|
58
|
+
id: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
59
|
+
sig: 'invalid-signature',
|
|
60
|
+
kind: 1,
|
|
61
|
+
content: 'Hello, Nostr!',
|
|
62
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
63
|
+
tags: [],
|
|
64
|
+
pubkey: '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
|
|
65
|
+
};
|
|
66
|
+
const result = validateSignedEvent(event);
|
|
67
|
+
expect(result.isValid).toBe(false);
|
|
68
|
+
expect(result.errors).toContain('Invalid signature format');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('validateFilter', () => {
|
|
72
|
+
it('should validate a valid filter', () => {
|
|
73
|
+
const filter = {
|
|
74
|
+
kinds: [1, 2],
|
|
75
|
+
authors: ['1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'],
|
|
76
|
+
since: Math.floor(Date.now() / 1000) - 3600,
|
|
77
|
+
until: Math.floor(Date.now() / 1000),
|
|
78
|
+
limit: 100
|
|
79
|
+
};
|
|
80
|
+
const result = validateFilter(filter);
|
|
81
|
+
expect(result.isValid).toBe(true);
|
|
82
|
+
expect(result.errors).toHaveLength(0);
|
|
83
|
+
});
|
|
84
|
+
it('should reject a filter with invalid timestamps', () => {
|
|
85
|
+
const filter = {
|
|
86
|
+
kinds: [1],
|
|
87
|
+
since: Math.floor(Date.now() / 1000),
|
|
88
|
+
until: Math.floor(Date.now() / 1000) - 3600 // until before since
|
|
89
|
+
};
|
|
90
|
+
const result = validateFilter(filter);
|
|
91
|
+
expect(result.isValid).toBe(false);
|
|
92
|
+
expect(result.errors).toContain('since timestamp cannot be greater than until timestamp');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('validateSubscription', () => {
|
|
96
|
+
it('should validate a valid subscription', () => {
|
|
97
|
+
const subscription = {
|
|
98
|
+
id: 'sub1',
|
|
99
|
+
filters: [{
|
|
100
|
+
kinds: [1],
|
|
101
|
+
limit: 100
|
|
102
|
+
}]
|
|
103
|
+
};
|
|
104
|
+
const result = validateSubscription(subscription);
|
|
105
|
+
expect(result.isValid).toBe(true);
|
|
106
|
+
expect(result.errors).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
it('should reject a subscription without filters', () => {
|
|
109
|
+
const subscription = {
|
|
110
|
+
id: 'sub1',
|
|
111
|
+
filters: []
|
|
112
|
+
};
|
|
113
|
+
const result = validateSubscription(subscription);
|
|
114
|
+
expect(result.isValid).toBe(false);
|
|
115
|
+
expect(result.errors).toContain('Subscription must contain at least one filter');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|