ncc-05 1.1.0 → 1.1.2
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 +106 -10
- package/dist/index.js +125 -36
- package/dist/mock-relay.d.ts +6 -0
- package/dist/mock-relay.js +47 -0
- package/dist/test.js +49 -9
- package/package.json +5 -3
- package/src/index.ts +177 -49
- package/src/mock-relay.ts +51 -0
- package/src/test.ts +55 -9
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,65 +1,161 @@
|
|
|
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.
|
|
54
|
+
*/
|
|
55
|
+
export interface WrappedContent {
|
|
56
|
+
/** The NCC05Payload encrypted with a random symmetric session key */
|
|
57
|
+
ciphertext: string;
|
|
58
|
+
/** Map of recipient pubkey (hex) to the encrypted session key */
|
|
59
|
+
wraps: Record<string, string>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Utility for managing shared group access to service records.
|
|
23
63
|
*/
|
|
24
64
|
export declare class NCC05Group {
|
|
25
65
|
/**
|
|
26
|
-
*
|
|
27
|
-
* The nsec should be shared with all authorized members.
|
|
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.
|
|
28
70
|
*/
|
|
29
71
|
static createGroupIdentity(): {
|
|
30
72
|
nsec: `nsec1${string}`;
|
|
31
73
|
sk: Uint8Array<ArrayBufferLike>;
|
|
32
74
|
pk: string;
|
|
75
|
+
npub: `npub1${string}`;
|
|
33
76
|
};
|
|
34
77
|
/**
|
|
35
|
-
*
|
|
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.
|
|
36
85
|
*/
|
|
37
86
|
static resolveAsGroup(resolver: NCC05Resolver, groupPubkey: string, groupSecretKey: Uint8Array, identifier?: string): Promise<NCC05Payload | null>;
|
|
38
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Handles the discovery, selection, and decryption of NCC-05 locator records.
|
|
90
|
+
*/
|
|
39
91
|
export declare class NCC05Resolver {
|
|
40
92
|
private pool;
|
|
41
93
|
private bootstrapRelays;
|
|
42
94
|
private timeout;
|
|
95
|
+
/**
|
|
96
|
+
* @param options - Configuration for the resolver.
|
|
97
|
+
*/
|
|
43
98
|
constructor(options?: ResolverOptions);
|
|
44
99
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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.
|
|
47
110
|
*/
|
|
48
|
-
resolve(targetPubkey: string, secretKey
|
|
111
|
+
resolve(targetPubkey: string, secretKey?: Uint8Array, identifier?: string, options?: {
|
|
49
112
|
strict?: boolean;
|
|
50
113
|
gossip?: boolean;
|
|
51
114
|
}): Promise<NCC05Payload | null>;
|
|
115
|
+
/**
|
|
116
|
+
* Closes connections to all relays in the pool.
|
|
117
|
+
*/
|
|
52
118
|
close(): void;
|
|
53
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Handles the construction, encryption, and publication of NCC-05 events.
|
|
122
|
+
*/
|
|
54
123
|
export declare class NCC05Publisher {
|
|
55
124
|
private pool;
|
|
125
|
+
/**
|
|
126
|
+
* @param options - Configuration for the publisher.
|
|
127
|
+
*/
|
|
56
128
|
constructor(options?: {
|
|
57
129
|
websocketImplementation?: any;
|
|
58
130
|
});
|
|
59
131
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
152
|
+
publish(relays: string[], secretKey: Uint8Array, payload: NCC05Payload, options?: {
|
|
153
|
+
identifier?: string;
|
|
154
|
+
recipientPubkey?: string;
|
|
155
|
+
public?: boolean;
|
|
156
|
+
}): Promise<Event>;
|
|
157
|
+
/**
|
|
158
|
+
* Closes connections to the specified relays.
|
|
62
159
|
*/
|
|
63
|
-
publish(relays: string[], secretKey: Uint8Array, payload: NCC05Payload, identifier?: string, recipientPubkey?: string): Promise<Event>;
|
|
64
160
|
close(relays: string[]): void;
|
|
65
161
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,45 +1,75 @@
|
|
|
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';
|
|
2
10
|
/**
|
|
3
|
-
* Utility for managing shared group access to
|
|
11
|
+
* Utility for managing shared group access to service records.
|
|
4
12
|
*/
|
|
5
13
|
export class NCC05Group {
|
|
6
14
|
/**
|
|
7
|
-
*
|
|
8
|
-
* The nsec should be shared with all authorized members.
|
|
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.
|
|
9
19
|
*/
|
|
10
20
|
static createGroupIdentity() {
|
|
11
21
|
const sk = generateSecretKey();
|
|
22
|
+
const pk = getPublicKey(sk);
|
|
12
23
|
return {
|
|
13
24
|
nsec: nip19.nsecEncode(sk),
|
|
14
25
|
sk: sk,
|
|
15
|
-
pk:
|
|
26
|
+
pk: pk,
|
|
27
|
+
npub: nip19.npubEncode(pk)
|
|
16
28
|
};
|
|
17
29
|
}
|
|
18
30
|
/**
|
|
19
|
-
*
|
|
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.
|
|
20
38
|
*/
|
|
21
39
|
static async resolveAsGroup(resolver, groupPubkey, groupSecretKey, identifier = 'addr') {
|
|
22
|
-
// In group mode, we use the group's SK to decrypt a record
|
|
23
|
-
// that was self-encrypted by the group's PK.
|
|
24
40
|
return resolver.resolve(groupPubkey, groupSecretKey, identifier);
|
|
25
41
|
}
|
|
26
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Handles the discovery, selection, and decryption of NCC-05 locator records.
|
|
45
|
+
*/
|
|
27
46
|
export class NCC05Resolver {
|
|
28
47
|
pool;
|
|
29
48
|
bootstrapRelays;
|
|
30
49
|
timeout;
|
|
50
|
+
/**
|
|
51
|
+
* @param options - Configuration for the resolver.
|
|
52
|
+
*/
|
|
31
53
|
constructor(options = {}) {
|
|
32
54
|
this.pool = new SimplePool();
|
|
33
55
|
if (options.websocketImplementation) {
|
|
34
|
-
// @ts-ignore - Patching pool for custom
|
|
56
|
+
// @ts-ignore - Patching pool for custom transport
|
|
35
57
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
36
58
|
}
|
|
37
59
|
this.bootstrapRelays = options.bootstrapRelays || ['wss://relay.damus.io', 'wss://nos.lol'];
|
|
38
60
|
this.timeout = options.timeout || 10000;
|
|
39
61
|
}
|
|
40
62
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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.
|
|
43
73
|
*/
|
|
44
74
|
async resolve(targetPubkey, secretKey, identifier = 'addr', options = {}) {
|
|
45
75
|
let hexPubkey = targetPubkey;
|
|
@@ -70,79 +100,138 @@ export class NCC05Resolver {
|
|
|
70
100
|
limit: 10
|
|
71
101
|
};
|
|
72
102
|
const queryPromise = this.pool.querySync(queryRelays, filter);
|
|
73
|
-
const timeoutPromise = new Promise((
|
|
103
|
+
const timeoutPromise = new Promise((r) => setTimeout(() => r(null), this.timeout));
|
|
74
104
|
const result = await Promise.race([queryPromise, timeoutPromise]);
|
|
75
105
|
if (!result || (Array.isArray(result) && result.length === 0))
|
|
76
106
|
return null;
|
|
77
|
-
// 2. Filter for valid signatures and sort by created_at
|
|
78
107
|
const validEvents = result
|
|
79
108
|
.filter(e => verifyEvent(e))
|
|
80
109
|
.sort((a, b) => b.created_at - a.created_at);
|
|
81
110
|
if (validEvents.length === 0)
|
|
82
111
|
return null;
|
|
83
112
|
const latestEvent = validEvents[0];
|
|
84
|
-
// 2. Decrypt
|
|
85
113
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
114
|
+
let content = latestEvent.content;
|
|
115
|
+
// Handle "Wrapped" multi-recipient content
|
|
116
|
+
if (content.includes('"wraps"') && secretKey) {
|
|
117
|
+
const wrapped = JSON.parse(content);
|
|
118
|
+
const myPk = getPublicKey(secretKey);
|
|
119
|
+
const myWrap = wrapped.wraps[myPk];
|
|
120
|
+
if (myWrap) {
|
|
121
|
+
const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
|
|
122
|
+
const symmetricKeyHex = nip44.decrypt(myWrap, conversationKey);
|
|
123
|
+
const symmetricKey = new Uint8Array(symmetricKeyHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
|
124
|
+
const sessionConversationKey = nip44.getConversationKey(symmetricKey, getPublicKey(symmetricKey));
|
|
125
|
+
content = nip44.decrypt(wrapped.ciphertext, sessionConversationKey);
|
|
126
|
+
}
|
|
93
127
|
}
|
|
94
|
-
|
|
128
|
+
else if (secretKey) {
|
|
129
|
+
// Standard NIP-44
|
|
130
|
+
const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
|
|
131
|
+
content = nip44.decrypt(latestEvent.content, conversationKey);
|
|
132
|
+
}
|
|
133
|
+
const payload = JSON.parse(content);
|
|
134
|
+
if (!payload.endpoints)
|
|
135
|
+
return null;
|
|
136
|
+
// Freshness validation
|
|
95
137
|
const now = Math.floor(Date.now() / 1000);
|
|
96
138
|
if (now > payload.updated_at + payload.ttl) {
|
|
97
|
-
if (options.strict)
|
|
98
|
-
console.warn('Rejecting expired NCC-05 record (strict mode)');
|
|
139
|
+
if (options.strict)
|
|
99
140
|
return null;
|
|
100
|
-
}
|
|
101
141
|
console.warn('NCC-05 record has expired');
|
|
102
142
|
}
|
|
103
143
|
return payload;
|
|
104
144
|
}
|
|
105
145
|
catch (e) {
|
|
106
|
-
console.error('Failed to decrypt or parse NCC-05 record:', e);
|
|
107
146
|
return null;
|
|
108
147
|
}
|
|
109
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Closes connections to all relays in the pool.
|
|
151
|
+
*/
|
|
110
152
|
close() {
|
|
111
153
|
this.pool.close(this.bootstrapRelays);
|
|
112
154
|
}
|
|
113
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Handles the construction, encryption, and publication of NCC-05 events.
|
|
158
|
+
*/
|
|
114
159
|
export class NCC05Publisher {
|
|
115
160
|
pool;
|
|
161
|
+
/**
|
|
162
|
+
* @param options - Configuration for the publisher.
|
|
163
|
+
*/
|
|
116
164
|
constructor(options = {}) {
|
|
117
165
|
this.pool = new SimplePool();
|
|
118
166
|
if (options.websocketImplementation) {
|
|
119
|
-
// @ts-ignore
|
|
167
|
+
// @ts-ignore
|
|
120
168
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
121
169
|
}
|
|
122
170
|
}
|
|
123
171
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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
|
+
*/
|
|
182
|
+
async publishWrapped(relays, secretKey, recipients, payload, identifier = 'addr') {
|
|
183
|
+
const sessionKey = generateSecretKey();
|
|
184
|
+
const sessionKeyHex = Array.from(sessionKey).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
185
|
+
const selfConversation = nip44.getConversationKey(sessionKey, getPublicKey(sessionKey));
|
|
186
|
+
const ciphertext = nip44.encrypt(JSON.stringify(payload), selfConversation);
|
|
187
|
+
const wraps = {};
|
|
188
|
+
for (const rPk of recipients) {
|
|
189
|
+
const conversationKey = nip44.getConversationKey(secretKey, rPk);
|
|
190
|
+
wraps[rPk] = nip44.encrypt(sessionKeyHex, conversationKey);
|
|
191
|
+
}
|
|
192
|
+
const wrappedContent = { ciphertext, wraps };
|
|
193
|
+
const eventTemplate = {
|
|
194
|
+
kind: 30058,
|
|
195
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
196
|
+
tags: [['d', identifier]],
|
|
197
|
+
content: JSON.stringify(wrappedContent),
|
|
198
|
+
};
|
|
199
|
+
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
200
|
+
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
201
|
+
return signedEvent;
|
|
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.
|
|
126
211
|
*/
|
|
127
|
-
async publish(relays, secretKey, payload,
|
|
212
|
+
async publish(relays, secretKey, payload, options = {}) {
|
|
128
213
|
const myPubkey = getPublicKey(secretKey);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
214
|
+
const identifier = options.identifier || 'addr';
|
|
215
|
+
let content = JSON.stringify(payload);
|
|
216
|
+
if (!options.public) {
|
|
217
|
+
const encryptionTarget = options.recipientPubkey || myPubkey;
|
|
218
|
+
const conversationKey = nip44.getConversationKey(secretKey, encryptionTarget);
|
|
219
|
+
content = nip44.encrypt(content, conversationKey);
|
|
220
|
+
}
|
|
134
221
|
const eventTemplate = {
|
|
135
222
|
kind: 30058,
|
|
136
223
|
created_at: Math.floor(Date.now() / 1000),
|
|
137
224
|
pubkey: myPubkey,
|
|
138
225
|
tags: [['d', identifier]],
|
|
139
|
-
content:
|
|
226
|
+
content: content,
|
|
140
227
|
};
|
|
141
228
|
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
142
|
-
// 3. Publish
|
|
143
229
|
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
144
230
|
return signedEvent;
|
|
145
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Closes connections to the specified relays.
|
|
234
|
+
*/
|
|
146
235
|
close(relays) {
|
|
147
236
|
this.pool.close(relays);
|
|
148
237
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
export class MockRelay {
|
|
3
|
+
wss;
|
|
4
|
+
events = [];
|
|
5
|
+
constructor(port = 8080) {
|
|
6
|
+
this.wss = new WebSocketServer({ port });
|
|
7
|
+
this.wss.on('connection', (ws) => {
|
|
8
|
+
ws.on('message', (data) => {
|
|
9
|
+
const msg = JSON.parse(data);
|
|
10
|
+
const type = msg[0];
|
|
11
|
+
if (type === 'EVENT') {
|
|
12
|
+
const event = msg[1];
|
|
13
|
+
// Basic replaceable logic for test consistency
|
|
14
|
+
if (event.kind === 30058 || event.kind === 10002) {
|
|
15
|
+
const dTag = event.tags.find((t) => t[0] === 'd')?.[1] || "";
|
|
16
|
+
this.events = this.events.filter(e => !(e.pubkey === event.pubkey && e.kind === event.kind && (e.tags.find((t) => t[0] === 'd')?.[1] || "") === dTag));
|
|
17
|
+
}
|
|
18
|
+
this.events.push(event);
|
|
19
|
+
ws.send(JSON.stringify(["OK", event.id, true, ""]));
|
|
20
|
+
}
|
|
21
|
+
else if (type === 'REQ') {
|
|
22
|
+
const subId = msg[1];
|
|
23
|
+
const filters = msg[2];
|
|
24
|
+
this.events.forEach(event => {
|
|
25
|
+
let match = true;
|
|
26
|
+
if (filters.authors && !filters.authors.includes(event.pubkey))
|
|
27
|
+
match = false;
|
|
28
|
+
if (filters.kinds && !filters.kinds.includes(event.kind))
|
|
29
|
+
match = false;
|
|
30
|
+
if (filters['#d']) {
|
|
31
|
+
const dTag = event.tags.find((t) => t[0] === 'd')?.[1];
|
|
32
|
+
if (!filters['#d'].includes(dTag))
|
|
33
|
+
match = false;
|
|
34
|
+
}
|
|
35
|
+
if (match) {
|
|
36
|
+
ws.send(JSON.stringify(["EVENT", subId, event]));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
ws.send(JSON.stringify(["EOSE", subId]));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
stop() {
|
|
45
|
+
this.wss.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/test.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { NCC05Publisher, NCC05Resolver, NCC05Group } from './index.js';
|
|
2
|
-
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
|
2
|
+
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
|
3
|
+
import { MockRelay } from './mock-relay.js';
|
|
3
4
|
async function test() {
|
|
5
|
+
console.log('Starting Mock Relay...');
|
|
6
|
+
const relay = new MockRelay(8080);
|
|
7
|
+
const relays = ['ws://localhost:8080'];
|
|
4
8
|
const sk = generateSecretKey();
|
|
5
9
|
const pk = getPublicKey(sk);
|
|
6
|
-
const relays = ['wss://relay.damus.io'];
|
|
7
10
|
const publisher = new NCC05Publisher();
|
|
8
11
|
const resolver = new NCC05Resolver({ bootstrapRelays: relays });
|
|
9
12
|
const payload = {
|
|
@@ -23,7 +26,8 @@ async function test() {
|
|
|
23
26
|
console.log('Successfully resolved:', JSON.stringify(resolved, null, 2));
|
|
24
27
|
}
|
|
25
28
|
else {
|
|
26
|
-
console.
|
|
29
|
+
console.error('FAILED: resolution.');
|
|
30
|
+
process.exit(1);
|
|
27
31
|
}
|
|
28
32
|
// Test Strict Mode with Expired Record
|
|
29
33
|
console.log('Testing expired record in strict mode...');
|
|
@@ -33,7 +37,7 @@ async function test() {
|
|
|
33
37
|
updated_at: Math.floor(Date.now() / 1000) - 10, // 10s ago
|
|
34
38
|
endpoints: [{ type: 'tcp', uri: '1.1.1.1:1', priority: 1, family: 'ipv4' }]
|
|
35
39
|
};
|
|
36
|
-
await publisher.publish(relays, sk, expiredPayload, 'expired-test');
|
|
40
|
+
await publisher.publish(relays, sk, expiredPayload, { identifier: 'expired-test' });
|
|
37
41
|
const strictResult = await resolver.resolve(pk, sk, 'expired-test', { strict: true });
|
|
38
42
|
if (strictResult === null) {
|
|
39
43
|
console.log('Correctly rejected expired record in strict mode.');
|
|
@@ -44,8 +48,6 @@ async function test() {
|
|
|
44
48
|
}
|
|
45
49
|
// Test Gossip Mode
|
|
46
50
|
console.log('Testing Gossip discovery...');
|
|
47
|
-
// In this test, we just point kind:10002 to the same relay we are using
|
|
48
|
-
// to verify the code path executes.
|
|
49
51
|
const relayListTemplate = {
|
|
50
52
|
kind: 10002,
|
|
51
53
|
created_at: Math.floor(Date.now() / 1000),
|
|
@@ -53,6 +55,7 @@ async function test() {
|
|
|
53
55
|
content: '',
|
|
54
56
|
};
|
|
55
57
|
const signedRL = (await import('nostr-tools')).finalizeEvent(relayListTemplate, sk);
|
|
58
|
+
// @ts-ignore
|
|
56
59
|
await Promise.all(publisher['pool'].publish(relays, signedRL));
|
|
57
60
|
const gossipResult = await resolver.resolve(pk, sk, 'addr', { gossip: true });
|
|
58
61
|
if (gossipResult) {
|
|
@@ -64,7 +67,7 @@ async function test() {
|
|
|
64
67
|
}
|
|
65
68
|
// Test npub resolution
|
|
66
69
|
console.log('Testing npub resolution...');
|
|
67
|
-
const npub =
|
|
70
|
+
const npub = nip19.npubEncode(pk);
|
|
68
71
|
const npubResult = await resolver.resolve(npub, sk);
|
|
69
72
|
if (npubResult) {
|
|
70
73
|
console.log('npub resolution successful.');
|
|
@@ -85,7 +88,17 @@ async function test() {
|
|
|
85
88
|
};
|
|
86
89
|
// User A publishes for User B
|
|
87
90
|
console.log('User A publishing for User B...');
|
|
88
|
-
await publisher.publish(relays, skA, payloadFriend, 'friend-test', pkB);
|
|
91
|
+
await publisher.publish(relays, skA, payloadFriend, { identifier: 'friend-test', recipientPubkey: pkB });
|
|
92
|
+
// User B resolves User A's record
|
|
93
|
+
console.log('User B resolving User A...');
|
|
94
|
+
const friendResult = await resolver.resolve(pkA, skB, 'friend-test');
|
|
95
|
+
if (friendResult && friendResult.endpoints[0].uri === 'friend:7777') {
|
|
96
|
+
console.log('Friend-to-Friend resolution successful.');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.error('FAILED: Friend-to-Friend resolution.');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
89
102
|
// Test Group Resolution Utility
|
|
90
103
|
console.log('Testing NCC05Group utility...');
|
|
91
104
|
const groupIdentity = NCC05Group.createGroupIdentity();
|
|
@@ -94,7 +107,7 @@ async function test() {
|
|
|
94
107
|
endpoints: [{ type: 'tcp', uri: 'group-service:8888', priority: 1, family: 'ipv4' }]
|
|
95
108
|
};
|
|
96
109
|
console.log('Publishing as Group...');
|
|
97
|
-
await publisher.publish(relays, groupIdentity.sk, payloadGroup, 'group-test');
|
|
110
|
+
await publisher.publish(relays, groupIdentity.sk, payloadGroup, { identifier: 'group-test' });
|
|
98
111
|
console.log('Resolving as Group Member...');
|
|
99
112
|
const groupResult = await NCC05Group.resolveAsGroup(resolver, groupIdentity.pk, groupIdentity.sk, 'group-test');
|
|
100
113
|
if (groupResult && groupResult.endpoints[0].uri === 'group-service:8888') {
|
|
@@ -104,8 +117,35 @@ async function test() {
|
|
|
104
117
|
console.error('FAILED: NCC05Group resolution.');
|
|
105
118
|
process.exit(1);
|
|
106
119
|
}
|
|
120
|
+
// Test Group Wrapping (Multi-Recipient)
|
|
121
|
+
console.log('Testing Group Wrapping (Multi-Recipient)...');
|
|
122
|
+
const skAlice = generateSecretKey();
|
|
123
|
+
const pkAlice = getPublicKey(skAlice);
|
|
124
|
+
const skBob = generateSecretKey();
|
|
125
|
+
const pkBob = getPublicKey(skBob);
|
|
126
|
+
const skCharlie = generateSecretKey();
|
|
127
|
+
const pkCharlie = getPublicKey(skCharlie);
|
|
128
|
+
const payloadWrap = {
|
|
129
|
+
v: 1, ttl: 60, updated_at: Math.floor(Date.now() / 1000),
|
|
130
|
+
endpoints: [{ type: 'tcp', uri: 'multi-recipient:9999', priority: 1, family: 'ipv4' }]
|
|
131
|
+
};
|
|
132
|
+
console.log('Alice publishing wrapped record for Bob and Charlie...');
|
|
133
|
+
await publisher.publishWrapped(relays, skAlice, [pkBob, pkCharlie], payloadWrap, 'wrap-test');
|
|
134
|
+
console.log('Bob resolving Alice...');
|
|
135
|
+
const bobResult = await resolver.resolve(pkAlice, skBob, 'wrap-test');
|
|
136
|
+
console.log('Charlie resolving Alice...');
|
|
137
|
+
const charlieResult = await resolver.resolve(pkAlice, skCharlie, 'wrap-test');
|
|
138
|
+
if (bobResult && charlieResult && bobResult.endpoints[0].uri === 'multi-recipient:9999') {
|
|
139
|
+
console.log('Group Wrapping successful! Both recipients resolved Alice.');
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.error('FAILED: Group Wrapping resolution.');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
107
145
|
publisher.close(relays);
|
|
108
146
|
resolver.close();
|
|
147
|
+
relay.stop();
|
|
148
|
+
console.log('Local Mock Test Suite Passed.');
|
|
109
149
|
process.exit(0);
|
|
110
150
|
}
|
|
111
151
|
test().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ncc-05",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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,14 @@
|
|
|
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.0.3",
|
|
26
|
+
"@types/ws": "^8.18.1",
|
|
25
27
|
"typescript": "^5.0.0",
|
|
26
|
-
"
|
|
28
|
+
"ws": "^8.18.3"
|
|
27
29
|
}
|
|
28
30
|
}
|
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,47 +18,90 @@ 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;
|
|
31
|
-
|
|
61
|
+
/** Custom WebSocket implementation (e.g., for Tor/SOCKS5 in Node.js) */
|
|
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.
|
|
68
|
+
*/
|
|
69
|
+
export interface WrappedContent {
|
|
70
|
+
/** The NCC05Payload encrypted with a random symmetric session key */
|
|
71
|
+
ciphertext: string;
|
|
72
|
+
/** Map of recipient pubkey (hex) to the encrypted session key */
|
|
73
|
+
wraps: Record<string, string>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Utility for managing shared group access to service records.
|
|
36
78
|
*/
|
|
37
79
|
export class NCC05Group {
|
|
38
80
|
/**
|
|
39
|
-
*
|
|
40
|
-
* The nsec should be shared with all authorized members.
|
|
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.
|
|
41
85
|
*/
|
|
42
86
|
static createGroupIdentity() {
|
|
43
87
|
const sk = generateSecretKey();
|
|
88
|
+
const pk = getPublicKey(sk);
|
|
44
89
|
return {
|
|
45
90
|
nsec: nip19.nsecEncode(sk),
|
|
46
91
|
sk: sk,
|
|
47
|
-
pk:
|
|
92
|
+
pk: pk,
|
|
93
|
+
npub: nip19.npubEncode(pk)
|
|
48
94
|
};
|
|
49
95
|
}
|
|
50
96
|
|
|
51
97
|
/**
|
|
52
|
-
*
|
|
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.
|
|
53
105
|
*/
|
|
54
106
|
static async resolveAsGroup(
|
|
55
107
|
resolver: NCC05Resolver,
|
|
@@ -57,21 +109,25 @@ export class NCC05Group {
|
|
|
57
109
|
groupSecretKey: Uint8Array,
|
|
58
110
|
identifier: string = 'addr'
|
|
59
111
|
): Promise<NCC05Payload | null> {
|
|
60
|
-
// In group mode, we use the group's SK to decrypt a record
|
|
61
|
-
// that was self-encrypted by the group's PK.
|
|
62
112
|
return resolver.resolve(groupPubkey, groupSecretKey, identifier);
|
|
63
113
|
}
|
|
64
114
|
}
|
|
65
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Handles the discovery, selection, and decryption of NCC-05 locator records.
|
|
118
|
+
*/
|
|
66
119
|
export class NCC05Resolver {
|
|
67
120
|
private pool: SimplePool;
|
|
68
121
|
private bootstrapRelays: string[];
|
|
69
122
|
private timeout: number;
|
|
70
123
|
|
|
124
|
+
/**
|
|
125
|
+
* @param options - Configuration for the resolver.
|
|
126
|
+
*/
|
|
71
127
|
constructor(options: ResolverOptions = {}) {
|
|
72
128
|
this.pool = new SimplePool();
|
|
73
129
|
if (options.websocketImplementation) {
|
|
74
|
-
// @ts-ignore - Patching pool for custom
|
|
130
|
+
// @ts-ignore - Patching pool for custom transport
|
|
75
131
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
76
132
|
}
|
|
77
133
|
this.bootstrapRelays = options.bootstrapRelays || ['wss://relay.damus.io', 'wss://nos.lol'];
|
|
@@ -79,12 +135,20 @@ export class NCC05Resolver {
|
|
|
79
135
|
}
|
|
80
136
|
|
|
81
137
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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.
|
|
84
148
|
*/
|
|
85
149
|
async resolve(
|
|
86
150
|
targetPubkey: string,
|
|
87
|
-
secretKey
|
|
151
|
+
secretKey?: Uint8Array,
|
|
88
152
|
identifier: string = 'addr',
|
|
89
153
|
options: { strict?: boolean, gossip?: boolean } = {}
|
|
90
154
|
): Promise<NCC05Payload | null> {
|
|
@@ -102,7 +166,6 @@ export class NCC05Resolver {
|
|
|
102
166
|
authors: [hexPubkey],
|
|
103
167
|
kinds: [10002]
|
|
104
168
|
});
|
|
105
|
-
|
|
106
169
|
if (relayListEvent) {
|
|
107
170
|
const discoveredRelays = relayListEvent.tags
|
|
108
171
|
.filter(t => t[0] === 'r')
|
|
@@ -121,15 +184,11 @@ export class NCC05Resolver {
|
|
|
121
184
|
};
|
|
122
185
|
|
|
123
186
|
const queryPromise = this.pool.querySync(queryRelays, filter);
|
|
124
|
-
const timeoutPromise = new Promise<null>((
|
|
125
|
-
setTimeout(() => resolve(null), this.timeout)
|
|
126
|
-
);
|
|
127
|
-
|
|
187
|
+
const timeoutPromise = new Promise<null>((r) => setTimeout(() => r(null), this.timeout));
|
|
128
188
|
const result = await Promise.race([queryPromise, timeoutPromise]);
|
|
129
189
|
|
|
130
190
|
if (!result || (Array.isArray(result) && result.length === 0)) return null;
|
|
131
191
|
|
|
132
|
-
// 2. Filter for valid signatures and sort by created_at
|
|
133
192
|
const validEvents = (result as Event[])
|
|
134
193
|
.filter(e => verifyEvent(e))
|
|
135
194
|
.sort((a, b) => b.created_at - a.created_at);
|
|
@@ -137,87 +196,156 @@ export class NCC05Resolver {
|
|
|
137
196
|
if (validEvents.length === 0) return null;
|
|
138
197
|
const latestEvent = validEvents[0];
|
|
139
198
|
|
|
140
|
-
// 2. Decrypt
|
|
141
199
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
200
|
+
let content = latestEvent.content;
|
|
201
|
+
|
|
202
|
+
// Handle "Wrapped" multi-recipient content
|
|
203
|
+
if (content.includes('"wraps"') && secretKey) {
|
|
204
|
+
const wrapped = JSON.parse(content) as WrappedContent;
|
|
205
|
+
const myPk = getPublicKey(secretKey);
|
|
206
|
+
const myWrap = wrapped.wraps[myPk];
|
|
207
|
+
|
|
208
|
+
if (myWrap) {
|
|
209
|
+
const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
|
|
210
|
+
const symmetricKeyHex = nip44.decrypt(myWrap, conversationKey);
|
|
211
|
+
const symmetricKey = new Uint8Array(symmetricKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
|
|
212
|
+
|
|
213
|
+
const sessionConversationKey = nip44.getConversationKey(symmetricKey, getPublicKey(symmetricKey));
|
|
214
|
+
content = nip44.decrypt(wrapped.ciphertext, sessionConversationKey);
|
|
215
|
+
}
|
|
216
|
+
} else if (secretKey) {
|
|
217
|
+
// Standard NIP-44
|
|
218
|
+
const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
|
|
219
|
+
content = nip44.decrypt(latestEvent.content, conversationKey);
|
|
150
220
|
}
|
|
151
221
|
|
|
152
|
-
|
|
222
|
+
const payload = JSON.parse(content) as NCC05Payload;
|
|
223
|
+
if (!payload.endpoints) return null;
|
|
224
|
+
|
|
225
|
+
// Freshness validation
|
|
153
226
|
const now = Math.floor(Date.now() / 1000);
|
|
154
227
|
if (now > payload.updated_at + payload.ttl) {
|
|
155
|
-
if (options.strict)
|
|
156
|
-
console.warn('Rejecting expired NCC-05 record (strict mode)');
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
228
|
+
if (options.strict) return null;
|
|
159
229
|
console.warn('NCC-05 record has expired');
|
|
160
230
|
}
|
|
161
231
|
|
|
162
232
|
return payload;
|
|
163
233
|
} catch (e) {
|
|
164
|
-
console.error('Failed to decrypt or parse NCC-05 record:', e);
|
|
165
234
|
return null;
|
|
166
235
|
}
|
|
167
236
|
}
|
|
168
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Closes connections to all relays in the pool.
|
|
240
|
+
*/
|
|
169
241
|
close() {
|
|
170
242
|
this.pool.close(this.bootstrapRelays);
|
|
171
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) {
|
|
180
|
-
// @ts-ignore
|
|
258
|
+
// @ts-ignore
|
|
181
259
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
182
260
|
}
|
|
183
261
|
}
|
|
184
262
|
|
|
185
263
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
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
|
+
*/
|
|
274
|
+
async publishWrapped(
|
|
275
|
+
relays: string[],
|
|
276
|
+
secretKey: Uint8Array,
|
|
277
|
+
recipients: string[],
|
|
278
|
+
payload: NCC05Payload,
|
|
279
|
+
identifier: string = 'addr'
|
|
280
|
+
): Promise<Event> {
|
|
281
|
+
const sessionKey = generateSecretKey();
|
|
282
|
+
const sessionKeyHex = Array.from(sessionKey).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
283
|
+
|
|
284
|
+
const selfConversation = nip44.getConversationKey(sessionKey, getPublicKey(sessionKey));
|
|
285
|
+
const ciphertext = nip44.encrypt(JSON.stringify(payload), selfConversation);
|
|
286
|
+
|
|
287
|
+
const wraps: Record<string, string> = {};
|
|
288
|
+
for (const rPk of recipients) {
|
|
289
|
+
const conversationKey = nip44.getConversationKey(secretKey, rPk);
|
|
290
|
+
wraps[rPk] = nip44.encrypt(sessionKeyHex, conversationKey);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const wrappedContent: WrappedContent = { ciphertext, wraps };
|
|
294
|
+
|
|
295
|
+
const eventTemplate = {
|
|
296
|
+
kind: 30058,
|
|
297
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
298
|
+
tags: [['d', identifier]],
|
|
299
|
+
content: JSON.stringify(wrappedContent),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
303
|
+
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
304
|
+
return signedEvent;
|
|
305
|
+
}
|
|
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.
|
|
188
315
|
*/
|
|
189
316
|
async publish(
|
|
190
317
|
relays: string[],
|
|
191
318
|
secretKey: Uint8Array,
|
|
192
319
|
payload: NCC05Payload,
|
|
193
|
-
|
|
194
|
-
recipientPubkey?: string
|
|
320
|
+
options: { identifier?: string, recipientPubkey?: string, public?: boolean } = {}
|
|
195
321
|
): Promise<Event> {
|
|
196
322
|
const myPubkey = getPublicKey(secretKey);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
323
|
+
const identifier = options.identifier || 'addr';
|
|
324
|
+
let content = JSON.stringify(payload);
|
|
325
|
+
|
|
326
|
+
if (!options.public) {
|
|
327
|
+
const encryptionTarget = options.recipientPubkey || myPubkey;
|
|
328
|
+
const conversationKey = nip44.getConversationKey(secretKey, encryptionTarget);
|
|
329
|
+
content = nip44.encrypt(content, conversationKey);
|
|
330
|
+
}
|
|
202
331
|
|
|
203
|
-
// 2. Create and Finalize Event
|
|
204
332
|
const eventTemplate = {
|
|
205
333
|
kind: 30058,
|
|
206
334
|
created_at: Math.floor(Date.now() / 1000),
|
|
207
335
|
pubkey: myPubkey,
|
|
208
336
|
tags: [['d', identifier]],
|
|
209
|
-
content:
|
|
337
|
+
content: content,
|
|
210
338
|
};
|
|
211
339
|
|
|
212
340
|
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
213
|
-
|
|
214
|
-
// 3. Publish
|
|
215
341
|
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
216
|
-
|
|
217
342
|
return signedEvent;
|
|
218
343
|
}
|
|
219
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Closes connections to the specified relays.
|
|
347
|
+
*/
|
|
220
348
|
close(relays: string[]) {
|
|
221
349
|
this.pool.close(relays);
|
|
222
350
|
}
|
|
223
|
-
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
|
|
3
|
+
export class MockRelay {
|
|
4
|
+
private wss: WebSocketServer;
|
|
5
|
+
private events: any[] = [];
|
|
6
|
+
|
|
7
|
+
constructor(port: number = 8080) {
|
|
8
|
+
this.wss = new WebSocketServer({ port });
|
|
9
|
+
this.wss.on('connection', (ws: WebSocket) => {
|
|
10
|
+
ws.on('message', (data: string) => {
|
|
11
|
+
const msg = JSON.parse(data);
|
|
12
|
+
const type = msg[0];
|
|
13
|
+
|
|
14
|
+
if (type === 'EVENT') {
|
|
15
|
+
const event = msg[1];
|
|
16
|
+
// Basic replaceable logic for test consistency
|
|
17
|
+
if (event.kind === 30058 || event.kind === 10002) {
|
|
18
|
+
const dTag = event.tags.find((t: any) => t[0] === 'd')?.[1] || "";
|
|
19
|
+
this.events = this.events.filter(e =>
|
|
20
|
+
!(e.pubkey === event.pubkey && e.kind === event.kind && (e.tags.find((t: any) => t[0] === 'd')?.[1] || "") === dTag)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
this.events.push(event);
|
|
24
|
+
ws.send(JSON.stringify(["OK", event.id, true, ""]));
|
|
25
|
+
} else if (type === 'REQ') {
|
|
26
|
+
const subId = msg[1];
|
|
27
|
+
const filters = msg[2];
|
|
28
|
+
|
|
29
|
+
this.events.forEach(event => {
|
|
30
|
+
let match = true;
|
|
31
|
+
if (filters.authors && !filters.authors.includes(event.pubkey)) match = false;
|
|
32
|
+
if (filters.kinds && !filters.kinds.includes(event.kind)) match = false;
|
|
33
|
+
if (filters['#d']) {
|
|
34
|
+
const dTag = event.tags.find((t: any) => t[0] === 'd')?.[1];
|
|
35
|
+
if (!filters['#d'].includes(dTag)) match = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (match) {
|
|
39
|
+
ws.send(JSON.stringify(["EVENT", subId, event]));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
ws.send(JSON.stringify(["EOSE", subId]));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
stop() {
|
|
49
|
+
this.wss.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/test.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { NCC05Publisher, NCC05Resolver, NCC05Payload, NCC05Group } from './index.js';
|
|
2
|
-
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
|
2
|
+
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
|
3
|
+
import { MockRelay } from './mock-relay.js';
|
|
3
4
|
|
|
4
5
|
async function test() {
|
|
6
|
+
console.log('Starting Mock Relay...');
|
|
7
|
+
const relay = new MockRelay(8080);
|
|
8
|
+
const relays = ['ws://localhost:8080'];
|
|
9
|
+
|
|
5
10
|
const sk = generateSecretKey();
|
|
6
11
|
const pk = getPublicKey(sk);
|
|
7
|
-
const relays = ['wss://relay.damus.io'];
|
|
8
12
|
|
|
9
13
|
const publisher = new NCC05Publisher();
|
|
10
14
|
const resolver = new NCC05Resolver({ bootstrapRelays: relays });
|
|
@@ -28,7 +32,8 @@ async function test() {
|
|
|
28
32
|
if (resolved) {
|
|
29
33
|
console.log('Successfully resolved:', JSON.stringify(resolved, null, 2));
|
|
30
34
|
} else {
|
|
31
|
-
console.
|
|
35
|
+
console.error('FAILED: resolution.');
|
|
36
|
+
process.exit(1);
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
// Test Strict Mode with Expired Record
|
|
@@ -39,7 +44,7 @@ async function test() {
|
|
|
39
44
|
updated_at: Math.floor(Date.now() / 1000) - 10, // 10s ago
|
|
40
45
|
endpoints: [{ type: 'tcp', uri: '1.1.1.1:1', priority: 1, family: 'ipv4' }]
|
|
41
46
|
};
|
|
42
|
-
await publisher.publish(relays, sk, expiredPayload, 'expired-test');
|
|
47
|
+
await publisher.publish(relays, sk, expiredPayload, { identifier: 'expired-test' });
|
|
43
48
|
const strictResult = await resolver.resolve(pk, sk, 'expired-test', { strict: true });
|
|
44
49
|
|
|
45
50
|
if (strictResult === null) {
|
|
@@ -51,8 +56,6 @@ async function test() {
|
|
|
51
56
|
|
|
52
57
|
// Test Gossip Mode
|
|
53
58
|
console.log('Testing Gossip discovery...');
|
|
54
|
-
// In this test, we just point kind:10002 to the same relay we are using
|
|
55
|
-
// to verify the code path executes.
|
|
56
59
|
const relayListTemplate = {
|
|
57
60
|
kind: 10002,
|
|
58
61
|
created_at: Math.floor(Date.now() / 1000),
|
|
@@ -60,6 +63,7 @@ async function test() {
|
|
|
60
63
|
content: '',
|
|
61
64
|
};
|
|
62
65
|
const signedRL = (await import('nostr-tools')).finalizeEvent(relayListTemplate, sk);
|
|
66
|
+
// @ts-ignore
|
|
63
67
|
await Promise.all(publisher['pool'].publish(relays, signedRL));
|
|
64
68
|
|
|
65
69
|
const gossipResult = await resolver.resolve(pk, sk, 'addr', { gossip: true });
|
|
@@ -72,7 +76,7 @@ async function test() {
|
|
|
72
76
|
|
|
73
77
|
// Test npub resolution
|
|
74
78
|
console.log('Testing npub resolution...');
|
|
75
|
-
const npub =
|
|
79
|
+
const npub = nip19.npubEncode(pk);
|
|
76
80
|
const npubResult = await resolver.resolve(npub, sk);
|
|
77
81
|
if (npubResult) {
|
|
78
82
|
console.log('npub resolution successful.');
|
|
@@ -95,7 +99,17 @@ async function test() {
|
|
|
95
99
|
|
|
96
100
|
// User A publishes for User B
|
|
97
101
|
console.log('User A publishing for User B...');
|
|
98
|
-
await publisher.publish(relays, skA, payloadFriend, 'friend-test', pkB);
|
|
102
|
+
await publisher.publish(relays, skA, payloadFriend, { identifier: 'friend-test', recipientPubkey: pkB });
|
|
103
|
+
|
|
104
|
+
// User B resolves User A's record
|
|
105
|
+
console.log('User B resolving User A...');
|
|
106
|
+
const friendResult = await resolver.resolve(pkA, skB, 'friend-test');
|
|
107
|
+
if (friendResult && friendResult.endpoints[0].uri === 'friend:7777') {
|
|
108
|
+
console.log('Friend-to-Friend resolution successful.');
|
|
109
|
+
} else {
|
|
110
|
+
console.error('FAILED: Friend-to-Friend resolution.');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
99
113
|
|
|
100
114
|
// Test Group Resolution Utility
|
|
101
115
|
console.log('Testing NCC05Group utility...');
|
|
@@ -106,7 +120,7 @@ async function test() {
|
|
|
106
120
|
};
|
|
107
121
|
|
|
108
122
|
console.log('Publishing as Group...');
|
|
109
|
-
await publisher.publish(relays, groupIdentity.sk, payloadGroup, 'group-test');
|
|
123
|
+
await publisher.publish(relays, groupIdentity.sk, payloadGroup, { identifier: 'group-test' });
|
|
110
124
|
|
|
111
125
|
console.log('Resolving as Group Member...');
|
|
112
126
|
const groupResult = await NCC05Group.resolveAsGroup(resolver, groupIdentity.pk, groupIdentity.sk, 'group-test');
|
|
@@ -117,8 +131,40 @@ async function test() {
|
|
|
117
131
|
process.exit(1);
|
|
118
132
|
}
|
|
119
133
|
|
|
134
|
+
// Test Group Wrapping (Multi-Recipient)
|
|
135
|
+
console.log('Testing Group Wrapping (Multi-Recipient)...');
|
|
136
|
+
const skAlice = generateSecretKey();
|
|
137
|
+
const pkAlice = getPublicKey(skAlice);
|
|
138
|
+
const skBob = generateSecretKey();
|
|
139
|
+
const pkBob = getPublicKey(skBob);
|
|
140
|
+
const skCharlie = generateSecretKey();
|
|
141
|
+
const pkCharlie = getPublicKey(skCharlie);
|
|
142
|
+
|
|
143
|
+
const payloadWrap: NCC05Payload = {
|
|
144
|
+
v: 1, ttl: 60, updated_at: Math.floor(Date.now() / 1000),
|
|
145
|
+
endpoints: [{ type: 'tcp', uri: 'multi-recipient:9999', priority: 1, family: 'ipv4' }]
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
console.log('Alice publishing wrapped record for Bob and Charlie...');
|
|
149
|
+
await publisher.publishWrapped(relays, skAlice, [pkBob, pkCharlie], payloadWrap, 'wrap-test');
|
|
150
|
+
|
|
151
|
+
console.log('Bob resolving Alice...');
|
|
152
|
+
const bobResult = await resolver.resolve(pkAlice, skBob, 'wrap-test');
|
|
153
|
+
|
|
154
|
+
console.log('Charlie resolving Alice...');
|
|
155
|
+
const charlieResult = await resolver.resolve(pkAlice, skCharlie, 'wrap-test');
|
|
156
|
+
|
|
157
|
+
if (bobResult && charlieResult && bobResult.endpoints[0].uri === 'multi-recipient:9999') {
|
|
158
|
+
console.log('Group Wrapping successful! Both recipients resolved Alice.');
|
|
159
|
+
} else {
|
|
160
|
+
console.error('FAILED: Group Wrapping resolution.');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
120
164
|
publisher.close(relays);
|
|
121
165
|
resolver.close();
|
|
166
|
+
relay.stop();
|
|
167
|
+
console.log('Local Mock Test Suite Passed.');
|
|
122
168
|
process.exit(0);
|
|
123
169
|
}
|
|
124
170
|
|