ncc-05 1.1.1 → 1.1.3

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,8 +1,17 @@
1
1
  # ncc-05
2
2
 
3
- Nostr Community Convention 05 - Identity-Bound Service Locator Resolution.
3
+ **Nostr Community Convention 05 - Identity-Bound Service Locator Resolution.**
4
4
 
5
- This library provides a simple way to publish and resolve identity-bound service endpoints (IP/Port/Onion) using Nostr `kind:30058` events.
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.
6
15
 
7
16
  ## Installation
8
17
 
@@ -12,7 +21,7 @@ npm install ncc-05
12
21
 
13
22
  ## Usage
14
23
 
15
- ### Resolver
24
+ ### 1. Basic Resolution (Self or Public)
16
25
 
17
26
  Resolve an identity-bound service locator for a given pubkey.
18
27
 
@@ -22,65 +31,62 @@ import { nip19 } from 'nostr-tools';
22
31
 
23
32
  const resolver = new NCC05Resolver();
24
33
 
25
- // Your secret key is needed to decrypt the record (NIP-44)
26
- const mySecretKey = nip19.decode('nsec...').data as Uint8Array;
27
- const targetPubkey = '...';
34
+ // Resolve using an npub (or hex pubkey)
35
+ const target = 'npub1...';
36
+ const mySecretKey = ...; // Uint8Array needed for encrypted records
28
37
 
29
- const payload = await resolver.resolve(targetPubkey, mySecretKey);
38
+ const payload = await resolver.resolve(target, mySecretKey, 'addr', {
39
+ gossip: true, // Follow NIP-65 hints
40
+ strict: true // Reject expired records
41
+ });
30
42
 
31
43
  if (payload) {
32
- console.log('Resolved Endpoints:');
33
- payload.endpoints.forEach(ep => {
34
- console.log(`- ${ep.type}://${ep.uri} (${ep.family})`);
35
- });
44
+ console.log('Resolved Endpoints:', payload.endpoints);
36
45
  }
37
46
  ```
38
47
 
39
- ### Publisher
48
+ ### 2. Targeted Encryption (Friend-to-Friend)
40
49
 
41
- Publish your own service locator record.
50
+ Alice publishes a record that only Bob can decrypt.
42
51
 
43
52
  ```typescript
44
- import { NCC05Publisher, NCC05Payload } from 'ncc-05';
53
+ import { NCC05Publisher } from 'ncc-05';
45
54
 
46
55
  const publisher = new NCC05Publisher();
47
- const mySecretKey = ...;
48
-
49
- const payload: NCC05Payload = {
50
- v: 1,
51
- ttl: 600,
52
- updated_at: Math.floor(Date.now() / 1000),
53
- endpoints: [
54
- {
55
- type: 'tcp',
56
- uri: '127.0.0.1:8080',
57
- priority: 10,
58
- family: 'ipv4'
59
- }
60
- ]
61
- };
62
-
63
- const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
64
- await publisher.publish(relays, mySecretKey, payload);
56
+ const AliceSK = ...;
57
+ const BobPK = "..."; // Bob's hex pubkey
58
+
59
+ await publisher.publish(relays, AliceSK, payload, {
60
+ identifier: 'for-bob',
61
+ recipientPubkey: BobPK // Encrypts specifically for Bob
62
+ });
65
63
  ```
66
64
 
67
- ## Features
65
+ ### 3. Group Wrapping (One Event, Many Recipients)
68
66
 
69
- - **NIP-44 Encryption**: All locator records are encrypted by default.
70
- - **NIP-01/NIP-33**: Uses standard Nostr primitives.
71
- - **Identity-Centric**: Resolution is bound to a cryptographic identity (Pubkey).
72
- - **Tor/Proxy Support**: Easily route relay traffic through SOCKS5 in Node.js.
67
+ Alice shares her endpoint with a list of authorized friends in a single event.
73
68
 
74
- ## Tor & Privacy (Node.js)
69
+ ```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');
80
+ ```
75
81
 
76
- To resolve anonymously through Tor, you can use the `socks-proxy-agent` and a custom `WebSocket` implementation:
82
+ ### 4. Tor & Privacy (Node.js)
83
+
84
+ Route all Nostr relay traffic through a local Tor proxy (`127.0.0.1:9050`).
77
85
 
78
86
  ```typescript
79
- import { NCC05Resolver } from 'ncc-05';
80
87
  import { SocksProxyAgent } from 'socks-proxy-agent';
81
88
  import { WebSocket } from 'ws';
82
89
 
83
- // Create a custom WebSocket class that uses the Tor agent
84
90
  class TorWebSocket extends WebSocket {
85
91
  constructor(address: string, protocols?: string | string[]) {
86
92
  const agent = new SocksProxyAgent('socks5h://127.0.0.1:9050');
@@ -93,6 +99,22 @@ const resolver = new NCC05Resolver({
93
99
  });
94
100
  ```
95
101
 
102
+ ## API Reference
103
+
104
+ ### `NCC05Resolver`
105
+ - `resolve(targetPubkey, secretKey?, identifier?, options?)`: Finds and decrypts a locator record.
106
+ - `close()`: Closes pool connections.
107
+
108
+ ### `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.
111
+
112
+ ### `NCC05Group`
113
+ - `createGroupIdentity()`: Generates a shared group keypair.
114
+ - `resolveAsGroup(...)`: Helper for shared-nsec resolution.
115
+
96
116
  ## License
97
117
 
98
- MIT
118
+
119
+
120
+ CC0 1.0 Universal
package/dist/index.d.ts CHANGED
@@ -1,33 +1,73 @@
1
+ /**
2
+ * NCC-05: Identity-Bound Service Locator Resolution
3
+ *
4
+ * This library implements the NCC-05 convention for publishing and resolving
5
+ * dynamic service endpoints (IP, Port, Onion) bound to Nostr identities.
6
+ *
7
+ * @module ncc-05
8
+ */
1
9
  import { Event } from 'nostr-tools';
10
+ /**
11
+ * Represents a single reachable service endpoint.
12
+ */
2
13
  export interface NCC05Endpoint {
14
+ /** Protocol type, e.g., 'tcp', 'udp', 'http' */
3
15
  type: 'tcp' | 'udp' | string;
16
+ /** The URI string, e.g., '1.2.3.4:8080' or '[2001:db8::1]:9000' */
4
17
  uri: string;
18
+ /** Priority for selection (lower is higher priority) */
5
19
  priority: number;
20
+ /** Network family for routing hints */
6
21
  family: 'ipv4' | 'ipv6' | 'onion' | string;
7
22
  }
23
+ /**
24
+ * The logical structure of an NCC-05 locator record payload.
25
+ */
8
26
  export interface NCC05Payload {
27
+ /** Payload version (currently 1) */
9
28
  v: number;
29
+ /** Time-to-live in seconds */
10
30
  ttl: number;
31
+ /** Unix timestamp of the last update */
11
32
  updated_at: number;
33
+ /** List of available endpoints */
12
34
  endpoints: NCC05Endpoint[];
35
+ /** Optional capability identifiers supported by the service */
13
36
  caps?: string[];
37
+ /** Optional human-readable notes */
14
38
  notes?: string;
15
39
  }
40
+ /**
41
+ * Options for configuring the NCC05Resolver.
42
+ */
16
43
  export interface ResolverOptions {
44
+ /** List of relays used to bootstrap discovery */
17
45
  bootstrapRelays?: string[];
46
+ /** Timeout for relay queries in milliseconds (default: 10000) */
18
47
  timeout?: number;
48
+ /** Custom WebSocket implementation (e.g., for Tor/SOCKS5 in Node.js) */
19
49
  websocketImplementation?: any;
20
50
  }
21
51
  /**
22
- * Advanced multi-recipient "wrapping" structure.
52
+ * Structure for multi-recipient encrypted events.
53
+ * Implements a "wrapping" pattern to share one event with multiple keys.
23
54
  */
24
55
  export interface WrappedContent {
25
- /** The actual payload encrypted with a random symmetric key */
56
+ /** The NCC05Payload encrypted with a random symmetric session key */
26
57
  ciphertext: string;
27
- /** Map of recipient pubkey -> wrapped symmetric key */
58
+ /** Map of recipient pubkey (hex) to the encrypted session key */
28
59
  wraps: Record<string, string>;
29
60
  }
61
+ /**
62
+ * Utility for managing shared group access to service records.
63
+ */
30
64
  export declare class NCC05Group {
65
+ /**
66
+ * Generates a fresh identity (keypair) for a shared group.
67
+ * The resulting nsec should be shared with all authorized group members.
68
+ *
69
+ * @returns An object containing nsec, hex pubkey, and the raw secret key.
70
+ */
31
71
  static createGroupIdentity(): {
32
72
  nsec: `nsec1${string}`;
33
73
  sk: Uint8Array<ArrayBufferLike>;
@@ -35,31 +75,87 @@ export declare class NCC05Group {
35
75
  npub: `npub1${string}`;
36
76
  };
37
77
  /**
38
- * Resolve a record that was published using a group's shared identity.
78
+ * Helper to resolve a record using a group's shared identity.
79
+ *
80
+ * @param resolver - An initialized NCC05Resolver instance.
81
+ * @param groupPubkey - The public key of the group.
82
+ * @param groupSecretKey - The shared secret key of the group.
83
+ * @param identifier - The 'd' tag of the record (default: 'addr').
84
+ * @returns The resolved NCC05Payload or null.
39
85
  */
40
86
  static resolveAsGroup(resolver: NCC05Resolver, groupPubkey: string, groupSecretKey: Uint8Array, identifier?: string): Promise<NCC05Payload | null>;
41
87
  }
88
+ /**
89
+ * Handles the discovery, selection, and decryption of NCC-05 locator records.
90
+ */
42
91
  export declare class NCC05Resolver {
43
92
  private pool;
44
93
  private bootstrapRelays;
45
94
  private timeout;
95
+ /**
96
+ * @param options - Configuration for the resolver.
97
+ */
46
98
  constructor(options?: ResolverOptions);
99
+ /**
100
+ * Resolves a locator record for a given identity.
101
+ *
102
+ * Supports standard NIP-44 encryption, multi-recipient "wrapping",
103
+ * and plaintext public records.
104
+ *
105
+ * @param targetPubkey - The pubkey (hex or npub) of the service owner.
106
+ * @param secretKey - Your secret key (required if the record is encrypted).
107
+ * @param identifier - The 'd' tag of the record (default: 'addr').
108
+ * @param options - Resolution options (strict mode, gossip discovery).
109
+ * @returns The resolved and validated NCC05Payload, or null if not found/invalid.
110
+ */
47
111
  resolve(targetPubkey: string, secretKey?: Uint8Array, identifier?: string, options?: {
48
112
  strict?: boolean;
49
113
  gossip?: boolean;
50
114
  }): Promise<NCC05Payload | null>;
115
+ /**
116
+ * Closes connections to all relays in the pool.
117
+ */
51
118
  close(): void;
52
119
  }
120
+ /**
121
+ * Handles the construction, encryption, and publication of NCC-05 events.
122
+ */
53
123
  export declare class NCC05Publisher {
54
124
  private pool;
125
+ /**
126
+ * @param options - Configuration for the publisher.
127
+ */
55
128
  constructor(options?: {
56
129
  websocketImplementation?: any;
57
130
  });
131
+ /**
132
+ * Publishes a single record encrypted for multiple recipients using the wrapping pattern.
133
+ * This avoids sharing a single group private key.
134
+ *
135
+ * @param relays - List of relays to publish to.
136
+ * @param secretKey - The publisher's secret key.
137
+ * @param recipients - List of recipient public keys (hex).
138
+ * @param payload - The service locator payload.
139
+ * @param identifier - The 'd' tag identifier (default: 'addr').
140
+ * @returns The signed Nostr event.
141
+ */
58
142
  publishWrapped(relays: string[], secretKey: Uint8Array, recipients: string[], payload: NCC05Payload, identifier?: string): Promise<Event>;
143
+ /**
144
+ * Publishes a locator record. Supports self-encryption, targeted encryption, or plaintext.
145
+ *
146
+ * @param relays - List of relays to publish to.
147
+ * @param secretKey - The publisher's secret key.
148
+ * @param payload - The service locator payload.
149
+ * @param options - Publishing options (identifier, recipient, or public flag).
150
+ * @returns The signed Nostr event.
151
+ */
59
152
  publish(relays: string[], secretKey: Uint8Array, payload: NCC05Payload, options?: {
60
153
  identifier?: string;
61
154
  recipientPubkey?: string;
62
155
  public?: boolean;
63
156
  }): Promise<Event>;
157
+ /**
158
+ * Closes connections to the specified relays.
159
+ */
64
160
  close(relays: string[]): void;
65
161
  }
package/dist/index.js CHANGED
@@ -1,5 +1,22 @@
1
+ /**
2
+ * NCC-05: Identity-Bound Service Locator Resolution
3
+ *
4
+ * This library implements the NCC-05 convention for publishing and resolving
5
+ * dynamic service endpoints (IP, Port, Onion) bound to Nostr identities.
6
+ *
7
+ * @module ncc-05
8
+ */
1
9
  import { SimplePool, nip44, nip19, finalizeEvent, verifyEvent, getPublicKey, generateSecretKey } from 'nostr-tools';
10
+ /**
11
+ * Utility for managing shared group access to service records.
12
+ */
2
13
  export class NCC05Group {
14
+ /**
15
+ * Generates a fresh identity (keypair) for a shared group.
16
+ * The resulting nsec should be shared with all authorized group members.
17
+ *
18
+ * @returns An object containing nsec, hex pubkey, and the raw secret key.
19
+ */
3
20
  static createGroupIdentity() {
4
21
  const sk = generateSecretKey();
5
22
  const pk = getPublicKey(sk);
@@ -11,25 +28,49 @@ export class NCC05Group {
11
28
  };
12
29
  }
13
30
  /**
14
- * Resolve a record that was published using a group's shared identity.
31
+ * Helper to resolve a record using a group's shared identity.
32
+ *
33
+ * @param resolver - An initialized NCC05Resolver instance.
34
+ * @param groupPubkey - The public key of the group.
35
+ * @param groupSecretKey - The shared secret key of the group.
36
+ * @param identifier - The 'd' tag of the record (default: 'addr').
37
+ * @returns The resolved NCC05Payload or null.
15
38
  */
16
39
  static async resolveAsGroup(resolver, groupPubkey, groupSecretKey, identifier = 'addr') {
17
40
  return resolver.resolve(groupPubkey, groupSecretKey, identifier);
18
41
  }
19
42
  }
43
+ /**
44
+ * Handles the discovery, selection, and decryption of NCC-05 locator records.
45
+ */
20
46
  export class NCC05Resolver {
21
47
  pool;
22
48
  bootstrapRelays;
23
49
  timeout;
50
+ /**
51
+ * @param options - Configuration for the resolver.
52
+ */
24
53
  constructor(options = {}) {
25
54
  this.pool = new SimplePool();
26
55
  if (options.websocketImplementation) {
27
- // @ts-ignore
56
+ // @ts-ignore - Patching pool for custom transport
28
57
  this.pool.websocketImplementation = options.websocketImplementation;
29
58
  }
30
59
  this.bootstrapRelays = options.bootstrapRelays || ['wss://relay.damus.io', 'wss://nos.lol'];
31
60
  this.timeout = options.timeout || 10000;
32
61
  }
62
+ /**
63
+ * Resolves a locator record for a given identity.
64
+ *
65
+ * Supports standard NIP-44 encryption, multi-recipient "wrapping",
66
+ * and plaintext public records.
67
+ *
68
+ * @param targetPubkey - The pubkey (hex or npub) of the service owner.
69
+ * @param secretKey - Your secret key (required if the record is encrypted).
70
+ * @param identifier - The 'd' tag of the record (default: 'addr').
71
+ * @param options - Resolution options (strict mode, gossip discovery).
72
+ * @returns The resolved and validated NCC05Payload, or null if not found/invalid.
73
+ */
33
74
  async resolve(targetPubkey, secretKey, identifier = 'addr', options = {}) {
34
75
  let hexPubkey = targetPubkey;
35
76
  if (targetPubkey.startsWith('npub1')) {
@@ -37,6 +78,7 @@ export class NCC05Resolver {
37
78
  hexPubkey = decoded.data;
38
79
  }
39
80
  let queryRelays = [...this.bootstrapRelays];
81
+ // 1. NIP-65 Gossip Discovery
40
82
  if (options.gossip) {
41
83
  const relayListEvent = await this.pool.get(this.bootstrapRelays, {
42
84
  authors: [hexPubkey],
@@ -46,7 +88,9 @@ export class NCC05Resolver {
46
88
  const discoveredRelays = relayListEvent.tags
47
89
  .filter(t => t[0] === 'r')
48
90
  .map(t => t[1]);
49
- queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
91
+ if (discoveredRelays.length > 0) {
92
+ queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
93
+ }
50
94
  }
51
95
  }
52
96
  const filter = {
@@ -68,7 +112,7 @@ export class NCC05Resolver {
68
112
  const latestEvent = validEvents[0];
69
113
  try {
70
114
  let content = latestEvent.content;
71
- // 1. Try to detect if it's a "Wrapped" multi-recipient event
115
+ // Handle "Wrapped" multi-recipient content
72
116
  if (content.includes('"wraps"') && secretKey) {
73
117
  const wrapped = JSON.parse(content);
74
118
  const myPk = getPublicKey(secretKey);
@@ -76,25 +120,25 @@ export class NCC05Resolver {
76
120
  if (myWrap) {
77
121
  const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
78
122
  const symmetricKeyHex = nip44.decrypt(myWrap, conversationKey);
79
- // Convert hex symmetric key back to Uint8Array for NIP-44 decryption
80
123
  const symmetricKey = new Uint8Array(symmetricKeyHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
81
- // The payload was self-encrypted by the publisher with the session key
82
124
  const sessionConversationKey = nip44.getConversationKey(symmetricKey, getPublicKey(symmetricKey));
83
125
  content = nip44.decrypt(wrapped.ciphertext, sessionConversationKey);
84
126
  }
85
127
  }
86
128
  else if (secretKey) {
129
+ // Standard NIP-44
87
130
  const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
88
131
  content = nip44.decrypt(latestEvent.content, conversationKey);
89
132
  }
90
133
  const payload = JSON.parse(content);
91
134
  if (!payload.endpoints)
92
135
  return null;
136
+ // Freshness validation
93
137
  const now = Math.floor(Date.now() / 1000);
94
138
  if (now > payload.updated_at + payload.ttl) {
95
139
  if (options.strict)
96
140
  return null;
97
- console.warn('NCC-05 record expired');
141
+ console.warn('NCC-05 record has expired');
98
142
  }
99
143
  return payload;
100
144
  }
@@ -102,10 +146,21 @@ export class NCC05Resolver {
102
146
  return null;
103
147
  }
104
148
  }
105
- close() { this.pool.close(this.bootstrapRelays); }
149
+ /**
150
+ * Closes connections to all relays in the pool.
151
+ */
152
+ close() {
153
+ this.pool.close(this.bootstrapRelays);
154
+ }
106
155
  }
156
+ /**
157
+ * Handles the construction, encryption, and publication of NCC-05 events.
158
+ */
107
159
  export class NCC05Publisher {
108
160
  pool;
161
+ /**
162
+ * @param options - Configuration for the publisher.
163
+ */
109
164
  constructor(options = {}) {
110
165
  this.pool = new SimplePool();
111
166
  if (options.websocketImplementation) {
@@ -113,6 +168,17 @@ export class NCC05Publisher {
113
168
  this.pool.websocketImplementation = options.websocketImplementation;
114
169
  }
115
170
  }
171
+ /**
172
+ * Publishes a single record encrypted for multiple recipients using the wrapping pattern.
173
+ * This avoids sharing a single group private key.
174
+ *
175
+ * @param relays - List of relays to publish to.
176
+ * @param secretKey - The publisher's secret key.
177
+ * @param recipients - List of recipient public keys (hex).
178
+ * @param payload - The service locator payload.
179
+ * @param identifier - The 'd' tag identifier (default: 'addr').
180
+ * @returns The signed Nostr event.
181
+ */
116
182
  async publishWrapped(relays, secretKey, recipients, payload, identifier = 'addr') {
117
183
  const sessionKey = generateSecretKey();
118
184
  const sessionKeyHex = Array.from(sessionKey).map(b => b.toString(16).padStart(2, '0')).join('');
@@ -134,6 +200,15 @@ export class NCC05Publisher {
134
200
  await Promise.all(this.pool.publish(relays, signedEvent));
135
201
  return signedEvent;
136
202
  }
203
+ /**
204
+ * Publishes a locator record. Supports self-encryption, targeted encryption, or plaintext.
205
+ *
206
+ * @param relays - List of relays to publish to.
207
+ * @param secretKey - The publisher's secret key.
208
+ * @param payload - The service locator payload.
209
+ * @param options - Publishing options (identifier, recipient, or public flag).
210
+ * @returns The signed Nostr event.
211
+ */
137
212
  async publish(relays, secretKey, payload, options = {}) {
138
213
  const myPubkey = getPublicKey(secretKey);
139
214
  const identifier = options.identifier || 'addr';
@@ -154,5 +229,10 @@ export class NCC05Publisher {
154
229
  await Promise.all(this.pool.publish(relays, signedEvent));
155
230
  return signedEvent;
156
231
  }
157
- close(relays) { this.pool.close(relays); }
232
+ /**
233
+ * Closes connections to the specified relays.
234
+ */
235
+ close(relays) {
236
+ this.pool.close(relays);
237
+ }
158
238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ncc-05",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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",
@@ -17,12 +17,12 @@
17
17
  "privacy"
18
18
  ],
19
19
  "author": "lostcause",
20
- "license": "MIT",
20
+ "license": "CC0-1.0",
21
21
  "dependencies": {
22
22
  "nostr-tools": "^2.10.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@types/node": "^20.0.0",
25
+ "@types/node": "^25.0.3",
26
26
  "@types/ws": "^8.18.1",
27
27
  "typescript": "^5.0.0",
28
28
  "ws": "^8.18.3"
package/src/index.ts CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * NCC-05: Identity-Bound Service Locator Resolution
3
+ *
4
+ * This library implements the NCC-05 convention for publishing and resolving
5
+ * dynamic service endpoints (IP, Port, Onion) bound to Nostr identities.
6
+ *
7
+ * @module ncc-05
8
+ */
9
+
1
10
  import {
2
11
  SimplePool,
3
12
  nip44,
@@ -9,39 +18,71 @@ import {
9
18
  generateSecretKey
10
19
  } from 'nostr-tools';
11
20
 
21
+ /**
22
+ * Represents a single reachable service endpoint.
23
+ */
12
24
  export interface NCC05Endpoint {
25
+ /** Protocol type, e.g., 'tcp', 'udp', 'http' */
13
26
  type: 'tcp' | 'udp' | string;
27
+ /** The URI string, e.g., '1.2.3.4:8080' or '[2001:db8::1]:9000' */
14
28
  uri: string;
29
+ /** Priority for selection (lower is higher priority) */
15
30
  priority: number;
31
+ /** Network family for routing hints */
16
32
  family: 'ipv4' | 'ipv6' | 'onion' | string;
17
33
  }
18
34
 
35
+ /**
36
+ * The logical structure of an NCC-05 locator record payload.
37
+ */
19
38
  export interface NCC05Payload {
39
+ /** Payload version (currently 1) */
20
40
  v: number;
41
+ /** Time-to-live in seconds */
21
42
  ttl: number;
43
+ /** Unix timestamp of the last update */
22
44
  updated_at: number;
45
+ /** List of available endpoints */
23
46
  endpoints: NCC05Endpoint[];
47
+ /** Optional capability identifiers supported by the service */
24
48
  caps?: string[];
49
+ /** Optional human-readable notes */
25
50
  notes?: string;
26
51
  }
27
52
 
53
+ /**
54
+ * Options for configuring the NCC05Resolver.
55
+ */
28
56
  export interface ResolverOptions {
57
+ /** List of relays used to bootstrap discovery */
29
58
  bootstrapRelays?: string[];
59
+ /** Timeout for relay queries in milliseconds (default: 10000) */
30
60
  timeout?: number;
61
+ /** Custom WebSocket implementation (e.g., for Tor/SOCKS5 in Node.js) */
31
62
  websocketImplementation?: any;
32
63
  }
33
64
 
34
65
  /**
35
- * Advanced multi-recipient "wrapping" structure.
66
+ * Structure for multi-recipient encrypted events.
67
+ * Implements a "wrapping" pattern to share one event with multiple keys.
36
68
  */
37
69
  export interface WrappedContent {
38
- /** The actual payload encrypted with a random symmetric key */
70
+ /** The NCC05Payload encrypted with a random symmetric session key */
39
71
  ciphertext: string;
40
- /** Map of recipient pubkey -> wrapped symmetric key */
72
+ /** Map of recipient pubkey (hex) to the encrypted session key */
41
73
  wraps: Record<string, string>;
42
74
  }
43
75
 
76
+ /**
77
+ * Utility for managing shared group access to service records.
78
+ */
44
79
  export class NCC05Group {
80
+ /**
81
+ * Generates a fresh identity (keypair) for a shared group.
82
+ * The resulting nsec should be shared with all authorized group members.
83
+ *
84
+ * @returns An object containing nsec, hex pubkey, and the raw secret key.
85
+ */
45
86
  static createGroupIdentity() {
46
87
  const sk = generateSecretKey();
47
88
  const pk = getPublicKey(sk);
@@ -54,7 +95,13 @@ export class NCC05Group {
54
95
  }
55
96
 
56
97
  /**
57
- * Resolve a record that was published using a group's shared identity.
98
+ * Helper to resolve a record using a group's shared identity.
99
+ *
100
+ * @param resolver - An initialized NCC05Resolver instance.
101
+ * @param groupPubkey - The public key of the group.
102
+ * @param groupSecretKey - The shared secret key of the group.
103
+ * @param identifier - The 'd' tag of the record (default: 'addr').
104
+ * @returns The resolved NCC05Payload or null.
58
105
  */
59
106
  static async resolveAsGroup(
60
107
  resolver: NCC05Resolver,
@@ -66,21 +113,39 @@ export class NCC05Group {
66
113
  }
67
114
  }
68
115
 
116
+ /**
117
+ * Handles the discovery, selection, and decryption of NCC-05 locator records.
118
+ */
69
119
  export class NCC05Resolver {
70
120
  private pool: SimplePool;
71
121
  private bootstrapRelays: string[];
72
122
  private timeout: number;
73
123
 
124
+ /**
125
+ * @param options - Configuration for the resolver.
126
+ */
74
127
  constructor(options: ResolverOptions = {}) {
75
128
  this.pool = new SimplePool();
76
129
  if (options.websocketImplementation) {
77
- // @ts-ignore
130
+ // @ts-ignore - Patching pool for custom transport
78
131
  this.pool.websocketImplementation = options.websocketImplementation;
79
132
  }
80
133
  this.bootstrapRelays = options.bootstrapRelays || ['wss://relay.damus.io', 'wss://nos.lol'];
81
134
  this.timeout = options.timeout || 10000;
82
135
  }
83
136
 
137
+ /**
138
+ * Resolves a locator record for a given identity.
139
+ *
140
+ * Supports standard NIP-44 encryption, multi-recipient "wrapping",
141
+ * and plaintext public records.
142
+ *
143
+ * @param targetPubkey - The pubkey (hex or npub) of the service owner.
144
+ * @param secretKey - Your secret key (required if the record is encrypted).
145
+ * @param identifier - The 'd' tag of the record (default: 'addr').
146
+ * @param options - Resolution options (strict mode, gossip discovery).
147
+ * @returns The resolved and validated NCC05Payload, or null if not found/invalid.
148
+ */
84
149
  async resolve(
85
150
  targetPubkey: string,
86
151
  secretKey?: Uint8Array,
@@ -95,6 +160,7 @@ export class NCC05Resolver {
95
160
 
96
161
  let queryRelays = [...this.bootstrapRelays];
97
162
 
163
+ // 1. NIP-65 Gossip Discovery
98
164
  if (options.gossip) {
99
165
  const relayListEvent = await this.pool.get(this.bootstrapRelays, {
100
166
  authors: [hexPubkey],
@@ -104,7 +170,9 @@ export class NCC05Resolver {
104
170
  const discoveredRelays = relayListEvent.tags
105
171
  .filter(t => t[0] === 'r')
106
172
  .map(t => t[1]);
107
- queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
173
+ if (discoveredRelays.length > 0) {
174
+ queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
175
+ }
108
176
  }
109
177
  }
110
178
 
@@ -131,7 +199,7 @@ export class NCC05Resolver {
131
199
  try {
132
200
  let content = latestEvent.content;
133
201
 
134
- // 1. Try to detect if it's a "Wrapped" multi-recipient event
202
+ // Handle "Wrapped" multi-recipient content
135
203
  if (content.includes('"wraps"') && secretKey) {
136
204
  const wrapped = JSON.parse(content) as WrappedContent;
137
205
  const myPk = getPublicKey(secretKey);
@@ -140,15 +208,13 @@ export class NCC05Resolver {
140
208
  if (myWrap) {
141
209
  const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
142
210
  const symmetricKeyHex = nip44.decrypt(myWrap, conversationKey);
143
-
144
- // Convert hex symmetric key back to Uint8Array for NIP-44 decryption
145
211
  const symmetricKey = new Uint8Array(symmetricKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
146
212
 
147
- // The payload was self-encrypted by the publisher with the session key
148
213
  const sessionConversationKey = nip44.getConversationKey(symmetricKey, getPublicKey(symmetricKey));
149
214
  content = nip44.decrypt(wrapped.ciphertext, sessionConversationKey);
150
215
  }
151
216
  } else if (secretKey) {
217
+ // Standard NIP-44
152
218
  const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
153
219
  content = nip44.decrypt(latestEvent.content, conversationKey);
154
220
  }
@@ -156,10 +222,11 @@ export class NCC05Resolver {
156
222
  const payload = JSON.parse(content) as NCC05Payload;
157
223
  if (!payload.endpoints) return null;
158
224
 
225
+ // Freshness validation
159
226
  const now = Math.floor(Date.now() / 1000);
160
227
  if (now > payload.updated_at + payload.ttl) {
161
228
  if (options.strict) return null;
162
- console.warn('NCC-05 record expired');
229
+ console.warn('NCC-05 record has expired');
163
230
  }
164
231
 
165
232
  return payload;
@@ -168,12 +235,23 @@ export class NCC05Resolver {
168
235
  }
169
236
  }
170
237
 
171
- close() { this.pool.close(this.bootstrapRelays); }
238
+ /**
239
+ * Closes connections to all relays in the pool.
240
+ */
241
+ close() {
242
+ this.pool.close(this.bootstrapRelays);
243
+ }
172
244
  }
173
245
 
246
+ /**
247
+ * Handles the construction, encryption, and publication of NCC-05 events.
248
+ */
174
249
  export class NCC05Publisher {
175
250
  private pool: SimplePool;
176
251
 
252
+ /**
253
+ * @param options - Configuration for the publisher.
254
+ */
177
255
  constructor(options: { websocketImplementation?: any } = {}) {
178
256
  this.pool = new SimplePool();
179
257
  if (options.websocketImplementation) {
@@ -182,6 +260,17 @@ export class NCC05Publisher {
182
260
  }
183
261
  }
184
262
 
263
+ /**
264
+ * Publishes a single record encrypted for multiple recipients using the wrapping pattern.
265
+ * This avoids sharing a single group private key.
266
+ *
267
+ * @param relays - List of relays to publish to.
268
+ * @param secretKey - The publisher's secret key.
269
+ * @param recipients - List of recipient public keys (hex).
270
+ * @param payload - The service locator payload.
271
+ * @param identifier - The 'd' tag identifier (default: 'addr').
272
+ * @returns The signed Nostr event.
273
+ */
185
274
  async publishWrapped(
186
275
  relays: string[],
187
276
  secretKey: Uint8Array,
@@ -215,6 +304,15 @@ export class NCC05Publisher {
215
304
  return signedEvent;
216
305
  }
217
306
 
307
+ /**
308
+ * Publishes a locator record. Supports self-encryption, targeted encryption, or plaintext.
309
+ *
310
+ * @param relays - List of relays to publish to.
311
+ * @param secretKey - The publisher's secret key.
312
+ * @param payload - The service locator payload.
313
+ * @param options - Publishing options (identifier, recipient, or public flag).
314
+ * @returns The signed Nostr event.
315
+ */
218
316
  async publish(
219
317
  relays: string[],
220
318
  secretKey: Uint8Array,
@@ -244,5 +342,10 @@ export class NCC05Publisher {
244
342
  return signedEvent;
245
343
  }
246
344
 
247
- close(relays: string[]) { this.pool.close(relays); }
248
- }
345
+ /**
346
+ * Closes connections to the specified relays.
347
+ */
348
+ close(relays: string[]) {
349
+ this.pool.close(relays);
350
+ }
351
+ }