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 +64 -42
- package/dist/index.d.ts +100 -4
- package/dist/index.js +89 -9
- package/package.json +3 -3
- package/src/index.ts +117 -14
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
|
-
|
|
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
|
-
###
|
|
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
|
-
//
|
|
26
|
-
const
|
|
27
|
-
const
|
|
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(
|
|
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
|
-
###
|
|
48
|
+
### 2. Targeted Encryption (Friend-to-Friend)
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
Alice publishes a record that only Bob can decrypt.
|
|
42
51
|
|
|
43
52
|
```typescript
|
|
44
|
-
import { NCC05Publisher
|
|
53
|
+
import { NCC05Publisher } from 'ncc-05';
|
|
45
54
|
|
|
46
55
|
const publisher = new NCC05Publisher();
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
65
|
+
### 3. Group Wrapping (One Event, Many Recipients)
|
|
68
66
|
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
56
|
+
/** The NCC05Payload encrypted with a random symmetric session key */
|
|
26
57
|
ciphertext: string;
|
|
27
|
-
/** Map of recipient pubkey
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
20
|
+
"license": "CC0-1.0",
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"nostr-tools": "^2.10.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@types/node": "^
|
|
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
|
-
*
|
|
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
|
|
70
|
+
/** The NCC05Payload encrypted with a random symmetric session key */
|
|
39
71
|
ciphertext: string;
|
|
40
|
-
/** Map of recipient pubkey
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Closes connections to the specified relays.
|
|
347
|
+
*/
|
|
348
|
+
close(relays: string[]) {
|
|
349
|
+
this.pool.close(relays);
|
|
350
|
+
}
|
|
351
|
+
}
|