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.
Files changed (111) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +151 -103
  3. package/dist/__mocks__/extendedWsMock.d.ts +35 -0
  4. package/dist/__mocks__/extendedWsMock.js +156 -0
  5. package/dist/__mocks__/logger.d.ts +9 -0
  6. package/dist/__mocks__/logger.js +6 -0
  7. package/dist/__mocks__/mockLogger.d.ts +41 -0
  8. package/dist/__mocks__/mockLogger.js +47 -0
  9. package/dist/__mocks__/mockserver.d.ts +31 -0
  10. package/dist/__mocks__/mockserver.js +39 -0
  11. package/dist/__mocks__/wsMock.d.ts +26 -0
  12. package/dist/__mocks__/wsMock.js +120 -0
  13. package/dist/client.d.ts +105 -0
  14. package/dist/client.js +105 -0
  15. package/dist/core/client.d.ts +94 -0
  16. package/dist/core/client.js +360 -0
  17. package/dist/core/nostr-server.d.ts +27 -0
  18. package/dist/core/nostr-server.js +95 -0
  19. package/dist/core/queue.d.ts +61 -0
  20. package/dist/core/queue.js +108 -0
  21. package/dist/core/server.d.ts +27 -0
  22. package/dist/core/server.js +114 -0
  23. package/dist/crypto/bech32.d.ts +26 -0
  24. package/dist/crypto/bech32.js +163 -0
  25. package/dist/crypto/handlers.d.ts +11 -0
  26. package/dist/crypto/handlers.js +36 -0
  27. package/dist/crypto/index.d.ts +5 -0
  28. package/dist/crypto/index.js +5 -0
  29. package/dist/crypto/schnorr.d.ts +16 -0
  30. package/dist/crypto/schnorr.js +51 -0
  31. package/dist/endpoints/metrics.d.ts +29 -0
  32. package/dist/endpoints/metrics.js +101 -0
  33. package/dist/index.d.ts +11 -6
  34. package/dist/index.js +16 -4
  35. package/dist/nips/index.d.ts +19 -0
  36. package/dist/nips/index.js +34 -0
  37. package/dist/nips/nip-01.d.ts +34 -0
  38. package/dist/nips/nip-01.js +145 -0
  39. package/dist/nips/nip-02.d.ts +83 -0
  40. package/dist/nips/nip-02.js +123 -0
  41. package/dist/nips/nip-04.d.ts +36 -0
  42. package/dist/nips/nip-04.js +105 -0
  43. package/dist/nips/nip-05.d.ts +86 -0
  44. package/dist/nips/nip-05.js +151 -0
  45. package/dist/nips/nip-09.d.ts +92 -0
  46. package/dist/nips/nip-09.js +190 -0
  47. package/dist/nips/nip-11.d.ts +64 -0
  48. package/dist/nips/nip-11.js +154 -0
  49. package/dist/nips/nip-13.d.ts +73 -0
  50. package/dist/nips/nip-13.js +128 -0
  51. package/dist/nips/nip-15.d.ts +83 -0
  52. package/dist/nips/nip-15.js +101 -0
  53. package/dist/nips/nip-16.d.ts +88 -0
  54. package/dist/nips/nip-16.js +150 -0
  55. package/dist/nips/nip-19.d.ts +28 -0
  56. package/dist/nips/nip-19.js +103 -0
  57. package/dist/nips/nip-20.d.ts +59 -0
  58. package/dist/nips/nip-20.js +95 -0
  59. package/dist/nips/nip-22.d.ts +89 -0
  60. package/dist/nips/nip-22.js +142 -0
  61. package/dist/nips/nip-26.d.ts +52 -0
  62. package/dist/nips/nip-26.js +139 -0
  63. package/dist/nips/nip-28.d.ts +103 -0
  64. package/dist/nips/nip-28.js +170 -0
  65. package/dist/nips/nip-33.d.ts +94 -0
  66. package/dist/nips/nip-33.js +133 -0
  67. package/dist/nostr-server.d.ts +23 -0
  68. package/dist/nostr-server.js +44 -0
  69. package/dist/server.d.ts +13 -3
  70. package/dist/server.js +60 -33
  71. package/dist/transport/base.d.ts +54 -0
  72. package/dist/transport/base.js +104 -0
  73. package/dist/transport/websocket.d.ts +22 -0
  74. package/dist/transport/websocket.js +122 -0
  75. package/dist/types/events.d.ts +63 -0
  76. package/dist/types/events.js +5 -0
  77. package/dist/types/filters.d.ts +19 -0
  78. package/dist/types/filters.js +5 -0
  79. package/dist/types/handlers.d.ts +80 -0
  80. package/dist/types/handlers.js +5 -0
  81. package/dist/types/index.d.ts +118 -39
  82. package/dist/types/index.js +21 -1
  83. package/dist/types/logger.d.ts +40 -0
  84. package/dist/types/logger.js +5 -0
  85. package/dist/types/messages.d.ts +135 -0
  86. package/dist/types/messages.js +40 -0
  87. package/dist/types/nostr.d.ts +120 -39
  88. package/dist/types/nostr.js +5 -10
  89. package/dist/types/options.d.ts +154 -0
  90. package/dist/types/options.js +5 -0
  91. package/dist/types/relays.d.ts +26 -0
  92. package/dist/types/relays.js +5 -0
  93. package/dist/types/scoring.d.ts +47 -0
  94. package/dist/types/scoring.js +29 -0
  95. package/dist/types/socket.d.ts +99 -0
  96. package/dist/types/socket.js +5 -0
  97. package/dist/types/transport.d.ts +97 -0
  98. package/dist/types/transport.js +5 -0
  99. package/dist/types/validation.d.ts +50 -0
  100. package/dist/types/validation.js +5 -0
  101. package/dist/types/websocket.d.ts +172 -0
  102. package/dist/types/websocket.js +5 -0
  103. package/dist/utils/http.d.ts +10 -0
  104. package/dist/utils/http.js +24 -0
  105. package/dist/utils/logger.d.ts +11 -2
  106. package/dist/utils/logger.js +18 -13
  107. package/dist/utils/metrics.d.ts +81 -0
  108. package/dist/utils/metrics.js +206 -0
  109. package/dist/utils/rate-limiter.d.ts +85 -0
  110. package/dist/utils/rate-limiter.js +175 -0
  111. package/package.json +18 -21
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @file NIP-05: DNS-Based Verification
3
+ * @module nips/nip-05
4
+ * @see https://github.com/nostr-protocol/nips/blob/master/05.md
5
+ */
6
+ import { fetchJson } from '../utils/http';
7
+ /**
8
+ * Verifies a NIP-05 identifier
9
+ * @param identifier - Internet identifier (user@domain.com)
10
+ * @param pubkey - Public key to verify
11
+ * @param logger - Logger instance
12
+ * @returns {Promise<NIP05VerificationResult>} Verification result
13
+ */
14
+ export async function verifyNIP05Identifier(identifier, pubkey, logger) {
15
+ try {
16
+ // Parse identifier
17
+ const [name, domain] = identifier.split('@');
18
+ if (!name || !domain) {
19
+ return {
20
+ valid: false,
21
+ error: 'Invalid identifier format'
22
+ };
23
+ }
24
+ // Fetch well-known URL
25
+ const url = `https://${domain}/.well-known/nostr.json?name=${name}`;
26
+ const response = await fetchJson(url);
27
+ if (!response || !response.names || !response.names[name]) {
28
+ return {
29
+ valid: false,
30
+ error: 'Name not found in well-known file'
31
+ };
32
+ }
33
+ // Verify name matches pubkey
34
+ const verifiedPubkey = response.names[name];
35
+ if (!verifiedPubkey) {
36
+ return {
37
+ valid: false,
38
+ error: 'Name not found'
39
+ };
40
+ }
41
+ if (verifiedPubkey !== pubkey) {
42
+ return {
43
+ valid: false,
44
+ error: 'Public key mismatch'
45
+ };
46
+ }
47
+ // Get associated relays if available
48
+ const relays = response.relays?.[verifiedPubkey];
49
+ return {
50
+ valid: true,
51
+ pubkey: verifiedPubkey,
52
+ relays
53
+ };
54
+ }
55
+ catch (error) {
56
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
57
+ logger.error(`NIP-05 verification failed: ${errorMessage}`);
58
+ return {
59
+ valid: false,
60
+ error: errorMessage
61
+ };
62
+ }
63
+ }
64
+ /**
65
+ * Creates metadata event with NIP-05 identifier
66
+ * @param identifier - Internet identifier
67
+ * @param metadata - Additional metadata
68
+ * @returns {Record<string, unknown>} Metadata event content
69
+ */
70
+ export function createNIP05Metadata(identifier, metadata = {}) {
71
+ return {
72
+ ...metadata,
73
+ nip05: identifier
74
+ };
75
+ }
76
+ /**
77
+ * Creates a NIP-05 verification cache
78
+ * @param defaultTTL - Default TTL in seconds
79
+ * @returns {NIP05VerificationCache} Verification cache
80
+ */
81
+ export function createNIP05VerificationCache(defaultTTL = 3600) {
82
+ const cache = new Map();
83
+ function getCacheKey(identifier, pubkey) {
84
+ return `${identifier}:${pubkey}`;
85
+ }
86
+ return {
87
+ get(identifier, pubkey) {
88
+ const key = getCacheKey(identifier, pubkey);
89
+ const entry = cache.get(key);
90
+ if (!entry)
91
+ return undefined;
92
+ if (Date.now() > entry.expiresAt) {
93
+ cache.delete(key);
94
+ return undefined;
95
+ }
96
+ return entry.result;
97
+ },
98
+ set(identifier, pubkey, result, ttl = defaultTTL) {
99
+ const key = getCacheKey(identifier, pubkey);
100
+ cache.set(key, {
101
+ result,
102
+ expiresAt: Date.now() + (ttl * 1000)
103
+ });
104
+ },
105
+ cleanup() {
106
+ const now = Date.now();
107
+ for (const [key, entry] of cache.entries()) {
108
+ if (now > entry.expiresAt) {
109
+ cache.delete(key);
110
+ }
111
+ }
112
+ }
113
+ };
114
+ }
115
+ /**
116
+ * Creates a NIP-05 batch verifier
117
+ * @param logger - Logger instance
118
+ * @param cache - Optional verification cache
119
+ * @returns {NIP05BatchVerifier} Batch verifier
120
+ */
121
+ export function createNIP05BatchVerifier(logger, cache) {
122
+ const queue = new Map();
123
+ return {
124
+ addToQueue(identifier, pubkey) {
125
+ queue.set(identifier, pubkey);
126
+ },
127
+ async verifyAll() {
128
+ const results = new Map();
129
+ for (const [identifier, pubkey] of queue.entries()) {
130
+ // Check cache first
131
+ if (cache) {
132
+ const cached = cache.get(identifier, pubkey);
133
+ if (cached) {
134
+ results.set(identifier, cached);
135
+ continue;
136
+ }
137
+ }
138
+ // Verify and cache result
139
+ const result = await verifyNIP05Identifier(identifier, pubkey, logger);
140
+ results.set(identifier, result);
141
+ if (cache) {
142
+ cache.set(identifier, pubkey, result, 3600);
143
+ }
144
+ }
145
+ return results;
146
+ },
147
+ clearQueue() {
148
+ queue.clear();
149
+ }
150
+ };
151
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @file NIP-09: Event Deletion
3
+ * @module nips/nip-09
4
+ * @see https://github.com/nostr-protocol/nips/blob/master/09.md
5
+ */
6
+ import type { NostrEvent, NostrSubscriptionEvent } from '../types/events';
7
+ /**
8
+ * Represents the result of a deletion operation
9
+ * @interface DeletionResult
10
+ * @property {boolean} success - Whether the deletion operation was successful
11
+ * @property {string} [error] - Error message if the operation failed
12
+ * @property {string[]} [deletedIds] - Array of successfully deleted event IDs
13
+ */
14
+ interface DeletionResult {
15
+ success: boolean;
16
+ error?: string;
17
+ deletedIds?: string[];
18
+ }
19
+ /**
20
+ * Event deletion kind
21
+ */
22
+ export declare const EVENT_DELETION_KIND = 5;
23
+ /**
24
+ * Creates a deletion event
25
+ */
26
+ export declare function createDeletionEvent(eventIds: string[], reason?: string): Omit<NostrEvent, 'id' | 'sig'>;
27
+ /**
28
+ * Validates a deletion event
29
+ */
30
+ export declare function validateDeletionEvent(event: NostrEvent): boolean;
31
+ /**
32
+ * Processes a deletion event
33
+ */
34
+ export declare function processDeletionEvent(event: NostrEvent, _logger: any, deleteEvent: (id: string) => Promise<boolean>): Promise<DeletionResult>;
35
+ /**
36
+ * Event deletion manager interface
37
+ */
38
+ export interface EventDeletionManager {
39
+ /**
40
+ * Processes a deletion event
41
+ * @param message - Deletion message
42
+ * @returns {Promise<string[]>} Deleted event IDs
43
+ */
44
+ processDeletion(message: NostrEvent): Promise<string[]>;
45
+ /**
46
+ * Checks if an event has been deleted
47
+ * @param eventId - Event ID to check
48
+ * @returns {boolean} True if event is deleted
49
+ */
50
+ isDeleted(eventId: string): boolean;
51
+ /**
52
+ * Gets deletion reason for an event
53
+ * @param eventId - Event ID
54
+ * @returns {string | undefined} Deletion reason if available
55
+ */
56
+ getDeletionReason(eventId: string): string | undefined;
57
+ /**
58
+ * Gets all deleted events
59
+ * @returns {Map<string, string>} Map of event IDs to deletion reasons
60
+ */
61
+ getDeletedEvents(): Map<string, string>;
62
+ }
63
+ /**
64
+ * Creates an event deletion manager
65
+ * @param _logger - Logger instance
66
+ * @returns {EventDeletionManager} Deletion manager
67
+ */
68
+ export declare function createEventDeletionManager(_logger: any): EventDeletionManager;
69
+ /**
70
+ * Event deletion subscription manager interface
71
+ */
72
+ export interface DeletionSubscriptionManager {
73
+ /**
74
+ * Creates a subscription for deletion events
75
+ * @param eventIds - Event IDs to monitor
76
+ * @returns {NostrSubscriptionEvent} Subscription message
77
+ */
78
+ subscribeToDeletions(eventIds: string[]): NostrSubscriptionEvent;
79
+ /**
80
+ * Creates a subscription for all deletions by a user
81
+ * @param pubkey - Public key of user
82
+ * @returns {NostrSubscriptionEvent} Subscription message
83
+ */
84
+ subscribeToUserDeletions(pubkey: string): NostrSubscriptionEvent;
85
+ }
86
+ /**
87
+ * Creates a deletion subscription manager
88
+ * @param _logger - Logger instance
89
+ * @returns {DeletionSubscriptionManager} Subscription manager
90
+ */
91
+ export declare function createDeletionSubscriptionManager(_logger: any): DeletionSubscriptionManager;
92
+ export {};
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @file NIP-09: Event Deletion
3
+ * @module nips/nip-09
4
+ * @see https://github.com/nostr-protocol/nips/blob/master/09.md
5
+ */
6
+ import { getLogger } from '../utils/logger';
7
+ const logger = getLogger('NIP-09');
8
+ /**
9
+ * Event deletion kind
10
+ */
11
+ export const EVENT_DELETION_KIND = 5;
12
+ /**
13
+ * Creates a deletion event
14
+ */
15
+ export function createDeletionEvent(eventIds, reason) {
16
+ return {
17
+ kind: EVENT_DELETION_KIND,
18
+ created_at: Math.floor(Date.now() / 1000),
19
+ tags: eventIds.map(id => ['e', id]),
20
+ content: reason || '',
21
+ pubkey: '', // To be filled by the caller
22
+ };
23
+ }
24
+ /**
25
+ * Validates a deletion event
26
+ */
27
+ export function validateDeletionEvent(event) {
28
+ try {
29
+ // Must be kind 5
30
+ if (event.kind !== EVENT_DELETION_KIND) {
31
+ logger.debug('Invalid event kind for deletion');
32
+ return false;
33
+ }
34
+ // Must have at least one e tag
35
+ const eventIds = event.tags
36
+ .filter((tag) => Array.isArray(tag) && tag[0] === 'e' && typeof tag[1] === 'string')
37
+ .map(tag => tag[1]);
38
+ if (eventIds.length === 0) {
39
+ logger.debug('No event IDs to delete');
40
+ return false;
41
+ }
42
+ // Validate each event ID format
43
+ const validHexString = /^[0-9a-f]{64}$/;
44
+ const invalidIds = eventIds.filter(id => !validHexString.test(id));
45
+ if (invalidIds.length > 0) {
46
+ logger.debug(`Invalid event IDs: ${invalidIds.join(', ')}`);
47
+ return false;
48
+ }
49
+ return true;
50
+ }
51
+ catch (error) {
52
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
53
+ logger.error(`Error validating deletion event: ${errorMessage}`);
54
+ return false;
55
+ }
56
+ }
57
+ /**
58
+ * Processes a deletion event
59
+ */
60
+ export async function processDeletionEvent(event, _logger, deleteEvent) {
61
+ try {
62
+ if (!validateDeletionEvent(event)) {
63
+ return {
64
+ success: false,
65
+ error: 'Invalid deletion event'
66
+ };
67
+ }
68
+ const eventIds = event.tags
69
+ .filter((tag) => tag[0] === 'e')
70
+ .map(tag => tag[1]);
71
+ const deletedIds = [];
72
+ const failedIds = [];
73
+ for (const eventId of eventIds) {
74
+ try {
75
+ const success = await deleteEvent(eventId);
76
+ if (success) {
77
+ deletedIds.push(eventId);
78
+ }
79
+ else {
80
+ failedIds.push(eventId);
81
+ }
82
+ }
83
+ catch (error) {
84
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
85
+ logger.error(`Failed to delete event ${eventId}: ${errorMessage}`);
86
+ failedIds.push(eventId);
87
+ }
88
+ }
89
+ if (failedIds.length > 0) {
90
+ return {
91
+ success: false,
92
+ error: `Failed to delete events: ${failedIds.join(', ')}`,
93
+ deletedIds
94
+ };
95
+ }
96
+ return {
97
+ success: true,
98
+ deletedIds
99
+ };
100
+ }
101
+ catch (error) {
102
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
103
+ logger.error(`Error processing deletion event: ${errorMessage}`);
104
+ return {
105
+ success: false,
106
+ error: errorMessage
107
+ };
108
+ }
109
+ }
110
+ /**
111
+ * Creates an event deletion manager
112
+ * @param _logger - Logger instance
113
+ * @returns {EventDeletionManager} Deletion manager
114
+ */
115
+ export function createEventDeletionManager(_logger) {
116
+ // Map of deleted event IDs to deletion reasons
117
+ const deletedEvents = new Map();
118
+ // Map of deleted event IDs to deletion event IDs
119
+ const deletionEvents = new Map();
120
+ return {
121
+ async processDeletion(message) {
122
+ try {
123
+ if (!validateDeletionEvent(message)) {
124
+ throw new Error('Invalid deletion event');
125
+ }
126
+ const event = message;
127
+ const eventIds = event.tags
128
+ .filter(tag => tag[0] === 'e')
129
+ .map(tag => tag[1]);
130
+ const reason = event.content;
131
+ const deletionId = event.id;
132
+ // Process each event ID
133
+ eventIds.forEach(eventId => {
134
+ // Only delete if not already deleted or if this deletion is newer
135
+ const existingDeletionId = deletionEvents.get(eventId);
136
+ if (!existingDeletionId || deletionId > existingDeletionId) {
137
+ deletedEvents.set(eventId, reason);
138
+ deletionEvents.set(eventId, deletionId);
139
+ }
140
+ });
141
+ logger.debug('Processed deletion event', {
142
+ deletionId,
143
+ eventIds,
144
+ reason
145
+ });
146
+ return eventIds;
147
+ }
148
+ catch (error) {
149
+ logger.error('Error processing deletion event:', error);
150
+ return [];
151
+ }
152
+ },
153
+ isDeleted(eventId) {
154
+ return deletedEvents.has(eventId);
155
+ },
156
+ getDeletionReason(eventId) {
157
+ return deletedEvents.get(eventId);
158
+ },
159
+ getDeletedEvents() {
160
+ return new Map(deletedEvents);
161
+ }
162
+ };
163
+ }
164
+ /**
165
+ * Creates a deletion subscription manager
166
+ * @param _logger - Logger instance
167
+ * @returns {DeletionSubscriptionManager} Subscription manager
168
+ */
169
+ export function createDeletionSubscriptionManager(_logger) {
170
+ return {
171
+ subscribeToDeletions(eventIds) {
172
+ return {
173
+ subscriptionId: `deletion_${eventIds.join('_')}`,
174
+ filters: [{
175
+ kinds: [EVENT_DELETION_KIND],
176
+ '#e': eventIds
177
+ }]
178
+ };
179
+ },
180
+ subscribeToUserDeletions(pubkey) {
181
+ return {
182
+ subscriptionId: `user_deletion_${pubkey}`,
183
+ filters: [{
184
+ kinds: [EVENT_DELETION_KIND],
185
+ authors: [pubkey]
186
+ }]
187
+ };
188
+ }
189
+ };
190
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @file NIP-11: Relay Information Document
3
+ * @module nips/nip-11
4
+ * @see https://github.com/nostr-protocol/nips/blob/master/11.md
5
+ */
6
+ /**
7
+ * Relay information document structure
8
+ */
9
+ export interface RelayInformation {
10
+ name?: string;
11
+ description?: string;
12
+ pubkey?: string;
13
+ contact?: string;
14
+ supported_nips?: number[];
15
+ software?: string;
16
+ version?: string;
17
+ limitation?: {
18
+ max_message_length?: number;
19
+ max_subscriptions?: number;
20
+ max_filters?: number;
21
+ max_limit?: number;
22
+ max_subid_length?: number;
23
+ min_prefix?: number;
24
+ max_event_tags?: number;
25
+ max_content_length?: number;
26
+ min_pow_difficulty?: number;
27
+ auth_required?: boolean;
28
+ payment_required?: boolean;
29
+ };
30
+ }
31
+ /**
32
+ * Fetches relay information document
33
+ * @param url - Relay URL (ws:// or wss://)
34
+ * @returns {Promise<RelayInformation>} Relay information
35
+ */
36
+ export declare function getRelayInformation(url: string): Promise<RelayInformation>;
37
+ /**
38
+ * Checks if relay meets requirements
39
+ * @param relay - Relay information
40
+ * @param requirements - Required relay features
41
+ * @returns {boolean} True if relay meets all requirements
42
+ */
43
+ export declare function checkRelayRequirements(relay: RelayInformation, requirements: Partial<RelayInformation>): boolean;
44
+ /**
45
+ * Validates relay capabilities against required features
46
+ * @param info - Relay information
47
+ * @param requiredNips - Required NIPs
48
+ * @param requiredFeatures - Required relay features
49
+ * @returns {boolean} True if relay supports all required features
50
+ */
51
+ export declare function validateRelayCapabilities(info: RelayInformation, requiredNips?: number[], requiredFeatures?: Partial<RelayInformation['limitation']>): boolean;
52
+ /**
53
+ * Creates a relay selection score based on capabilities
54
+ * @param info - Relay information
55
+ * @param preferences - Scoring preferences
56
+ * @returns {number} Relay score (higher is better)
57
+ */
58
+ export declare function scoreRelayCapabilities(info: RelayInformation, preferences?: {
59
+ preferredNips?: number[];
60
+ minMessageLength?: number;
61
+ minSubscriptions?: number;
62
+ requireAuth?: boolean;
63
+ requirePayment?: boolean;
64
+ }): number;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * @file NIP-11: Relay Information Document
3
+ * @module nips/nip-11
4
+ * @see https://github.com/nostr-protocol/nips/blob/master/11.md
5
+ */
6
+ import { getLogger } from '../utils/logger';
7
+ import { fetchJson } from '../utils/http';
8
+ const logger = getLogger('NIP-11');
9
+ /**
10
+ * Fetches relay information document
11
+ * @param url - Relay URL (ws:// or wss://)
12
+ * @returns {Promise<RelayInformation>} Relay information
13
+ */
14
+ export async function getRelayInformation(url) {
15
+ try {
16
+ const relayUrl = url.replace('ws://', 'http://').replace('wss://', 'https://');
17
+ const response = await fetchJson(`${relayUrl}/.well-known/nostr.json`);
18
+ return response;
19
+ }
20
+ catch (error) {
21
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
22
+ logger.error(`Failed to fetch relay information: ${errorMessage}`);
23
+ throw new Error(`Failed to fetch relay information: ${errorMessage}`);
24
+ }
25
+ }
26
+ /**
27
+ * Checks if relay meets requirements
28
+ * @param relay - Relay information
29
+ * @param requirements - Required relay features
30
+ * @returns {boolean} True if relay meets all requirements
31
+ */
32
+ export function checkRelayRequirements(relay, requirements) {
33
+ try {
34
+ // Check supported NIPs
35
+ if (requirements.supported_nips && relay.supported_nips) {
36
+ const missingNips = requirements.supported_nips.filter(nip => !relay.supported_nips?.includes(nip));
37
+ if (missingNips.length > 0) {
38
+ logger.debug(`Missing required NIPs: ${missingNips.join(', ')}`);
39
+ return false;
40
+ }
41
+ }
42
+ // Check limitations
43
+ if (requirements.limitation && relay.limitation) {
44
+ for (const [key, value] of Object.entries(requirements.limitation)) {
45
+ const relayValue = relay.limitation[key];
46
+ // Skip if relay doesn't specify this limitation
47
+ if (relayValue === undefined)
48
+ continue;
49
+ // For maximum values, relay should support at least the required value
50
+ if (key.startsWith('max_') && typeof relayValue === 'number' && typeof value === 'number') {
51
+ if (relayValue < value) {
52
+ logger.debug(`Relay ${key} too low: ${relayValue} < ${value}`);
53
+ return false;
54
+ }
55
+ }
56
+ // For minimum values, relay should not require more than specified
57
+ if (key.startsWith('min_') && typeof relayValue === 'number' && typeof value === 'number') {
58
+ if (relayValue > value) {
59
+ logger.debug(`Relay ${key} too high: ${relayValue} > ${value}`);
60
+ return false;
61
+ }
62
+ }
63
+ // For boolean flags, values must match if specified
64
+ if (typeof relayValue === 'boolean' && typeof value === 'boolean') {
65
+ if (relayValue !== value) {
66
+ logger.debug(`Relay ${key} mismatch: ${relayValue} !== ${value}`);
67
+ return false;
68
+ }
69
+ }
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+ catch (error) {
75
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
76
+ logger.error(`Error checking relay requirements: ${errorMessage}`);
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * Validates relay capabilities against required features
82
+ * @param info - Relay information
83
+ * @param requiredNips - Required NIPs
84
+ * @param requiredFeatures - Required relay features
85
+ * @returns {boolean} True if relay supports all required features
86
+ */
87
+ export function validateRelayCapabilities(info, requiredNips = [], requiredFeatures = {}) {
88
+ // Check NIP support
89
+ if (requiredNips.length > 0) {
90
+ if (!info.supported_nips)
91
+ return false;
92
+ if (!requiredNips.every(nip => info.supported_nips?.includes(nip))) {
93
+ return false;
94
+ }
95
+ }
96
+ // Check limitations
97
+ if (info.limitation) {
98
+ for (const [key, value] of Object.entries(requiredFeatures)) {
99
+ const relayValue = info.limitation[key];
100
+ if (relayValue === undefined)
101
+ return false;
102
+ // For maximum values, relay limit should be higher
103
+ if (key.startsWith('max_') && typeof relayValue === 'number' && typeof value === 'number') {
104
+ if (relayValue < value)
105
+ return false;
106
+ }
107
+ // For minimum values, relay limit should be lower
108
+ if (key.startsWith('min_') && typeof relayValue === 'number' && typeof value === 'number') {
109
+ if (relayValue > value)
110
+ return false;
111
+ }
112
+ // For boolean flags, must match exactly
113
+ if (typeof relayValue === 'boolean' && typeof value === 'boolean') {
114
+ if (relayValue !== value)
115
+ return false;
116
+ }
117
+ }
118
+ }
119
+ return true;
120
+ }
121
+ /**
122
+ * Creates a relay selection score based on capabilities
123
+ * @param info - Relay information
124
+ * @param preferences - Scoring preferences
125
+ * @returns {number} Relay score (higher is better)
126
+ */
127
+ export function scoreRelayCapabilities(info, preferences = {}) {
128
+ let score = 0;
129
+ // Score NIP support
130
+ if (info.supported_nips && preferences.preferredNips) {
131
+ score += preferences.preferredNips.filter(nip => info.supported_nips?.includes(nip)).length * 10;
132
+ }
133
+ // Score limitations
134
+ if (info.limitation) {
135
+ const limits = info.limitation;
136
+ // Message length
137
+ if (limits.max_message_length && preferences.minMessageLength) {
138
+ score += limits.max_message_length >= preferences.minMessageLength ? 5 : -5;
139
+ }
140
+ // Subscriptions
141
+ if (limits.max_subscriptions && preferences.minSubscriptions) {
142
+ score += limits.max_subscriptions >= preferences.minSubscriptions ? 5 : -5;
143
+ }
144
+ // Auth requirements
145
+ if (preferences.requireAuth !== undefined) {
146
+ score += (limits.auth_required === preferences.requireAuth) ? 5 : -10;
147
+ }
148
+ // Payment requirements
149
+ if (preferences.requirePayment !== undefined) {
150
+ score += (limits.payment_required === preferences.requirePayment) ? 5 : -10;
151
+ }
152
+ }
153
+ return score;
154
+ }