nostr-websocket-utils 0.2.4 → 0.3.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/LICENSE +1 -1
- package/README.md +151 -103
- package/dist/__mocks__/extendedWsMock.d.ts +35 -0
- package/dist/__mocks__/extendedWsMock.js +156 -0
- package/dist/__mocks__/logger.d.ts +9 -0
- package/dist/__mocks__/logger.js +6 -0
- package/dist/__mocks__/mockLogger.d.ts +41 -0
- package/dist/__mocks__/mockLogger.js +47 -0
- package/dist/__mocks__/mockserver.d.ts +31 -0
- package/dist/__mocks__/mockserver.js +39 -0
- package/dist/__mocks__/wsMock.d.ts +26 -0
- package/dist/__mocks__/wsMock.js +120 -0
- package/dist/client.d.ts +105 -0
- package/dist/client.js +105 -0
- package/dist/core/client.d.ts +94 -0
- package/dist/core/client.js +360 -0
- package/dist/core/nostr-server.d.ts +27 -0
- package/dist/core/nostr-server.js +95 -0
- package/dist/core/queue.d.ts +61 -0
- package/dist/core/queue.js +108 -0
- package/dist/core/server.d.ts +27 -0
- package/dist/core/server.js +114 -0
- package/dist/crypto/bech32.d.ts +26 -0
- package/dist/crypto/bech32.js +163 -0
- package/dist/crypto/handlers.d.ts +11 -0
- package/dist/crypto/handlers.js +36 -0
- package/dist/crypto/index.d.ts +5 -0
- package/dist/crypto/index.js +5 -0
- package/dist/crypto/schnorr.d.ts +16 -0
- package/dist/crypto/schnorr.js +51 -0
- package/dist/endpoints/metrics.d.ts +29 -0
- package/dist/endpoints/metrics.js +101 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +16 -4
- package/dist/nips/index.d.ts +19 -0
- package/dist/nips/index.js +34 -0
- package/dist/nips/nip-01.d.ts +34 -0
- package/dist/nips/nip-01.js +145 -0
- package/dist/nips/nip-02.d.ts +83 -0
- package/dist/nips/nip-02.js +123 -0
- package/dist/nips/nip-04.d.ts +36 -0
- package/dist/nips/nip-04.js +105 -0
- package/dist/nips/nip-05.d.ts +86 -0
- package/dist/nips/nip-05.js +151 -0
- package/dist/nips/nip-09.d.ts +92 -0
- package/dist/nips/nip-09.js +190 -0
- package/dist/nips/nip-11.d.ts +64 -0
- package/dist/nips/nip-11.js +154 -0
- package/dist/nips/nip-13.d.ts +73 -0
- package/dist/nips/nip-13.js +128 -0
- package/dist/nips/nip-15.d.ts +83 -0
- package/dist/nips/nip-15.js +101 -0
- package/dist/nips/nip-16.d.ts +88 -0
- package/dist/nips/nip-16.js +150 -0
- package/dist/nips/nip-19.d.ts +28 -0
- package/dist/nips/nip-19.js +103 -0
- package/dist/nips/nip-20.d.ts +59 -0
- package/dist/nips/nip-20.js +95 -0
- package/dist/nips/nip-22.d.ts +89 -0
- package/dist/nips/nip-22.js +142 -0
- package/dist/nips/nip-26.d.ts +52 -0
- package/dist/nips/nip-26.js +139 -0
- package/dist/nips/nip-28.d.ts +103 -0
- package/dist/nips/nip-28.js +170 -0
- package/dist/nips/nip-33.d.ts +94 -0
- package/dist/nips/nip-33.js +133 -0
- package/dist/nostr-server.d.ts +23 -0
- package/dist/nostr-server.js +44 -0
- package/dist/server.d.ts +13 -3
- package/dist/server.js +60 -33
- package/dist/transport/base.d.ts +54 -0
- package/dist/transport/base.js +104 -0
- package/dist/transport/websocket.d.ts +22 -0
- package/dist/transport/websocket.js +122 -0
- package/dist/types/events.d.ts +63 -0
- package/dist/types/events.js +5 -0
- package/dist/types/filters.d.ts +19 -0
- package/dist/types/filters.js +5 -0
- package/dist/types/handlers.d.ts +80 -0
- package/dist/types/handlers.js +5 -0
- package/dist/types/index.d.ts +118 -39
- package/dist/types/index.js +21 -1
- package/dist/types/logger.d.ts +40 -0
- package/dist/types/logger.js +5 -0
- package/dist/types/messages.d.ts +135 -0
- package/dist/types/messages.js +40 -0
- package/dist/types/nostr.d.ts +120 -39
- package/dist/types/nostr.js +5 -10
- package/dist/types/options.d.ts +154 -0
- package/dist/types/options.js +5 -0
- package/dist/types/relays.d.ts +26 -0
- package/dist/types/relays.js +5 -0
- package/dist/types/scoring.d.ts +47 -0
- package/dist/types/scoring.js +29 -0
- package/dist/types/socket.d.ts +99 -0
- package/dist/types/socket.js +5 -0
- package/dist/types/transport.d.ts +97 -0
- package/dist/types/transport.js +5 -0
- package/dist/types/validation.d.ts +50 -0
- package/dist/types/validation.js +5 -0
- package/dist/types/websocket.d.ts +172 -0
- package/dist/types/websocket.js +5 -0
- package/dist/utils/http.d.ts +10 -0
- package/dist/utils/http.js +24 -0
- package/dist/utils/logger.d.ts +11 -2
- package/dist/utils/logger.js +18 -13
- package/dist/utils/metrics.d.ts +81 -0
- package/dist/utils/metrics.js +206 -0
- package/dist/utils/rate-limiter.d.ts +85 -0
- package/dist/utils/rate-limiter.js +175 -0
- package/package.json +18 -21
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-13: Proof of Work
|
|
3
|
+
* @module nips/nip-13
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/13.md
|
|
5
|
+
*/
|
|
6
|
+
import type { NostrWSMessage } from '../types/messages';
|
|
7
|
+
import type { Logger } from '../types/logger';
|
|
8
|
+
/**
|
|
9
|
+
* Calculates the number of leading zero bits in a hex string
|
|
10
|
+
* @param hex - Hex string to check
|
|
11
|
+
* @returns {number} Number of leading zero bits
|
|
12
|
+
*/
|
|
13
|
+
export declare function countLeadingZeroBits(hex: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Calculates event ID with proof of work
|
|
16
|
+
* @param event - Event object without ID
|
|
17
|
+
* @param targetDifficulty - Target number of leading zero bits
|
|
18
|
+
* @param maxAttempts - Maximum number of attempts
|
|
19
|
+
* @returns {Promise<string>} Event ID with sufficient proof of work
|
|
20
|
+
*/
|
|
21
|
+
export declare function calculatePowEventId(event: Record<string, unknown>, targetDifficulty: number, maxAttempts?: number): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Validates proof of work for an event
|
|
24
|
+
* @param message - Message containing event
|
|
25
|
+
* @param minDifficulty - Minimum required difficulty
|
|
26
|
+
* @param logger - Logger instance
|
|
27
|
+
* @returns {boolean} True if proof of work is valid
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateEventPoW(message: NostrWSMessage, minDifficulty: number, logger: Logger): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Dynamic difficulty calculator based on event type and content
|
|
32
|
+
*/
|
|
33
|
+
export interface DifficultyCalculator {
|
|
34
|
+
/**
|
|
35
|
+
* Calculates required difficulty for an event
|
|
36
|
+
* @param event - Event to check
|
|
37
|
+
* @returns {number} Required number of leading zero bits
|
|
38
|
+
*/
|
|
39
|
+
calculateRequiredDifficulty(event: Record<string, unknown>): number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Creates a default difficulty calculator
|
|
43
|
+
* @param baseDifficulty - Base difficulty for all events
|
|
44
|
+
* @param contentMultiplier - Multiplier based on content length
|
|
45
|
+
* @returns {DifficultyCalculator} Difficulty calculator
|
|
46
|
+
*/
|
|
47
|
+
export declare function createDifficultyCalculator(baseDifficulty?: number, contentMultiplier?: number): DifficultyCalculator;
|
|
48
|
+
/**
|
|
49
|
+
* Rate limiter interface for proof of work
|
|
50
|
+
*/
|
|
51
|
+
export interface PowRateLimiter {
|
|
52
|
+
/**
|
|
53
|
+
* Checks if an event should be rate limited
|
|
54
|
+
* @param pubkey - Publisher's public key
|
|
55
|
+
* @param currentTime - Current timestamp
|
|
56
|
+
* @returns {boolean} True if should be rate limited
|
|
57
|
+
*/
|
|
58
|
+
shouldRateLimit(pubkey: string, currentTime: number): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Records an event for rate limiting
|
|
61
|
+
* @param pubkey - Publisher's public key
|
|
62
|
+
* @param difficulty - Event difficulty
|
|
63
|
+
* @param currentTime - Current timestamp
|
|
64
|
+
*/
|
|
65
|
+
recordEvent(pubkey: string, difficulty: number, currentTime: number): void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Creates a default PoW rate limiter
|
|
69
|
+
* @param windowSeconds - Time window for rate limiting
|
|
70
|
+
* @param maxDifficulty - Maximum cumulative difficulty per window
|
|
71
|
+
* @returns {PowRateLimiter} Rate limiter
|
|
72
|
+
*/
|
|
73
|
+
export declare function createPowRateLimiter(windowSeconds?: number, maxDifficulty?: number): PowRateLimiter;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-13: Proof of Work
|
|
3
|
+
* @module nips/nip-13
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/13.md
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
/**
|
|
8
|
+
* Calculates the number of leading zero bits in a hex string
|
|
9
|
+
* @param hex - Hex string to check
|
|
10
|
+
* @returns {number} Number of leading zero bits
|
|
11
|
+
*/
|
|
12
|
+
export function countLeadingZeroBits(hex) {
|
|
13
|
+
let count = 0;
|
|
14
|
+
for (const char of hex) {
|
|
15
|
+
const nibble = parseInt(char, 16);
|
|
16
|
+
if (nibble === 0) {
|
|
17
|
+
count += 4;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
count += Math.clz32(nibble) - 28;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return count;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Calculates event ID with proof of work
|
|
28
|
+
* @param event - Event object without ID
|
|
29
|
+
* @param targetDifficulty - Target number of leading zero bits
|
|
30
|
+
* @param maxAttempts - Maximum number of attempts
|
|
31
|
+
* @returns {Promise<string>} Event ID with sufficient proof of work
|
|
32
|
+
*/
|
|
33
|
+
export async function calculatePowEventId(event, targetDifficulty, maxAttempts = 1000000) {
|
|
34
|
+
let nonce = 0;
|
|
35
|
+
const eventCopy = { ...event };
|
|
36
|
+
while (nonce < maxAttempts) {
|
|
37
|
+
eventCopy.nonce = nonce.toString();
|
|
38
|
+
const serialized = JSON.stringify([
|
|
39
|
+
0,
|
|
40
|
+
eventCopy.pubkey,
|
|
41
|
+
eventCopy.created_at,
|
|
42
|
+
eventCopy.kind,
|
|
43
|
+
eventCopy.tags,
|
|
44
|
+
eventCopy.content,
|
|
45
|
+
eventCopy.nonce
|
|
46
|
+
]);
|
|
47
|
+
const hash = createHash('sha256').update(serialized).digest('hex');
|
|
48
|
+
const difficulty = countLeadingZeroBits(hash);
|
|
49
|
+
if (difficulty >= targetDifficulty) {
|
|
50
|
+
return hash;
|
|
51
|
+
}
|
|
52
|
+
nonce++;
|
|
53
|
+
}
|
|
54
|
+
throw new Error('Failed to find proof of work within maximum attempts');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validates proof of work for an event
|
|
58
|
+
* @param message - Message containing event
|
|
59
|
+
* @param minDifficulty - Minimum required difficulty
|
|
60
|
+
* @param logger - Logger instance
|
|
61
|
+
* @returns {boolean} True if proof of work is valid
|
|
62
|
+
*/
|
|
63
|
+
export function validateEventPoW(message, minDifficulty, logger) {
|
|
64
|
+
try {
|
|
65
|
+
if (message.type !== 'EVENT' || !message.data) {
|
|
66
|
+
return true; // Not an event message
|
|
67
|
+
}
|
|
68
|
+
const event = message.data;
|
|
69
|
+
if (!event.id || typeof event.id !== 'string') {
|
|
70
|
+
logger.debug('Missing event ID');
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
// Calculate difficulty
|
|
74
|
+
const difficulty = countLeadingZeroBits(event.id);
|
|
75
|
+
return difficulty >= minDifficulty;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
logger.error('Error validating proof of work:', error);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Creates a default difficulty calculator
|
|
84
|
+
* @param baseDifficulty - Base difficulty for all events
|
|
85
|
+
* @param contentMultiplier - Multiplier based on content length
|
|
86
|
+
* @returns {DifficultyCalculator} Difficulty calculator
|
|
87
|
+
*/
|
|
88
|
+
export function createDifficultyCalculator(baseDifficulty = 8, contentMultiplier = 0.001) {
|
|
89
|
+
return {
|
|
90
|
+
calculateRequiredDifficulty(event) {
|
|
91
|
+
let difficulty = baseDifficulty;
|
|
92
|
+
// Increase difficulty for larger content
|
|
93
|
+
if (typeof event.content === 'string') {
|
|
94
|
+
difficulty += Math.floor(event.content.length * contentMultiplier);
|
|
95
|
+
}
|
|
96
|
+
// Increase difficulty for certain event kinds
|
|
97
|
+
const kind = event.kind;
|
|
98
|
+
if (kind >= 1000) {
|
|
99
|
+
difficulty += 4; // Higher difficulty for application-specific events
|
|
100
|
+
}
|
|
101
|
+
return difficulty;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Creates a default PoW rate limiter
|
|
107
|
+
* @param windowSeconds - Time window for rate limiting
|
|
108
|
+
* @param maxDifficulty - Maximum cumulative difficulty per window
|
|
109
|
+
* @returns {PowRateLimiter} Rate limiter
|
|
110
|
+
*/
|
|
111
|
+
export function createPowRateLimiter(windowSeconds = 3600, maxDifficulty = 100) {
|
|
112
|
+
const difficulties = new Map();
|
|
113
|
+
return {
|
|
114
|
+
shouldRateLimit(pubkey, currentTime) {
|
|
115
|
+
const events = difficulties.get(pubkey) || [];
|
|
116
|
+
// Remove old events
|
|
117
|
+
const validEvents = events.filter(([time]) => currentTime - time < windowSeconds);
|
|
118
|
+
// Calculate cumulative difficulty
|
|
119
|
+
const totalDifficulty = validEvents.reduce((sum, [, diff]) => sum + diff, 0);
|
|
120
|
+
return totalDifficulty >= maxDifficulty;
|
|
121
|
+
},
|
|
122
|
+
recordEvent(pubkey, difficulty, currentTime) {
|
|
123
|
+
const events = difficulties.get(pubkey) || [];
|
|
124
|
+
events.push([currentTime, difficulty]);
|
|
125
|
+
difficulties.set(pubkey, events);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-15: End of Stored Events Notice
|
|
3
|
+
* @module nips/nip-15
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/15.md
|
|
5
|
+
*/
|
|
6
|
+
import type { NostrWSMessage } from '../types/messages';
|
|
7
|
+
import type { Logger } from '../types/logger';
|
|
8
|
+
/**
|
|
9
|
+
* Creates an EOSE (End of Stored Events) message
|
|
10
|
+
* @param subscriptionId - Subscription ID
|
|
11
|
+
* @returns {NostrWSMessage} EOSE message
|
|
12
|
+
*/
|
|
13
|
+
export declare function createEOSEMessage(subscriptionId: string): NostrWSMessage;
|
|
14
|
+
/**
|
|
15
|
+
* Subscription state manager interface
|
|
16
|
+
*/
|
|
17
|
+
export interface SubscriptionStateManager {
|
|
18
|
+
/**
|
|
19
|
+
* Registers a new subscription
|
|
20
|
+
* @param subscriptionId - Subscription ID
|
|
21
|
+
* @param filter - Subscription filter
|
|
22
|
+
*/
|
|
23
|
+
registerSubscription(subscriptionId: string, filter: Record<string, unknown>): void;
|
|
24
|
+
/**
|
|
25
|
+
* Marks a subscription as complete (EOSE sent)
|
|
26
|
+
* @param subscriptionId - Subscription ID
|
|
27
|
+
*/
|
|
28
|
+
markComplete(subscriptionId: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a subscription is complete
|
|
31
|
+
* @param subscriptionId - Subscription ID
|
|
32
|
+
* @returns {boolean} True if EOSE has been sent
|
|
33
|
+
*/
|
|
34
|
+
isComplete(subscriptionId: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Gets subscription filter
|
|
37
|
+
* @param subscriptionId - Subscription ID
|
|
38
|
+
* @returns {Record<string, unknown> | undefined} Subscription filter
|
|
39
|
+
*/
|
|
40
|
+
getFilter(subscriptionId: string): Record<string, unknown> | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Removes a subscription
|
|
43
|
+
* @param subscriptionId - Subscription ID
|
|
44
|
+
*/
|
|
45
|
+
removeSubscription(subscriptionId: string): void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Creates a subscription state manager
|
|
49
|
+
* @param logger - Logger instance
|
|
50
|
+
* @returns {SubscriptionStateManager} Subscription state manager
|
|
51
|
+
*/
|
|
52
|
+
export declare function createSubscriptionStateManager(logger: Logger): SubscriptionStateManager;
|
|
53
|
+
/**
|
|
54
|
+
* Pagination handler interface
|
|
55
|
+
*/
|
|
56
|
+
export interface PaginationHandler {
|
|
57
|
+
/**
|
|
58
|
+
* Gets next page of events
|
|
59
|
+
* @param subscriptionId - Subscription ID
|
|
60
|
+
* @param pageSize - Number of events per page
|
|
61
|
+
* @returns {Promise<NostrWSMessage[]>} Next page of events
|
|
62
|
+
*/
|
|
63
|
+
getNextPage(subscriptionId: string, pageSize: number): Promise<NostrWSMessage[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Checks if more events are available
|
|
66
|
+
* @param subscriptionId - Subscription ID
|
|
67
|
+
* @returns {boolean} True if more events exist
|
|
68
|
+
*/
|
|
69
|
+
hasMoreEvents(subscriptionId: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Updates pagination state with new events
|
|
72
|
+
* @param subscriptionId - Subscription ID
|
|
73
|
+
* @param events - New events
|
|
74
|
+
*/
|
|
75
|
+
updateState(subscriptionId: string, events: NostrWSMessage[]): void;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Creates a pagination handler
|
|
79
|
+
* @param stateManager - Subscription state manager
|
|
80
|
+
* @param logger - Logger instance
|
|
81
|
+
* @returns {PaginationHandler} Pagination handler
|
|
82
|
+
*/
|
|
83
|
+
export declare function createPaginationHandler(stateManager: SubscriptionStateManager, logger: Logger): PaginationHandler;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-15: End of Stored Events Notice
|
|
3
|
+
* @module nips/nip-15
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/15.md
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Creates an EOSE (End of Stored Events) message
|
|
8
|
+
* @param subscriptionId - Subscription ID
|
|
9
|
+
* @returns {NostrWSMessage} EOSE message
|
|
10
|
+
*/
|
|
11
|
+
export function createEOSEMessage(subscriptionId) {
|
|
12
|
+
return {
|
|
13
|
+
type: 'EOSE',
|
|
14
|
+
data: {
|
|
15
|
+
subscription_id: subscriptionId
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a subscription state manager
|
|
21
|
+
* @param logger - Logger instance
|
|
22
|
+
* @returns {SubscriptionStateManager} Subscription state manager
|
|
23
|
+
*/
|
|
24
|
+
export function createSubscriptionStateManager(logger) {
|
|
25
|
+
const subscriptions = new Map();
|
|
26
|
+
return {
|
|
27
|
+
registerSubscription(subscriptionId, filter) {
|
|
28
|
+
subscriptions.set(subscriptionId, {
|
|
29
|
+
filter,
|
|
30
|
+
complete: false,
|
|
31
|
+
timestamp: Date.now()
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
markComplete(subscriptionId) {
|
|
35
|
+
const state = subscriptions.get(subscriptionId);
|
|
36
|
+
if (state) {
|
|
37
|
+
state.complete = true;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
logger.debug(`Attempting to mark unknown subscription ${subscriptionId} as complete`);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
isComplete(subscriptionId) {
|
|
44
|
+
return subscriptions.get(subscriptionId)?.complete || false;
|
|
45
|
+
},
|
|
46
|
+
getFilter(subscriptionId) {
|
|
47
|
+
return subscriptions.get(subscriptionId)?.filter;
|
|
48
|
+
},
|
|
49
|
+
removeSubscription(subscriptionId) {
|
|
50
|
+
subscriptions.delete(subscriptionId);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Creates a pagination handler
|
|
56
|
+
* @param stateManager - Subscription state manager
|
|
57
|
+
* @param logger - Logger instance
|
|
58
|
+
* @returns {PaginationHandler} Pagination handler
|
|
59
|
+
*/
|
|
60
|
+
export function createPaginationHandler(stateManager, logger) {
|
|
61
|
+
const paginationStates = new Map();
|
|
62
|
+
return {
|
|
63
|
+
async getNextPage(subscriptionId, pageSize) {
|
|
64
|
+
const state = paginationStates.get(subscriptionId);
|
|
65
|
+
if (!state) {
|
|
66
|
+
logger.debug(`No pagination state for subscription ${subscriptionId}`);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const start = state.currentIndex;
|
|
70
|
+
const end = Math.min(start + pageSize, state.events.length);
|
|
71
|
+
state.currentIndex = end;
|
|
72
|
+
state.hasMore = end < state.events.length;
|
|
73
|
+
// If this is the last page, send EOSE
|
|
74
|
+
if (!state.hasMore && !stateManager.isComplete(subscriptionId)) {
|
|
75
|
+
stateManager.markComplete(subscriptionId);
|
|
76
|
+
return [
|
|
77
|
+
...state.events.slice(start, end),
|
|
78
|
+
createEOSEMessage(subscriptionId)
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
return state.events.slice(start, end);
|
|
82
|
+
},
|
|
83
|
+
hasMoreEvents(subscriptionId) {
|
|
84
|
+
return paginationStates.get(subscriptionId)?.hasMore || false;
|
|
85
|
+
},
|
|
86
|
+
updateState(subscriptionId, events) {
|
|
87
|
+
const existing = paginationStates.get(subscriptionId);
|
|
88
|
+
if (existing) {
|
|
89
|
+
existing.events.push(...events);
|
|
90
|
+
existing.hasMore = true;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
paginationStates.set(subscriptionId, {
|
|
94
|
+
events,
|
|
95
|
+
currentIndex: 0,
|
|
96
|
+
hasMore: true
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-16: Event Treatment
|
|
3
|
+
* @module nips/nip-16
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/16.md
|
|
5
|
+
*/
|
|
6
|
+
import type { NostrWSMessage } from '../types/messages';
|
|
7
|
+
import type { Logger } from '../types/logger';
|
|
8
|
+
/**
|
|
9
|
+
* Event treatment types
|
|
10
|
+
*/
|
|
11
|
+
export declare const EventTreatment: {
|
|
12
|
+
readonly EPHEMERAL: "ephemeral";
|
|
13
|
+
readonly REPLACEABLE: "replaceable";
|
|
14
|
+
readonly PERSISTENT: "persistent";
|
|
15
|
+
};
|
|
16
|
+
export type EventTreatmentType = typeof EventTreatment[keyof typeof EventTreatment];
|
|
17
|
+
/**
|
|
18
|
+
* Replaceable event kinds (10000-19999)
|
|
19
|
+
*/
|
|
20
|
+
export declare const REPLACEABLE_EVENT_KINDS: {
|
|
21
|
+
METADATA: number;
|
|
22
|
+
CONTACT_LIST: number;
|
|
23
|
+
CHANNEL_METADATA: number;
|
|
24
|
+
CHANNEL_MESSAGE: number;
|
|
25
|
+
USER_STATUS: number;
|
|
26
|
+
USER_PROFILE: number;
|
|
27
|
+
RELAY_LIST: number;
|
|
28
|
+
BOOKMARKS: number;
|
|
29
|
+
MUTE_LIST: number;
|
|
30
|
+
PIN_LIST: number;
|
|
31
|
+
RELAY_METADATA: number;
|
|
32
|
+
CLIENT_PREFERENCES: number;
|
|
33
|
+
CUSTOM_START: number;
|
|
34
|
+
CUSTOM_END: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Ephemeral event kinds (20000-29999)
|
|
38
|
+
*/
|
|
39
|
+
export declare const EPHEMERAL_EVENT_KINDS: {
|
|
40
|
+
TYPING_INDICATOR: number;
|
|
41
|
+
ONLINE_STATUS: number;
|
|
42
|
+
USER_PRESENCE: number;
|
|
43
|
+
CUSTOM_START: number;
|
|
44
|
+
CUSTOM_END: number;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Determines event treatment type
|
|
48
|
+
* @param eventKind - Event kind number
|
|
49
|
+
* @returns {EventTreatmentType} Event treatment type
|
|
50
|
+
*/
|
|
51
|
+
export declare function getEventTreatment(eventKind: number): EventTreatmentType;
|
|
52
|
+
/**
|
|
53
|
+
* Validates event treatment rules
|
|
54
|
+
* @param message - Message to validate
|
|
55
|
+
* @param logger - Logger instance
|
|
56
|
+
* @returns {boolean} True if event follows treatment rules
|
|
57
|
+
*/
|
|
58
|
+
export declare function validateEventTreatment(message: NostrWSMessage, logger: Logger): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Storage management interface for different event types
|
|
61
|
+
*/
|
|
62
|
+
export interface EventStorageManager {
|
|
63
|
+
/**
|
|
64
|
+
* Determines if an event should be stored
|
|
65
|
+
* @param event - Event to check
|
|
66
|
+
* @returns {boolean} True if event should be stored
|
|
67
|
+
*/
|
|
68
|
+
shouldStore(event: Record<string, unknown>): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Gets storage duration for an event
|
|
71
|
+
* @param event - Event to check
|
|
72
|
+
* @returns {number} Storage duration in seconds (0 for permanent)
|
|
73
|
+
*/
|
|
74
|
+
getStorageDuration(event: Record<string, unknown>): number;
|
|
75
|
+
/**
|
|
76
|
+
* Checks if an event should replace another
|
|
77
|
+
* @param newEvent - New event
|
|
78
|
+
* @param existingEvent - Existing event
|
|
79
|
+
* @returns {boolean} True if new event should replace existing
|
|
80
|
+
*/
|
|
81
|
+
shouldReplace(newEvent: Record<string, unknown>, existingEvent: Record<string, unknown>): boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Creates default event storage manager
|
|
85
|
+
* @param logger - Logger instance
|
|
86
|
+
* @returns {EventStorageManager} Storage manager
|
|
87
|
+
*/
|
|
88
|
+
export declare function createEventStorageManager(logger: Logger): EventStorageManager;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-16: Event Treatment
|
|
3
|
+
* @module nips/nip-16
|
|
4
|
+
* @see https://github.com/nostr-protocol/nips/blob/master/16.md
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Event treatment types
|
|
8
|
+
*/
|
|
9
|
+
export const EventTreatment = {
|
|
10
|
+
EPHEMERAL: 'ephemeral',
|
|
11
|
+
REPLACEABLE: 'replaceable',
|
|
12
|
+
PERSISTENT: 'persistent'
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Replaceable event kinds (10000-19999)
|
|
16
|
+
*/
|
|
17
|
+
export const REPLACEABLE_EVENT_KINDS = {
|
|
18
|
+
METADATA: 0,
|
|
19
|
+
CONTACT_LIST: 3,
|
|
20
|
+
CHANNEL_METADATA: 41,
|
|
21
|
+
CHANNEL_MESSAGE: 42,
|
|
22
|
+
USER_STATUS: 10000,
|
|
23
|
+
USER_PROFILE: 10001,
|
|
24
|
+
RELAY_LIST: 10002,
|
|
25
|
+
BOOKMARKS: 10003,
|
|
26
|
+
MUTE_LIST: 10004,
|
|
27
|
+
PIN_LIST: 10005,
|
|
28
|
+
RELAY_METADATA: 10010,
|
|
29
|
+
CLIENT_PREFERENCES: 10015,
|
|
30
|
+
CUSTOM_START: 11000,
|
|
31
|
+
CUSTOM_END: 19999
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Ephemeral event kinds (20000-29999)
|
|
35
|
+
*/
|
|
36
|
+
export const EPHEMERAL_EVENT_KINDS = {
|
|
37
|
+
TYPING_INDICATOR: 20001,
|
|
38
|
+
ONLINE_STATUS: 20002,
|
|
39
|
+
USER_PRESENCE: 20003,
|
|
40
|
+
CUSTOM_START: 21000,
|
|
41
|
+
CUSTOM_END: 29999
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Determines event treatment type
|
|
45
|
+
* @param eventKind - Event kind number
|
|
46
|
+
* @returns {EventTreatmentType} Event treatment type
|
|
47
|
+
*/
|
|
48
|
+
export function getEventTreatment(eventKind) {
|
|
49
|
+
if (eventKind >= 20000 && eventKind < 30000) {
|
|
50
|
+
return EventTreatment.EPHEMERAL;
|
|
51
|
+
}
|
|
52
|
+
if (eventKind >= 10000 && eventKind < 20000 || [0, 3, 41, 42].includes(eventKind)) {
|
|
53
|
+
return EventTreatment.REPLACEABLE;
|
|
54
|
+
}
|
|
55
|
+
return EventTreatment.PERSISTENT;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Validates event treatment rules
|
|
59
|
+
* @param message - Message to validate
|
|
60
|
+
* @param logger - Logger instance
|
|
61
|
+
* @returns {boolean} True if event follows treatment rules
|
|
62
|
+
*/
|
|
63
|
+
export function validateEventTreatment(message, logger) {
|
|
64
|
+
try {
|
|
65
|
+
if (message.type !== 'EVENT' || !message.data) {
|
|
66
|
+
return true; // Not an event message
|
|
67
|
+
}
|
|
68
|
+
const event = message.data;
|
|
69
|
+
const kind = event.kind;
|
|
70
|
+
const treatment = getEventTreatment(kind);
|
|
71
|
+
// Validate based on treatment type
|
|
72
|
+
switch (treatment) {
|
|
73
|
+
case EventTreatment.EPHEMERAL:
|
|
74
|
+
// Ephemeral events shouldn't have large content
|
|
75
|
+
if (typeof event.content === 'string' && event.content.length > 1000) {
|
|
76
|
+
logger.debug('Ephemeral event content too large');
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
case EventTreatment.REPLACEABLE:
|
|
81
|
+
// Replaceable events must have proper 'd' tag for uniqueness
|
|
82
|
+
if (Array.isArray(event.tags)) {
|
|
83
|
+
const dTags = event.tags.filter(tag => Array.isArray(tag) && tag[0] === 'd');
|
|
84
|
+
if (dTags.length > 1) {
|
|
85
|
+
logger.debug('Multiple d tags in replaceable event');
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
logger.error('Error validating event treatment:', error);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Creates default event storage manager
|
|
100
|
+
* @param logger - Logger instance
|
|
101
|
+
* @returns {EventStorageManager} Storage manager
|
|
102
|
+
*/
|
|
103
|
+
export function createEventStorageManager(logger) {
|
|
104
|
+
return {
|
|
105
|
+
shouldStore(event) {
|
|
106
|
+
const kind = event.kind;
|
|
107
|
+
const treatment = getEventTreatment(kind);
|
|
108
|
+
// Don't store ephemeral events by default
|
|
109
|
+
if (treatment === EventTreatment.EPHEMERAL) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
},
|
|
114
|
+
getStorageDuration(event) {
|
|
115
|
+
const kind = event.kind;
|
|
116
|
+
const treatment = getEventTreatment(kind);
|
|
117
|
+
switch (treatment) {
|
|
118
|
+
case EventTreatment.EPHEMERAL:
|
|
119
|
+
return 300; // 5 minutes
|
|
120
|
+
case EventTreatment.REPLACEABLE:
|
|
121
|
+
return 0; // Permanent until replaced
|
|
122
|
+
default:
|
|
123
|
+
return 0; // Permanent
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
shouldReplace(newEvent, existingEvent) {
|
|
127
|
+
try {
|
|
128
|
+
const kind = newEvent.kind;
|
|
129
|
+
if (kind !== existingEvent.kind)
|
|
130
|
+
return false;
|
|
131
|
+
const treatment = getEventTreatment(kind);
|
|
132
|
+
if (treatment !== EventTreatment.REPLACEABLE)
|
|
133
|
+
return false;
|
|
134
|
+
// Check 'd' tag for parameterized replaceable events
|
|
135
|
+
if (Array.isArray(newEvent.tags) && Array.isArray(existingEvent.tags)) {
|
|
136
|
+
const newDTag = newEvent.tags.find(tag => Array.isArray(tag) && tag[0] === 'd')?.[1];
|
|
137
|
+
const existingDTag = existingEvent.tags.find(tag => Array.isArray(tag) && tag[0] === 'd')?.[1];
|
|
138
|
+
if (newDTag !== existingDTag)
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
// Replace if new event is newer
|
|
142
|
+
return newEvent.created_at > existingEvent.created_at;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
logger.error('Error checking event replacement:', error);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NIP-19: bech32-encoded entities
|
|
3
|
+
* @module nips/nip-19
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Encode a public key to bech32 npub format
|
|
7
|
+
*/
|
|
8
|
+
export declare function encodePubkey(pubkey: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Encode a private key to bech32 nsec format
|
|
11
|
+
*/
|
|
12
|
+
export declare function encodePrivkey(privkey: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Decode a bech32 npub to hex pubkey
|
|
15
|
+
*/
|
|
16
|
+
export declare function decodePubkey(npub: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Decode a bech32 nsec to hex privkey
|
|
19
|
+
*/
|
|
20
|
+
export declare function decodePrivkey(nsec: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Process tags containing bech32-encoded entities
|
|
23
|
+
*/
|
|
24
|
+
export declare function processBech32Tags(tags: string[][]): string[][];
|
|
25
|
+
/**
|
|
26
|
+
* Encode tags containing hex pubkeys to bech32
|
|
27
|
+
*/
|
|
28
|
+
export declare function encodeBech32Tags(tags: string[][]): string[][];
|