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 +214 -67
- package/dist/index.d.ts +4 -6
- package/dist/index.js +15 -32
- package/dist/mock-relay.d.ts +4 -0
- package/dist/mock-relay.js +18 -1
- package/dist/test-lifecycle.d.ts +1 -0
- package/dist/test-lifecycle.js +33 -0
- package/dist/test-new.js +2 -2
- package/eslint.config.mjs +25 -0
- package/package.json +8 -1
- package/src/index.ts +15 -38
- package/src/mock-relay.ts +19 -0
- package/src/test-lifecycle.ts +40 -0
- package/src/test-new.ts +2 -2
package/README.md
CHANGED
|
@@ -1,120 +1,267 @@
|
|
|
1
1
|
# ncc-05
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
13
|
+
## Configuration
|
|
23
14
|
|
|
24
|
-
###
|
|
15
|
+
### Relays
|
|
25
16
|
|
|
26
|
-
|
|
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
|
|
22
|
+
const customRelays = ['wss://relay.example.com', 'wss://another.relay.io'];
|
|
23
|
+
const resolver = new NCC05Resolver({ bootstrapRelays: customRelays });
|
|
24
|
+
```
|
|
33
25
|
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
strict: true // Reject expired records
|
|
41
|
-
});
|
|
28
|
+
```typescript
|
|
29
|
+
import { NCC05Publisher } from 'ncc-05';
|
|
42
30
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
const publisher = new NCC05Publisher();
|
|
32
|
+
const relaysToPublishTo = ['wss://relay.example.com'];
|
|
33
|
+
// ... then call publisher.publish(relaysToPublishTo, ...)
|
|
46
34
|
```
|
|
47
35
|
|
|
48
|
-
###
|
|
36
|
+
### Shared SimplePool
|
|
49
37
|
|
|
50
|
-
|
|
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 {
|
|
41
|
+
import { SimplePool } from 'nostr-tools';
|
|
42
|
+
import { NCC05Resolver, NCC05Publisher } from 'ncc-05';
|
|
54
43
|
|
|
55
|
-
const
|
|
56
|
-
const AliceSK = ...;
|
|
57
|
-
const BobPK = "..."; // Bob's hex pubkey
|
|
44
|
+
const pool = new SimplePool();
|
|
58
45
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
###
|
|
121
|
+
### 2. Publishing a Service Locator
|
|
83
122
|
|
|
84
|
-
|
|
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 {
|
|
88
|
-
import {
|
|
126
|
+
import { NCC05Publisher, NCC05Payload } from 'ncc-05';
|
|
127
|
+
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
|
89
128
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
- `
|
|
106
|
-
- `
|
|
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
|
-
- `
|
|
110
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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.
|
|
348
|
+
if (this._ownPool) {
|
|
349
|
+
this.pool.close(relays);
|
|
350
|
+
}
|
|
368
351
|
}
|
|
369
352
|
}
|
package/dist/mock-relay.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/mock-relay.js
CHANGED
|
@@ -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
|
|
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.
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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.
|
|
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
|
|
79
|
+
} catch (e) {
|
|
80
80
|
const duration = Date.now() - start;
|
|
81
|
-
if (e
|
|
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);
|