ncc-05 1.1.5 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,120 +1,267 @@
1
1
  # ncc-05
2
2
 
3
- **Nostr Community Convention 05 - Identity-Bound Service Locator Resolution.**
4
-
5
- A TypeScript library for publishing and resolving dynamic, encrypted service endpoints (IP, Port, Onion) bound to cryptographic identities using Nostr `kind:30058` events.
6
-
7
- ## Features
8
-
9
- - **Identity-Centric**: Endpoints are bound to a Nostr Pubkey.
10
- - **Privacy-First**: NIP-44 encryption is mandatory by default.
11
- - **Multi-Recipient Support**: Implement "Wrapping" patterns to share endpoints with groups without sharing private keys.
12
- - **NIP-65 Gossip**: Built-in support for discovering a publisher's preferred relays.
13
- - **Tor Ready**: Easy integration with SOCKS5 proxies for anonymous resolution.
14
- - **Type Safe**: Fully typed with TypeScript.
3
+ Nostr Community Convention 05 - Identity-Bound Service Locator Resolution
15
4
 
16
5
  ## Installation
17
6
 
7
+ You can install this library using npm:
8
+
18
9
  ```bash
19
10
  npm install ncc-05
20
11
  ```
21
12
 
22
- ## Usage
13
+ ## Configuration
23
14
 
24
- ### 1. Basic Resolution (Self or Public)
15
+ ### Relays
25
16
 
26
- Resolve an identity-bound service locator for a given pubkey.
17
+ The `NCC05Resolver` uses a set of bootstrap relays to discover service locators. By default, it uses `['wss://relay.damus.io', 'wss://nos.lol']`. You can provide your own list of relays during initialization:
27
18
 
28
19
  ```typescript
29
20
  import { NCC05Resolver } from 'ncc-05';
30
- import { nip19 } from 'nostr-tools';
31
21
 
32
- const resolver = new NCC05Resolver();
22
+ const customRelays = ['wss://relay.example.com', 'wss://another.relay.io'];
23
+ const resolver = new NCC05Resolver({ bootstrapRelays: customRelays });
24
+ ```
33
25
 
34
- // Resolve using an npub (or hex pubkey)
35
- const target = 'npub1...';
36
- const mySecretKey = ...; // Uint8Array needed for encrypted records
26
+ The `NCC05Publisher` requires you to specify the relays to which you want to publish events for each `publish` or `publishWrapped` call:
37
27
 
38
- const payload = await resolver.resolve(target, mySecretKey, 'addr', {
39
- gossip: true, // Follow NIP-65 hints
40
- strict: true // Reject expired records
41
- });
28
+ ```typescript
29
+ import { NCC05Publisher } from 'ncc-05';
42
30
 
43
- if (payload) {
44
- console.log('Resolved Endpoints:', payload.endpoints);
45
- }
31
+ const publisher = new NCC05Publisher();
32
+ const relaysToPublishTo = ['wss://relay.example.com'];
33
+ // ... then call publisher.publish(relaysToPublishTo, ...)
46
34
  ```
47
35
 
48
- ### 2. Targeted Encryption (Friend-to-Friend)
36
+ ### Shared SimplePool
49
37
 
50
- Alice publishes a record that only Bob can decrypt.
38
+ Both `NCC05Resolver` and `NCC05Publisher` can optionally share a `nostr-tools` `SimplePool` instance. This is useful for managing relay connections more efficiently across your application.
51
39
 
52
40
  ```typescript
53
- import { NCC05Publisher } from 'ncc-05';
41
+ import { SimplePool } from 'nostr-tools';
42
+ import { NCC05Resolver, NCC05Publisher } from 'ncc-05';
54
43
 
55
- const publisher = new NCC05Publisher();
56
- const AliceSK = ...;
57
- const BobPK = "..."; // Bob's hex pubkey
44
+ const pool = new SimplePool();
58
45
 
59
- await publisher.publish(relays, AliceSK, payload, {
60
- identifier: 'for-bob',
61
- recipientPubkey: BobPK // Encrypts specifically for Bob
62
- });
46
+ const resolver = new NCC05Resolver({ pool });
47
+ const publisher = new NCC05Publisher({ pool });
48
+
49
+ // Remember to close the pool when done if you created it externally
50
+ // pool.close();
63
51
  ```
64
52
 
65
- ### 3. Group Wrapping (One Event, Many Recipients)
53
+ ## Usage
54
+
55
+ This library is designed for both resolving and publishing identity-bound service locators.
56
+
57
+ ### 1. Resolving a Service Locator
66
58
 
67
- Alice shares her endpoint with a list of authorized friends in a single event.
59
+ You can resolve a service locator for a user's identity using the `NCC05Resolver`. This involves specifying the target user's public key, your (optional) secret key for decryption, and a service identifier.
68
60
 
69
61
  ```typescript
70
- await publisher.publishWrapped(
71
- relays,
72
- AliceSK,
73
- [BobPK, CharliePK, DavePK],
74
- payload,
75
- 'private-group'
76
- );
77
-
78
- // Bob resolves it using his own key:
79
- const payload = await resolver.resolve(AlicePK, BobSK, 'private-group');
62
+ import { NCC05Resolver, NCC05Payload, NCC05TimeoutError } from 'ncc-05';
63
+ import { SimplePool } from 'nostr-tools';
64
+
65
+ // Optional: Share an existing connection pool to manage relay connections
66
+ const pool = new SimplePool();
67
+ const resolver = new NCC05Resolver({
68
+ pool,
69
+ timeout: 10000 // Custom timeout in milliseconds (default is 10000)
70
+ });
71
+
72
+ // The target user's public key (can be 'npub1...' (bech32 encoded) or a hex string)
73
+ const targetPubkey = 'npub1w9...';
74
+
75
+ // Your secret key (hex string or Uint8Array) is required if the record is encrypted for you.
76
+ // If the record is public or encrypted for the targetPubkey itself, your secretKey is not strictly needed.
77
+ const mySecretKey = "your_hex_secret_key";
78
+
79
+ // The 'd' tag identifier for the service (default is 'addr')
80
+ const serviceIdentifier = 'chat.example.com';
81
+
82
+ try {
83
+ const payload: NCC05Payload | null = await resolver.resolve(targetPubkey, mySecretKey, serviceIdentifier, {
84
+ gossip: true, // Attempt NIP-65 relay discovery
85
+ strict: false // If true, expired records return null; otherwise, a warning is logged.
86
+ });
87
+
88
+ if (payload) {
89
+ console.log('Resolved Service Locator Payload:', payload);
90
+ // Example output structure:
91
+ // {
92
+ // v: 1,
93
+ // ttl: 3600,
94
+ // updated_at: 1678886400,
95
+ // endpoints: [
96
+ // { type: 'tcp', uri: '192.168.1.100:8080', priority: 10, family: 'ipv4' },
97
+ // { type: 'onion', uri: 'vww6y4qj7y3t45b5.onion:443', priority: 20, family: 'onion' }
98
+ // ],
99
+ // caps: ['auth', 'upload'],
100
+ // notes: 'Main chat service instance'
101
+ // }
102
+ } else {
103
+ console.log('Service locator not found or could not be resolved.');
104
+ }
105
+ } catch (e) {
106
+ if (e instanceof NCC05TimeoutError) {
107
+ console.error('Resolution timed out:', e.message);
108
+ } else {
109
+ console.error('An error occurred during resolution:', e);
110
+ }
111
+ } finally {
112
+ // It's good practice to close the resolver when you're done if it created its own pool.
113
+ // If you passed an external pool, you manage its lifecycle.
114
+ resolver.close();
115
+ }
116
+
117
+ // Close the external pool if you instantiated it.
118
+ // pool.close();
80
119
  ```
81
120
 
82
- ### 4. Tor & Privacy (Node.js)
121
+ ### 2. Publishing a Service Locator
83
122
 
84
- Route all Nostr relay traffic through a local Tor proxy (`127.0.0.1:9050`).
123
+ You can publish a service locator using the `NCC05Publisher`. This allows you to broadcast your service's endpoints to the Nostr network, either publicly, encrypted for yourself, or encrypted for a specific recipient.
85
124
 
86
125
  ```typescript
87
- import { SocksProxyAgent } from 'socks-proxy-agent';
88
- import { WebSocket } from 'ws';
126
+ import { NCC05Publisher, NCC05Payload } from 'ncc-05';
127
+ import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
89
128
 
90
- class TorWebSocket extends WebSocket {
91
- constructor(address: string, protocols?: string | string[]) {
92
- const agent = new SocksProxyAgent('socks5h://127.0.0.1:9050');
93
- super(address, protocols, { agent });
94
- }
129
+ const publisher = new NCC05Publisher();
130
+ const relaysToPublishTo = ['wss://relay.damus.io', 'wss://relay.nostr.band'];
131
+
132
+ // Your secret key (should be a hex string or Uint8Array)
133
+ const publisherSecretKey = 'your_publisher_secret_key_hex';
134
+
135
+ const servicePayload: NCC05Payload = {
136
+ v: 1, // Payload version
137
+ ttl: 3600, // Time-to-live in seconds
138
+ updated_at: Math.floor(Date.now() / 1000), // Current timestamp
139
+ endpoints: [
140
+ { type: 'tcp', uri: '1.2.3.4:8080', priority: 10, family: 'ipv4' },
141
+ { type: 'http', uri: 'https://myservice.com', priority: 5, family: 'ipv4' }
142
+ ],
143
+ caps: ['login', 'status'], // Optional capabilities
144
+ notes: 'My primary service instance' // Optional notes
145
+ };
146
+
147
+ // --- Publish a public record (not encrypted) ---
148
+ try {
149
+ const publicEvent = await publisher.publish(relaysToPublishTo, publisherSecretKey, servicePayload, {
150
+ identifier: 'my-public-service',
151
+ public: true // Set to true for public, unencrypted records
152
+ });
153
+ console.log('Public record published:', publicEvent.id);
154
+ } catch (e) {
155
+ console.error('Failed to publish public record:', e);
95
156
  }
96
157
 
97
- const resolver = new NCC05Resolver({
98
- websocketImplementation: TorWebSocket
99
- });
158
+ // --- Publish a record encrypted for yourself (self-encrypted) ---
159
+ try {
160
+ // This record can only be decrypted by the publisherSecretKey
161
+ const selfEncryptedEvent = await publisher.publish(relaysToPublishTo, publisherSecretKey, servicePayload, {
162
+ identifier: 'my-private-service',
163
+ // public: false is default, recipientPubkey also defaults to publisher's pubkey
164
+ });
165
+ console.log('Self-encrypted record published:', selfEncryptedEvent.id);
166
+ } catch (e) {
167
+ console.error('Failed to publish self-encrypted record:', e);
168
+ }
169
+
170
+ // --- Publish a record encrypted for a specific recipient ---
171
+ try {
172
+ const recipientPubkey = getPublicKey(generateSecretKey()); // Example recipient
173
+ const recipientEncryptedEvent = await publisher.publish(relaysToPublishTo, publisherSecretKey, servicePayload, {
174
+ identifier: 'service-for-friend',
175
+ recipientPubkey: recipientPubkey // Encrypts content for this recipient
176
+ });
177
+ console.log('Recipient-encrypted record published:', recipientEncryptedEvent.id);
178
+ } catch (e) {
179
+ console.error('Failed to publish recipient-encrypted record:', e);
180
+ }
181
+
182
+ // --- Publish a record wrapped for multiple recipients ---
183
+ try {
184
+ const groupMembers = [
185
+ getPublicKey(generateSecretKey()), // Member 1
186
+ getPublicKey(generateSecretKey()), // Member 2
187
+ ];
188
+ const wrappedEvent = await publisher.publishWrapped(relaysToPublishTo, publisherSecretKey, groupMembers, servicePayload, 'shared-service');
189
+ console.log('Wrapped record published for multiple recipients:', wrappedEvent.id);
190
+ } catch (e) {
191
+ console.error('Failed to publish wrapped record:', e);
192
+ } finally {
193
+ publisher.close(relaysToPublishTo); // Close connections used by this publisher instance
194
+ }
100
195
  ```
101
196
 
197
+ ## Error Handling
198
+
199
+ The library exports specific error classes for granular handling:
200
+ - `NCC05Error`: Base class for all NCC-05 specific errors.
201
+ - `NCC05RelayError`: Communication failure with Nostr relays.
202
+ - `NCC05TimeoutError`: Operation exceeded the specified timeout.
203
+ - `NCC05DecryptionError`: Failed to decrypt the record (invalid keys or content).
204
+ - `NCC05ArgumentError`: Invalid arguments provided (e.g., malformed keys).
205
+
206
+ ## Description
207
+
208
+ This library implements Nostr Community Convention 05 (NCC-05) for identity-bound service locator resolution. NCC-05 defines a standard way for Nostr identities to publish and discover dynamic service endpoints (like IP addresses, ports, or Tor .onion addresses) using Nostr kind `30058` events. This allows applications to resolve a standardized service locator for a given user's Nostr public key and a specified service identifier.
209
+
210
+ It integrates with [NIP-05 (Nostr Identity)](https://github.com/nostr-protocol/nips/blob/master/05.md) by allowing resolution based on NIP-05 verified identities and leveraging NIP-65 for relay discovery.
211
+
212
+ ## How it works
213
+
214
+ NCC-05 leverages Nostr to store and retrieve service locator records. When `resolve` is called, the library performs the following steps:
215
+
216
+ 1. **Identity Resolution**: Converts `npub` formatted public keys to their hexadecimal representation.
217
+ 2. **Relay Discovery (Optional NIP-65)**: If enabled, attempts to discover additional relays from the target user's NIP-65 (kind `10002`) event, ensuring a broader search for their records.
218
+ 3. **Querying for `kind:30058` records**: It queries known Nostr relays (bootstrap and potentially NIP-65 discovered) for `kind:30058` events authored by the target public key and matching the provided 'd' tag identifier.
219
+ 4. **Filtering and Validation**: Discovered records are filtered for valid signatures and the correct author. The latest valid record is selected.
220
+ 5. **Decryption**: If the content is encrypted (using NIP-44), it attempts to decrypt it using the provided secret key. It supports both single-recipient (self-encrypted or targeted) and multi-recipient "wrapped" encryption patterns.
221
+ 6. **Payload Parsing and Validation**: The decrypted content is parsed as an `NCC05Payload` and validated for structural integrity and freshness (checking `ttl` against `updated_at`).
222
+ 7. **Resolution**: The most appropriate and valid `NCC05Payload` is returned.
223
+
224
+ ## Tor & Privacy (Onion Services)
225
+ ...
102
226
  ## API Reference
103
227
 
104
228
  ### `NCC05Resolver`
105
- - `resolve(targetPubkey, secretKey?, identifier?, options?)`: Finds and decrypts a locator record.
106
- - `close()`: Closes pool connections.
229
+ - `new NCC05Resolver(options?: ResolverOptions)`: Constructor.
230
+ - `resolve(targetPubkey: string, secretKey?: string | Uint8Array, identifier: string = 'addr', options?: { strict?: boolean, gossip?: boolean }): Promise<NCC05Payload | null>`: Finds and decrypts a locator record.
231
+ - `close(): void`: Closes pool connections.
107
232
 
108
233
  ### `NCC05Publisher`
109
- - `publish(relays, secretKey, payload, options?)`: Publishes a standard or targeted record.
110
- - `publishWrapped(relays, secretKey, recipients, payload, identifier?)`: Publishes a multi-recipient record.
234
+ - `new NCC05Publisher(options?: PublisherOptions)`: Constructor.
235
+ - `publish(relays: string[], secretKey: string | Uint8Array, payload: NCC05Payload, options?: { identifier?: string, recipientPubkey?: string, public?: boolean }): Promise<Event>`: Publishes a standard or targeted record.
236
+ - `publishWrapped(relays: string[], secretKey: string | Uint8Array, recipients: string[], payload: NCC05Payload, identifier: string = 'addr'): Promise<Event>`: Publishes a multi-recipient record.
237
+ - `close(relays: string[]): void`: Closes connections to specified relays.
111
238
 
112
239
  ### `NCC05Group`
113
240
  - `createGroupIdentity()`: Generates a shared group keypair.
114
241
  - `resolveAsGroup(...)`: Helper for shared-nsec resolution.
115
242
 
116
- ## License
243
+ ### Interfaces
244
+
245
+ #### `NCC05Payload`
117
246
 
247
+ The structure of the resolved service locator data:
118
248
 
249
+ ```typescript
250
+ export interface NCC05Payload {
251
+ v: number; // Payload version (currently 1)
252
+ ttl: number; // Time-to-live in seconds
253
+ updated_at: number; // Unix timestamp of the last update
254
+ endpoints: NCC05Endpoint[];// List of available endpoints
255
+ caps?: string[]; // Optional capability identifiers supported by the service
256
+ notes?: string; // Optional human-readable notes
257
+ }
258
+
259
+ export interface NCC05Endpoint {
260
+ type: 'tcp' | 'udp' | string; // Protocol type, e.g., 'tcp', 'udp', 'http'
261
+ uri: string; // The URI string, e.g., '1.2.3.4:8080' or '[2001:db8::1]:9000'
262
+ priority: number; // Priority for selection (lower is higher priority)
263
+ family: 'ipv4' | 'ipv6' | 'onion' | string; // Network family for routing hints
264
+ }
265
+ ```
119
266
 
120
- CC0 1.0 Universal
267
+ ## License
package/dist/index.d.ts CHANGED
@@ -60,8 +60,6 @@ export interface ResolverOptions {
60
60
  bootstrapRelays?: string[];
61
61
  /** Timeout for relay queries in milliseconds (default: 10000) */
62
62
  timeout?: number;
63
- /** Custom WebSocket implementation (e.g., for Tor/SOCKS5 in Node.js) */
64
- websocketImplementation?: any;
65
63
  /** Existing SimplePool instance to share connections */
66
64
  pool?: SimplePool;
67
65
  }
@@ -69,8 +67,6 @@ export interface ResolverOptions {
69
67
  * Options for configuring the NCC05Publisher.
70
68
  */
71
69
  export interface PublisherOptions {
72
- /** Custom WebSocket implementation */
73
- websocketImplementation?: any;
74
70
  /** Existing SimplePool instance */
75
71
  pool?: SimplePool;
76
72
  /** Timeout for publishing in milliseconds (default: 5000) */
@@ -118,6 +114,7 @@ export declare class NCC05Group {
118
114
  */
119
115
  export declare class NCC05Resolver {
120
116
  private pool;
117
+ private _ownPool;
121
118
  private bootstrapRelays;
122
119
  private timeout;
123
120
  /**
@@ -143,7 +140,7 @@ export declare class NCC05Resolver {
143
140
  gossip?: boolean;
144
141
  }): Promise<NCC05Payload | null>;
145
142
  /**
146
- * Closes connections to all relays in the pool.
143
+ * Closes connections to all relays in the pool if managed internally.
147
144
  */
148
145
  close(): void;
149
146
  }
@@ -152,6 +149,7 @@ export declare class NCC05Resolver {
152
149
  */
153
150
  export declare class NCC05Publisher {
154
151
  private pool;
152
+ private _ownPool;
155
153
  private timeout;
156
154
  /**
157
155
  * @param options - Configuration for the publisher.
@@ -185,7 +183,7 @@ export declare class NCC05Publisher {
185
183
  public?: boolean;
186
184
  }): Promise<Event>;
187
185
  /**
188
- * Closes connections to the specified relays.
186
+ * Closes connections to the specified relays if managed internally.
189
187
  */
190
188
  close(relays: string[]): void;
191
189
  }
package/dist/index.js CHANGED
@@ -51,12 +51,6 @@ function ensureUint8Array(key) {
51
51
  }
52
52
  throw new NCC05ArgumentError("Key must be a hex string or Uint8Array");
53
53
  }
54
- function getHexPubkey(key) {
55
- if (key instanceof Uint8Array) {
56
- return Array.from(key).map(b => b.toString(16).padStart(2, '0')).join('');
57
- }
58
- return key;
59
- }
60
54
  /**
61
55
  * Utility for managing shared group access to service records.
62
56
  */
@@ -95,24 +89,15 @@ export class NCC05Group {
95
89
  */
96
90
  export class NCC05Resolver {
97
91
  pool;
92
+ _ownPool;
98
93
  bootstrapRelays;
99
94
  timeout;
100
95
  /**
101
96
  * @param options - Configuration for the resolver.
102
97
  */
103
98
  constructor(options = {}) {
99
+ this._ownPool = !options.pool;
104
100
  this.pool = options.pool || new SimplePool();
105
- if (!options.pool) {
106
- if (options.websocketImplementation) {
107
- // @ts-ignore - Patching pool for custom transport
108
- this.pool.websocketImplementation = options.websocketImplementation;
109
- }
110
- else if (typeof globalThis !== 'undefined' && !globalThis.WebSocket) {
111
- // In Node.js environment without global WebSocket, this might fail later.
112
- // We leave it to the user or nostr-tools to handle, but this logic
113
- // allows 'websocketImplementation' to be explicitly checked.
114
- }
115
- }
116
101
  this.bootstrapRelays = options.bootstrapRelays || ['wss://relay.damus.io', 'wss://nos.lol'];
117
102
  this.timeout = options.timeout || 10000;
118
103
  }
@@ -201,7 +186,7 @@ export class NCC05Resolver {
201
186
  return null; // Not intended for us
202
187
  }
203
188
  }
204
- catch (e) {
189
+ catch (_e) {
205
190
  throw new NCC05DecryptionError("Failed to decrypt wrapped content");
206
191
  }
207
192
  }
@@ -211,7 +196,7 @@ export class NCC05Resolver {
211
196
  const conversationKey = nip44.getConversationKey(sk, hexPubkey);
212
197
  content = nip44.decrypt(latestEvent.content, conversationKey);
213
198
  }
214
- catch (e) {
199
+ catch (_e) {
215
200
  throw new NCC05DecryptionError("Failed to decrypt content");
216
201
  }
217
202
  }
@@ -220,7 +205,7 @@ export class NCC05Resolver {
220
205
  try {
221
206
  payload = JSON.parse(content);
222
207
  }
223
- catch (e) {
208
+ catch (_e) {
224
209
  return null; // Invalid JSON
225
210
  }
226
211
  if (!payload || !payload.endpoints || !Array.isArray(payload.endpoints)) {
@@ -242,14 +227,12 @@ export class NCC05Resolver {
242
227
  }
243
228
  }
244
229
  /**
245
- * Closes connections to all relays in the pool.
230
+ * Closes connections to all relays in the pool if managed internally.
246
231
  */
247
232
  close() {
248
- // If we didn't create the pool, we probably shouldn't close it?
249
- // But the previous implementation did.
250
- // We will only close bootstrap relays to be safe if sharing pool.
251
- // Actually, pool.close() takes args.
252
- this.pool.close(this.bootstrapRelays);
233
+ if (this._ownPool) {
234
+ this.pool.close(this.bootstrapRelays);
235
+ }
253
236
  }
254
237
  }
255
238
  /**
@@ -257,16 +240,14 @@ export class NCC05Resolver {
257
240
  */
258
241
  export class NCC05Publisher {
259
242
  pool;
243
+ _ownPool;
260
244
  timeout;
261
245
  /**
262
246
  * @param options - Configuration for the publisher.
263
247
  */
264
248
  constructor(options = {}) {
249
+ this._ownPool = !options.pool;
265
250
  this.pool = options.pool || new SimplePool();
266
- if (!options.pool && options.websocketImplementation) {
267
- // @ts-ignore
268
- this.pool.websocketImplementation = options.websocketImplementation;
269
- }
270
251
  this.timeout = options.timeout || 5000;
271
252
  }
272
253
  async _publishToRelays(relays, signedEvent) {
@@ -361,9 +342,11 @@ export class NCC05Publisher {
361
342
  return signedEvent;
362
343
  }
363
344
  /**
364
- * Closes connections to the specified relays.
345
+ * Closes connections to the specified relays if managed internally.
365
346
  */
366
347
  close(relays) {
367
- this.pool.close(relays);
348
+ if (this._ownPool) {
349
+ this.pool.close(relays);
350
+ }
368
351
  }
369
352
  }
@@ -1,6 +1,10 @@
1
+ import { WebSocket } from 'ws';
1
2
  export declare class MockRelay {
3
+ private static instance;
2
4
  private wss;
3
5
  private events;
4
6
  constructor(port?: number);
5
7
  stop(): void;
8
+ static getNostrConnections(): WebSocket[];
9
+ static closeAllClientConnections(): void;
6
10
  }
@@ -1,9 +1,11 @@
1
- import { WebSocketServer } from 'ws';
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
2
  export class MockRelay {
3
+ static instance;
3
4
  wss;
4
5
  events = [];
5
6
  constructor(port = 8080) {
6
7
  this.wss = new WebSocketServer({ port });
8
+ MockRelay.instance = this;
7
9
  this.wss.on('connection', (ws) => {
8
10
  ws.on('message', (data) => {
9
11
  const msg = JSON.parse(data);
@@ -44,4 +46,19 @@ export class MockRelay {
44
46
  stop() {
45
47
  this.wss.close();
46
48
  }
49
+ static getNostrConnections() {
50
+ if (!MockRelay.instance) {
51
+ return [];
52
+ }
53
+ return Array.from(MockRelay.instance.wss.clients);
54
+ }
55
+ static closeAllClientConnections() {
56
+ if (MockRelay.instance) {
57
+ MockRelay.instance.wss.clients.forEach(client => {
58
+ if (client.readyState === WebSocket.OPEN) {
59
+ client.close();
60
+ }
61
+ });
62
+ }
63
+ }
47
64
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { NCC05Resolver } from './index.js';
2
+ import { SimplePool } from 'nostr-tools';
3
+ async function testLifecycle() {
4
+ console.log('--- Starting Lifecycle Test ---');
5
+ // 1. Internal Pool Management
6
+ console.log('Test 1: Internal Pool (should close)');
7
+ const resolverInternal = new NCC05Resolver();
8
+ // @ts-ignore - Access private property for testing or infer from behavior
9
+ const internalPool = resolverInternal['pool'];
10
+ internalPool.close = (_relays) => {
11
+ console.log('Internal pool close called.');
12
+ };
13
+ resolverInternal.close(); // Should log
14
+ // 2. Shared Pool Management
15
+ console.log('Test 2: Shared Pool (should NOT close)');
16
+ const sharedPool = new SimplePool();
17
+ let sharedClosed = false;
18
+ sharedPool.close = (_relays) => {
19
+ sharedClosed = true;
20
+ console.error('ERROR: Shared pool was closed!');
21
+ };
22
+ const resolverShared = new NCC05Resolver({ pool: sharedPool });
23
+ resolverShared.close(); // Should NOT close sharedPool
24
+ if (!sharedClosed) {
25
+ console.log('Shared pool correctly remained open.');
26
+ }
27
+ else {
28
+ process.exit(1);
29
+ }
30
+ console.log('Lifecycle Test Suite Passed.');
31
+ process.exit(0);
32
+ }
33
+ testLifecycle().catch(console.error);
package/dist/test-new.js CHANGED
@@ -1,4 +1,4 @@
1
- import { NCC05Publisher, NCC05Resolver } from './index.js';
1
+ import { NCC05Publisher, NCC05Resolver, NCC05TimeoutError } from './index.js';
2
2
  import { generateSecretKey, getPublicKey, SimplePool } from 'nostr-tools';
3
3
  import { MockRelay } from './mock-relay.js';
4
4
  import { WebSocketServer } from 'ws';
@@ -73,7 +73,7 @@ async function testNewFeatures() {
73
73
  }
74
74
  catch (e) {
75
75
  const duration = Date.now() - start;
76
- if (e.name === 'NCC05TimeoutError') {
76
+ if (e instanceof NCC05TimeoutError) {
77
77
  console.log(`Timed out as expected in ${duration}ms: OK`);
78
78
  }
79
79
  else {
@@ -0,0 +1,25 @@
1
+ import globals from "globals";
2
+ import pluginJs from "@eslint/js";
3
+ import tseslint from "typescript-eslint";
4
+
5
+ export default [
6
+ {files: ["**/*.{js,mjs,cjs,ts}"]},
7
+ {languageOptions: { globals: globals.node }},
8
+ pluginJs.configs.recommended,
9
+ ...tseslint.configs.recommended,
10
+ {
11
+ rules: {
12
+ "@typescript-eslint/no-explicit-any": "off",
13
+ "@typescript-eslint/ban-ts-comment": "off",
14
+ "@typescript-eslint/no-unused-vars": ["warn", {
15
+ "argsIgnorePattern": "^_",
16
+ "varsIgnorePattern": "^_",
17
+ "caughtErrorsIgnorePattern": "^_"
18
+ }],
19
+ "no-console": "off"
20
+ }
21
+ },
22
+ {
23
+ ignores: ["dist/", "node_modules/"]
24
+ }
25
+ ];
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "ncc-05",
3
- "version": "1.1.5",
3
+ "version": "1.1.8",
4
4
  "description": "Nostr Community Convention 05 - Identity-Bound Service Locator Resolution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "type": "module",
8
8
  "scripts": {
9
9
  "build": "tsc",
10
+ "lint": "eslint src/**/*.ts",
10
11
  "prepublishOnly": "npm run build"
11
12
  },
12
13
  "keywords": [
@@ -22,9 +23,15 @@
22
23
  "nostr-tools": "^2.10.0"
23
24
  },
24
25
  "devDependencies": {
26
+ "@eslint/js": "^9.39.2",
25
27
  "@types/node": "^25.0.3",
26
28
  "@types/ws": "^8.18.1",
29
+ "@typescript-eslint/eslint-plugin": "^8.50.1",
30
+ "@typescript-eslint/parser": "^8.50.1",
31
+ "eslint": "^9.39.2",
32
+ "globals": "^16.5.0",
27
33
  "typescript": "^5.0.0",
34
+ "typescript-eslint": "^8.50.1",
28
35
  "ws": "^8.18.3"
29
36
  }
30
37
  }
package/src/index.ts CHANGED
@@ -69,13 +69,6 @@ function ensureUint8Array(key: string | Uint8Array): Uint8Array {
69
69
  throw new NCC05ArgumentError("Key must be a hex string or Uint8Array");
70
70
  }
71
71
 
72
- function getHexPubkey(key: string | Uint8Array): string {
73
- if (key instanceof Uint8Array) {
74
- return Array.from(key).map(b => b.toString(16).padStart(2, '0')).join('');
75
- }
76
- return key;
77
- }
78
-
79
72
  /**
80
73
  * Represents a single reachable service endpoint.
81
74
  */
@@ -116,8 +109,6 @@ export interface ResolverOptions {
116
109
  bootstrapRelays?: string[];
117
110
  /** Timeout for relay queries in milliseconds (default: 10000) */
118
111
  timeout?: number;
119
- /** Custom WebSocket implementation (e.g., for Tor/SOCKS5 in Node.js) */
120
- websocketImplementation?: any;
121
112
  /** Existing SimplePool instance to share connections */
122
113
  pool?: SimplePool;
123
114
  }
@@ -126,8 +117,6 @@ export interface ResolverOptions {
126
117
  * Options for configuring the NCC05Publisher.
127
118
  */
128
119
  export interface PublisherOptions {
129
- /** Custom WebSocket implementation */
130
- websocketImplementation?: any;
131
120
  /** Existing SimplePool instance */
132
121
  pool?: SimplePool;
133
122
  /** Timeout for publishing in milliseconds (default: 5000) */
@@ -190,6 +179,7 @@ export class NCC05Group {
190
179
  */
191
180
  export class NCC05Resolver {
192
181
  private pool: SimplePool;
182
+ private _ownPool: boolean;
193
183
  private bootstrapRelays: string[];
194
184
  private timeout: number;
195
185
 
@@ -197,19 +187,8 @@ export class NCC05Resolver {
197
187
  * @param options - Configuration for the resolver.
198
188
  */
199
189
  constructor(options: ResolverOptions = {}) {
190
+ this._ownPool = !options.pool;
200
191
  this.pool = options.pool || new SimplePool();
201
-
202
- if (!options.pool) {
203
- if (options.websocketImplementation) {
204
- // @ts-ignore - Patching pool for custom transport
205
- this.pool.websocketImplementation = options.websocketImplementation;
206
- } else if (typeof globalThis !== 'undefined' && !globalThis.WebSocket) {
207
- // In Node.js environment without global WebSocket, this might fail later.
208
- // We leave it to the user or nostr-tools to handle, but this logic
209
- // allows 'websocketImplementation' to be explicitly checked.
210
- }
211
- }
212
-
213
192
  this.bootstrapRelays = options.bootstrapRelays || ['wss://relay.damus.io', 'wss://nos.lol'];
214
193
  this.timeout = options.timeout || 10000;
215
194
  }
@@ -320,7 +299,7 @@ export class NCC05Resolver {
320
299
  } else {
321
300
  return null; // Not intended for us
322
301
  }
323
- } catch (e) {
302
+ } catch (_e) {
324
303
  throw new NCC05DecryptionError("Failed to decrypt wrapped content");
325
304
  }
326
305
  } else if (sk && !content.startsWith('{')) {
@@ -328,7 +307,7 @@ export class NCC05Resolver {
328
307
  try {
329
308
  const conversationKey = nip44.getConversationKey(sk, hexPubkey);
330
309
  content = nip44.decrypt(latestEvent.content, conversationKey);
331
- } catch (e) {
310
+ } catch (_e) {
332
311
  throw new NCC05DecryptionError("Failed to decrypt content");
333
312
  }
334
313
  }
@@ -337,7 +316,7 @@ export class NCC05Resolver {
337
316
  let payload: NCC05Payload;
338
317
  try {
339
318
  payload = JSON.parse(content) as NCC05Payload;
340
- } catch (e) {
319
+ } catch (_e) {
341
320
  return null; // Invalid JSON
342
321
  }
343
322
 
@@ -360,14 +339,12 @@ export class NCC05Resolver {
360
339
  }
361
340
 
362
341
  /**
363
- * Closes connections to all relays in the pool.
342
+ * Closes connections to all relays in the pool if managed internally.
364
343
  */
365
344
  close() {
366
- // If we didn't create the pool, we probably shouldn't close it?
367
- // But the previous implementation did.
368
- // We will only close bootstrap relays to be safe if sharing pool.
369
- // Actually, pool.close() takes args.
370
- this.pool.close(this.bootstrapRelays);
345
+ if (this._ownPool) {
346
+ this.pool.close(this.bootstrapRelays);
347
+ }
371
348
  }
372
349
  }
373
350
 
@@ -376,17 +353,15 @@ export class NCC05Resolver {
376
353
  */
377
354
  export class NCC05Publisher {
378
355
  private pool: SimplePool;
356
+ private _ownPool: boolean;
379
357
  private timeout: number;
380
358
 
381
359
  /**
382
360
  * @param options - Configuration for the publisher.
383
361
  */
384
362
  constructor(options: PublisherOptions = {}) {
363
+ this._ownPool = !options.pool;
385
364
  this.pool = options.pool || new SimplePool();
386
- if (!options.pool && options.websocketImplementation) {
387
- // @ts-ignore
388
- this.pool.websocketImplementation = options.websocketImplementation;
389
- }
390
365
  this.timeout = options.timeout || 5000;
391
366
  }
392
367
 
@@ -508,9 +483,11 @@ export class NCC05Publisher {
508
483
  }
509
484
 
510
485
  /**
511
- * Closes connections to the specified relays.
486
+ * Closes connections to the specified relays if managed internally.
512
487
  */
513
488
  close(relays: string[]) {
514
- this.pool.close(relays);
489
+ if (this._ownPool) {
490
+ this.pool.close(relays);
491
+ }
515
492
  }
516
493
  }
package/src/mock-relay.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { WebSocketServer, WebSocket } from 'ws';
2
2
 
3
3
  export class MockRelay {
4
+ private static instance: MockRelay;
4
5
  private wss: WebSocketServer;
5
6
  private events: any[] = [];
6
7
 
7
8
  constructor(port: number = 8080) {
8
9
  this.wss = new WebSocketServer({ port });
10
+ MockRelay.instance = this;
9
11
  this.wss.on('connection', (ws: WebSocket) => {
10
12
  ws.on('message', (data: string) => {
11
13
  const msg = JSON.parse(data);
@@ -48,4 +50,21 @@ export class MockRelay {
48
50
  stop() {
49
51
  this.wss.close();
50
52
  }
53
+
54
+ public static getNostrConnections(): WebSocket[] {
55
+ if (!MockRelay.instance) {
56
+ return [];
57
+ }
58
+ return Array.from(MockRelay.instance.wss.clients);
59
+ }
60
+
61
+ public static closeAllClientConnections() {
62
+ if (MockRelay.instance) {
63
+ MockRelay.instance.wss.clients.forEach(client => {
64
+ if (client.readyState === WebSocket.OPEN) {
65
+ client.close();
66
+ }
67
+ });
68
+ }
69
+ }
51
70
  }
@@ -0,0 +1,40 @@
1
+ import { NCC05Resolver } from './index.js';
2
+ import { SimplePool } from 'nostr-tools';
3
+
4
+ async function testLifecycle() {
5
+ console.log('--- Starting Lifecycle Test ---');
6
+
7
+ // 1. Internal Pool Management
8
+ console.log('Test 1: Internal Pool (should close)');
9
+ const resolverInternal = new NCC05Resolver();
10
+ // @ts-ignore - Access private property for testing or infer from behavior
11
+ const internalPool = resolverInternal['pool'];
12
+ internalPool.close = (_relays?: string[]) => {
13
+ console.log('Internal pool close called.');
14
+ };
15
+
16
+ resolverInternal.close(); // Should log
17
+
18
+ // 2. Shared Pool Management
19
+ console.log('Test 2: Shared Pool (should NOT close)');
20
+ const sharedPool = new SimplePool();
21
+ let sharedClosed = false;
22
+ sharedPool.close = (_relays?: string[]) => {
23
+ sharedClosed = true;
24
+ console.error('ERROR: Shared pool was closed!');
25
+ };
26
+
27
+ const resolverShared = new NCC05Resolver({ pool: sharedPool });
28
+ resolverShared.close(); // Should NOT close sharedPool
29
+
30
+ if (!sharedClosed) {
31
+ console.log('Shared pool correctly remained open.');
32
+ } else {
33
+ process.exit(1);
34
+ }
35
+
36
+ console.log('Lifecycle Test Suite Passed.');
37
+ process.exit(0);
38
+ }
39
+
40
+ testLifecycle().catch(console.error);
package/src/test-new.ts CHANGED
@@ -76,9 +76,9 @@ async function testNewFeatures() {
76
76
  console.error('Should have timed out!');
77
77
  wss.close();
78
78
  process.exit(1);
79
- } catch (e: any) {
79
+ } catch (e) {
80
80
  const duration = Date.now() - start;
81
- if (e.name === 'NCC05TimeoutError') {
81
+ if (e instanceof NCC05TimeoutError) {
82
82
  console.log(`Timed out as expected in ${duration}ms: OK`);
83
83
  } else {
84
84
  console.error('Caught unexpected error:', e);