ncc-05 1.1.0 → 1.1.1
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/dist/index.d.ts +15 -15
- package/dist/index.js +63 -54
- package/dist/mock-relay.d.ts +6 -0
- package/dist/mock-relay.js +47 -0
- package/dist/test.js +49 -9
- package/package.json +4 -2
- package/src/index.ts +89 -64
- package/src/mock-relay.ts +51 -0
- package/src/test.ts +55 -9
package/dist/index.d.ts
CHANGED
|
@@ -19,17 +19,20 @@ export interface ResolverOptions {
|
|
|
19
19
|
websocketImplementation?: any;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Advanced multi-recipient "wrapping" structure.
|
|
23
23
|
*/
|
|
24
|
+
export interface WrappedContent {
|
|
25
|
+
/** The actual payload encrypted with a random symmetric key */
|
|
26
|
+
ciphertext: string;
|
|
27
|
+
/** Map of recipient pubkey -> wrapped symmetric key */
|
|
28
|
+
wraps: Record<string, string>;
|
|
29
|
+
}
|
|
24
30
|
export declare class NCC05Group {
|
|
25
|
-
/**
|
|
26
|
-
* Generate a new shared identity for a group.
|
|
27
|
-
* The nsec should be shared with all authorized members.
|
|
28
|
-
*/
|
|
29
31
|
static createGroupIdentity(): {
|
|
30
32
|
nsec: `nsec1${string}`;
|
|
31
33
|
sk: Uint8Array<ArrayBufferLike>;
|
|
32
34
|
pk: string;
|
|
35
|
+
npub: `npub1${string}`;
|
|
33
36
|
};
|
|
34
37
|
/**
|
|
35
38
|
* Resolve a record that was published using a group's shared identity.
|
|
@@ -41,11 +44,7 @@ export declare class NCC05Resolver {
|
|
|
41
44
|
private bootstrapRelays;
|
|
42
45
|
private timeout;
|
|
43
46
|
constructor(options?: ResolverOptions);
|
|
44
|
-
|
|
45
|
-
* Resolve a locator record for a given pubkey.
|
|
46
|
-
* Supports both hex and npub strings.
|
|
47
|
-
*/
|
|
48
|
-
resolve(targetPubkey: string, secretKey: Uint8Array, identifier?: string, options?: {
|
|
47
|
+
resolve(targetPubkey: string, secretKey?: Uint8Array, identifier?: string, options?: {
|
|
49
48
|
strict?: boolean;
|
|
50
49
|
gossip?: boolean;
|
|
51
50
|
}): Promise<NCC05Payload | null>;
|
|
@@ -56,10 +55,11 @@ export declare class NCC05Publisher {
|
|
|
56
55
|
constructor(options?: {
|
|
57
56
|
websocketImplementation?: any;
|
|
58
57
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
publishWrapped(relays: string[], secretKey: Uint8Array, recipients: string[], payload: NCC05Payload, identifier?: string): Promise<Event>;
|
|
59
|
+
publish(relays: string[], secretKey: Uint8Array, payload: NCC05Payload, options?: {
|
|
60
|
+
identifier?: string;
|
|
61
|
+
recipientPubkey?: string;
|
|
62
|
+
public?: boolean;
|
|
63
|
+
}): Promise<Event>;
|
|
64
64
|
close(relays: string[]): void;
|
|
65
65
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
import { SimplePool, nip44, nip19, finalizeEvent, verifyEvent, getPublicKey, generateSecretKey } from 'nostr-tools';
|
|
2
|
-
/**
|
|
3
|
-
* Utility for managing shared group access to NCC-05 records.
|
|
4
|
-
*/
|
|
5
2
|
export class NCC05Group {
|
|
6
|
-
/**
|
|
7
|
-
* Generate a new shared identity for a group.
|
|
8
|
-
* The nsec should be shared with all authorized members.
|
|
9
|
-
*/
|
|
10
3
|
static createGroupIdentity() {
|
|
11
4
|
const sk = generateSecretKey();
|
|
5
|
+
const pk = getPublicKey(sk);
|
|
12
6
|
return {
|
|
13
7
|
nsec: nip19.nsecEncode(sk),
|
|
14
8
|
sk: sk,
|
|
15
|
-
pk:
|
|
9
|
+
pk: pk,
|
|
10
|
+
npub: nip19.npubEncode(pk)
|
|
16
11
|
};
|
|
17
12
|
}
|
|
18
13
|
/**
|
|
19
14
|
* Resolve a record that was published using a group's shared identity.
|
|
20
15
|
*/
|
|
21
16
|
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
17
|
return resolver.resolve(groupPubkey, groupSecretKey, identifier);
|
|
25
18
|
}
|
|
26
19
|
}
|
|
@@ -31,16 +24,12 @@ export class NCC05Resolver {
|
|
|
31
24
|
constructor(options = {}) {
|
|
32
25
|
this.pool = new SimplePool();
|
|
33
26
|
if (options.websocketImplementation) {
|
|
34
|
-
// @ts-ignore
|
|
27
|
+
// @ts-ignore
|
|
35
28
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
36
29
|
}
|
|
37
30
|
this.bootstrapRelays = options.bootstrapRelays || ['wss://relay.damus.io', 'wss://nos.lol'];
|
|
38
31
|
this.timeout = options.timeout || 10000;
|
|
39
32
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Resolve a locator record for a given pubkey.
|
|
42
|
-
* Supports both hex and npub strings.
|
|
43
|
-
*/
|
|
44
33
|
async resolve(targetPubkey, secretKey, identifier = 'addr', options = {}) {
|
|
45
34
|
let hexPubkey = targetPubkey;
|
|
46
35
|
if (targetPubkey.startsWith('npub1')) {
|
|
@@ -48,7 +37,6 @@ export class NCC05Resolver {
|
|
|
48
37
|
hexPubkey = decoded.data;
|
|
49
38
|
}
|
|
50
39
|
let queryRelays = [...this.bootstrapRelays];
|
|
51
|
-
// 1. NIP-65 Gossip Discovery
|
|
52
40
|
if (options.gossip) {
|
|
53
41
|
const relayListEvent = await this.pool.get(this.bootstrapRelays, {
|
|
54
42
|
authors: [hexPubkey],
|
|
@@ -58,9 +46,7 @@ export class NCC05Resolver {
|
|
|
58
46
|
const discoveredRelays = relayListEvent.tags
|
|
59
47
|
.filter(t => t[0] === 'r')
|
|
60
48
|
.map(t => t[1]);
|
|
61
|
-
|
|
62
|
-
queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
|
|
63
|
-
}
|
|
49
|
+
queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
|
|
64
50
|
}
|
|
65
51
|
}
|
|
66
52
|
const filter = {
|
|
@@ -70,80 +56,103 @@ export class NCC05Resolver {
|
|
|
70
56
|
limit: 10
|
|
71
57
|
};
|
|
72
58
|
const queryPromise = this.pool.querySync(queryRelays, filter);
|
|
73
|
-
const timeoutPromise = new Promise((
|
|
59
|
+
const timeoutPromise = new Promise((r) => setTimeout(() => r(null), this.timeout));
|
|
74
60
|
const result = await Promise.race([queryPromise, timeoutPromise]);
|
|
75
61
|
if (!result || (Array.isArray(result) && result.length === 0))
|
|
76
62
|
return null;
|
|
77
|
-
// 2. Filter for valid signatures and sort by created_at
|
|
78
63
|
const validEvents = result
|
|
79
64
|
.filter(e => verifyEvent(e))
|
|
80
65
|
.sort((a, b) => b.created_at - a.created_at);
|
|
81
66
|
if (validEvents.length === 0)
|
|
82
67
|
return null;
|
|
83
68
|
const latestEvent = validEvents[0];
|
|
84
|
-
// 2. Decrypt
|
|
85
69
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
70
|
+
let content = latestEvent.content;
|
|
71
|
+
// 1. Try to detect if it's a "Wrapped" multi-recipient event
|
|
72
|
+
if (content.includes('"wraps"') && secretKey) {
|
|
73
|
+
const wrapped = JSON.parse(content);
|
|
74
|
+
const myPk = getPublicKey(secretKey);
|
|
75
|
+
const myWrap = wrapped.wraps[myPk];
|
|
76
|
+
if (myWrap) {
|
|
77
|
+
const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
|
|
78
|
+
const symmetricKeyHex = nip44.decrypt(myWrap, conversationKey);
|
|
79
|
+
// Convert hex symmetric key back to Uint8Array for NIP-44 decryption
|
|
80
|
+
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
|
+
const sessionConversationKey = nip44.getConversationKey(symmetricKey, getPublicKey(symmetricKey));
|
|
83
|
+
content = nip44.decrypt(wrapped.ciphertext, sessionConversationKey);
|
|
84
|
+
}
|
|
93
85
|
}
|
|
94
|
-
|
|
86
|
+
else if (secretKey) {
|
|
87
|
+
const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
|
|
88
|
+
content = nip44.decrypt(latestEvent.content, conversationKey);
|
|
89
|
+
}
|
|
90
|
+
const payload = JSON.parse(content);
|
|
91
|
+
if (!payload.endpoints)
|
|
92
|
+
return null;
|
|
95
93
|
const now = Math.floor(Date.now() / 1000);
|
|
96
94
|
if (now > payload.updated_at + payload.ttl) {
|
|
97
|
-
if (options.strict)
|
|
98
|
-
console.warn('Rejecting expired NCC-05 record (strict mode)');
|
|
95
|
+
if (options.strict)
|
|
99
96
|
return null;
|
|
100
|
-
|
|
101
|
-
console.warn('NCC-05 record has expired');
|
|
97
|
+
console.warn('NCC-05 record expired');
|
|
102
98
|
}
|
|
103
99
|
return payload;
|
|
104
100
|
}
|
|
105
101
|
catch (e) {
|
|
106
|
-
console.error('Failed to decrypt or parse NCC-05 record:', e);
|
|
107
102
|
return null;
|
|
108
103
|
}
|
|
109
104
|
}
|
|
110
|
-
close() {
|
|
111
|
-
this.pool.close(this.bootstrapRelays);
|
|
112
|
-
}
|
|
105
|
+
close() { this.pool.close(this.bootstrapRelays); }
|
|
113
106
|
}
|
|
114
107
|
export class NCC05Publisher {
|
|
115
108
|
pool;
|
|
116
109
|
constructor(options = {}) {
|
|
117
110
|
this.pool = new SimplePool();
|
|
118
111
|
if (options.websocketImplementation) {
|
|
119
|
-
// @ts-ignore
|
|
112
|
+
// @ts-ignore
|
|
120
113
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
121
114
|
}
|
|
122
115
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
async publishWrapped(relays, secretKey, recipients, payload, identifier = 'addr') {
|
|
117
|
+
const sessionKey = generateSecretKey();
|
|
118
|
+
const sessionKeyHex = Array.from(sessionKey).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
119
|
+
const selfConversation = nip44.getConversationKey(sessionKey, getPublicKey(sessionKey));
|
|
120
|
+
const ciphertext = nip44.encrypt(JSON.stringify(payload), selfConversation);
|
|
121
|
+
const wraps = {};
|
|
122
|
+
for (const rPk of recipients) {
|
|
123
|
+
const conversationKey = nip44.getConversationKey(secretKey, rPk);
|
|
124
|
+
wraps[rPk] = nip44.encrypt(sessionKeyHex, conversationKey);
|
|
125
|
+
}
|
|
126
|
+
const wrappedContent = { ciphertext, wraps };
|
|
127
|
+
const eventTemplate = {
|
|
128
|
+
kind: 30058,
|
|
129
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
130
|
+
tags: [['d', identifier]],
|
|
131
|
+
content: JSON.stringify(wrappedContent),
|
|
132
|
+
};
|
|
133
|
+
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
134
|
+
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
135
|
+
return signedEvent;
|
|
136
|
+
}
|
|
137
|
+
async publish(relays, secretKey, payload, options = {}) {
|
|
128
138
|
const myPubkey = getPublicKey(secretKey);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
const identifier = options.identifier || 'addr';
|
|
140
|
+
let content = JSON.stringify(payload);
|
|
141
|
+
if (!options.public) {
|
|
142
|
+
const encryptionTarget = options.recipientPubkey || myPubkey;
|
|
143
|
+
const conversationKey = nip44.getConversationKey(secretKey, encryptionTarget);
|
|
144
|
+
content = nip44.encrypt(content, conversationKey);
|
|
145
|
+
}
|
|
134
146
|
const eventTemplate = {
|
|
135
147
|
kind: 30058,
|
|
136
148
|
created_at: Math.floor(Date.now() / 1000),
|
|
137
149
|
pubkey: myPubkey,
|
|
138
150
|
tags: [['d', identifier]],
|
|
139
|
-
content:
|
|
151
|
+
content: content,
|
|
140
152
|
};
|
|
141
153
|
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
142
|
-
// 3. Publish
|
|
143
154
|
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
144
155
|
return signedEvent;
|
|
145
156
|
}
|
|
146
|
-
close(relays) {
|
|
147
|
-
this.pool.close(relays);
|
|
148
|
-
}
|
|
157
|
+
close(relays) { this.pool.close(relays); }
|
|
149
158
|
}
|
|
@@ -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.1",
|
|
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",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"nostr-tools": "^2.10.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.0.0",
|
|
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
|
@@ -28,23 +28,28 @@ export interface NCC05Payload {
|
|
|
28
28
|
export interface ResolverOptions {
|
|
29
29
|
bootstrapRelays?: string[];
|
|
30
30
|
timeout?: number;
|
|
31
|
-
websocketImplementation?: any;
|
|
31
|
+
websocketImplementation?: any;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* Advanced multi-recipient "wrapping" structure.
|
|
36
36
|
*/
|
|
37
|
+
export interface WrappedContent {
|
|
38
|
+
/** The actual payload encrypted with a random symmetric key */
|
|
39
|
+
ciphertext: string;
|
|
40
|
+
/** Map of recipient pubkey -> wrapped symmetric key */
|
|
41
|
+
wraps: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
export class NCC05Group {
|
|
38
|
-
/**
|
|
39
|
-
* Generate a new shared identity for a group.
|
|
40
|
-
* The nsec should be shared with all authorized members.
|
|
41
|
-
*/
|
|
42
45
|
static createGroupIdentity() {
|
|
43
46
|
const sk = generateSecretKey();
|
|
47
|
+
const pk = getPublicKey(sk);
|
|
44
48
|
return {
|
|
45
49
|
nsec: nip19.nsecEncode(sk),
|
|
46
50
|
sk: sk,
|
|
47
|
-
pk:
|
|
51
|
+
pk: pk,
|
|
52
|
+
npub: nip19.npubEncode(pk)
|
|
48
53
|
};
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -57,8 +62,6 @@ export class NCC05Group {
|
|
|
57
62
|
groupSecretKey: Uint8Array,
|
|
58
63
|
identifier: string = 'addr'
|
|
59
64
|
): 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
65
|
return resolver.resolve(groupPubkey, groupSecretKey, identifier);
|
|
63
66
|
}
|
|
64
67
|
}
|
|
@@ -71,20 +74,16 @@ export class NCC05Resolver {
|
|
|
71
74
|
constructor(options: ResolverOptions = {}) {
|
|
72
75
|
this.pool = new SimplePool();
|
|
73
76
|
if (options.websocketImplementation) {
|
|
74
|
-
// @ts-ignore
|
|
77
|
+
// @ts-ignore
|
|
75
78
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
76
79
|
}
|
|
77
80
|
this.bootstrapRelays = options.bootstrapRelays || ['wss://relay.damus.io', 'wss://nos.lol'];
|
|
78
81
|
this.timeout = options.timeout || 10000;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
/**
|
|
82
|
-
* Resolve a locator record for a given pubkey.
|
|
83
|
-
* Supports both hex and npub strings.
|
|
84
|
-
*/
|
|
85
84
|
async resolve(
|
|
86
85
|
targetPubkey: string,
|
|
87
|
-
secretKey
|
|
86
|
+
secretKey?: Uint8Array,
|
|
88
87
|
identifier: string = 'addr',
|
|
89
88
|
options: { strict?: boolean, gossip?: boolean } = {}
|
|
90
89
|
): Promise<NCC05Payload | null> {
|
|
@@ -96,20 +95,16 @@ export class NCC05Resolver {
|
|
|
96
95
|
|
|
97
96
|
let queryRelays = [...this.bootstrapRelays];
|
|
98
97
|
|
|
99
|
-
// 1. NIP-65 Gossip Discovery
|
|
100
98
|
if (options.gossip) {
|
|
101
99
|
const relayListEvent = await this.pool.get(this.bootstrapRelays, {
|
|
102
100
|
authors: [hexPubkey],
|
|
103
101
|
kinds: [10002]
|
|
104
102
|
});
|
|
105
|
-
|
|
106
103
|
if (relayListEvent) {
|
|
107
104
|
const discoveredRelays = relayListEvent.tags
|
|
108
105
|
.filter(t => t[0] === 'r')
|
|
109
106
|
.map(t => t[1]);
|
|
110
|
-
|
|
111
|
-
queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
|
|
112
|
-
}
|
|
107
|
+
queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
|
|
113
108
|
}
|
|
114
109
|
}
|
|
115
110
|
|
|
@@ -121,15 +116,11 @@ export class NCC05Resolver {
|
|
|
121
116
|
};
|
|
122
117
|
|
|
123
118
|
const queryPromise = this.pool.querySync(queryRelays, filter);
|
|
124
|
-
const timeoutPromise = new Promise<null>((
|
|
125
|
-
setTimeout(() => resolve(null), this.timeout)
|
|
126
|
-
);
|
|
127
|
-
|
|
119
|
+
const timeoutPromise = new Promise<null>((r) => setTimeout(() => r(null), this.timeout));
|
|
128
120
|
const result = await Promise.race([queryPromise, timeoutPromise]);
|
|
129
121
|
|
|
130
122
|
if (!result || (Array.isArray(result) && result.length === 0)) return null;
|
|
131
123
|
|
|
132
|
-
// 2. Filter for valid signatures and sort by created_at
|
|
133
124
|
const validEvents = (result as Event[])
|
|
134
125
|
.filter(e => verifyEvent(e))
|
|
135
126
|
.sort((a, b) => b.created_at - a.created_at);
|
|
@@ -137,38 +128,47 @@ export class NCC05Resolver {
|
|
|
137
128
|
if (validEvents.length === 0) return null;
|
|
138
129
|
const latestEvent = validEvents[0];
|
|
139
130
|
|
|
140
|
-
// 2. Decrypt
|
|
141
131
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
132
|
+
let content = latestEvent.content;
|
|
133
|
+
|
|
134
|
+
// 1. Try to detect if it's a "Wrapped" multi-recipient event
|
|
135
|
+
if (content.includes('"wraps"') && secretKey) {
|
|
136
|
+
const wrapped = JSON.parse(content) as WrappedContent;
|
|
137
|
+
const myPk = getPublicKey(secretKey);
|
|
138
|
+
const myWrap = wrapped.wraps[myPk];
|
|
139
|
+
|
|
140
|
+
if (myWrap) {
|
|
141
|
+
const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
|
|
142
|
+
const symmetricKeyHex = nip44.decrypt(myWrap, conversationKey);
|
|
143
|
+
|
|
144
|
+
// Convert hex symmetric key back to Uint8Array for NIP-44 decryption
|
|
145
|
+
const symmetricKey = new Uint8Array(symmetricKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
|
|
146
|
+
|
|
147
|
+
// The payload was self-encrypted by the publisher with the session key
|
|
148
|
+
const sessionConversationKey = nip44.getConversationKey(symmetricKey, getPublicKey(symmetricKey));
|
|
149
|
+
content = nip44.decrypt(wrapped.ciphertext, sessionConversationKey);
|
|
150
|
+
}
|
|
151
|
+
} else if (secretKey) {
|
|
152
|
+
const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
|
|
153
|
+
content = nip44.decrypt(latestEvent.content, conversationKey);
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
const payload = JSON.parse(content) as NCC05Payload;
|
|
157
|
+
if (!payload.endpoints) return null;
|
|
158
|
+
|
|
153
159
|
const now = Math.floor(Date.now() / 1000);
|
|
154
160
|
if (now > payload.updated_at + payload.ttl) {
|
|
155
|
-
if (options.strict)
|
|
156
|
-
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
console.warn('NCC-05 record has expired');
|
|
161
|
+
if (options.strict) return null;
|
|
162
|
+
console.warn('NCC-05 record expired');
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
return payload;
|
|
163
166
|
} catch (e) {
|
|
164
|
-
console.error('Failed to decrypt or parse NCC-05 record:', e);
|
|
165
167
|
return null;
|
|
166
168
|
}
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
close() {
|
|
170
|
-
this.pool.close(this.bootstrapRelays);
|
|
171
|
-
}
|
|
171
|
+
close() { this.pool.close(this.bootstrapRelays); }
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
export class NCC05Publisher {
|
|
@@ -177,47 +177,72 @@ export class NCC05Publisher {
|
|
|
177
177
|
constructor(options: { websocketImplementation?: any } = {}) {
|
|
178
178
|
this.pool = new SimplePool();
|
|
179
179
|
if (options.websocketImplementation) {
|
|
180
|
-
// @ts-ignore
|
|
180
|
+
// @ts-ignore
|
|
181
181
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
async publishWrapped(
|
|
186
|
+
relays: string[],
|
|
187
|
+
secretKey: Uint8Array,
|
|
188
|
+
recipients: string[],
|
|
189
|
+
payload: NCC05Payload,
|
|
190
|
+
identifier: string = 'addr'
|
|
191
|
+
): Promise<Event> {
|
|
192
|
+
const sessionKey = generateSecretKey();
|
|
193
|
+
const sessionKeyHex = Array.from(sessionKey).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
194
|
+
|
|
195
|
+
const selfConversation = nip44.getConversationKey(sessionKey, getPublicKey(sessionKey));
|
|
196
|
+
const ciphertext = nip44.encrypt(JSON.stringify(payload), selfConversation);
|
|
197
|
+
|
|
198
|
+
const wraps: Record<string, string> = {};
|
|
199
|
+
for (const rPk of recipients) {
|
|
200
|
+
const conversationKey = nip44.getConversationKey(secretKey, rPk);
|
|
201
|
+
wraps[rPk] = nip44.encrypt(sessionKeyHex, conversationKey);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const wrappedContent: WrappedContent = { ciphertext, wraps };
|
|
205
|
+
|
|
206
|
+
const eventTemplate = {
|
|
207
|
+
kind: 30058,
|
|
208
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
209
|
+
tags: [['d', identifier]],
|
|
210
|
+
content: JSON.stringify(wrappedContent),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
214
|
+
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
215
|
+
return signedEvent;
|
|
216
|
+
}
|
|
217
|
+
|
|
189
218
|
async publish(
|
|
190
219
|
relays: string[],
|
|
191
220
|
secretKey: Uint8Array,
|
|
192
221
|
payload: NCC05Payload,
|
|
193
|
-
|
|
194
|
-
recipientPubkey?: string
|
|
222
|
+
options: { identifier?: string, recipientPubkey?: string, public?: boolean } = {}
|
|
195
223
|
): Promise<Event> {
|
|
196
224
|
const myPubkey = getPublicKey(secretKey);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
225
|
+
const identifier = options.identifier || 'addr';
|
|
226
|
+
let content = JSON.stringify(payload);
|
|
227
|
+
|
|
228
|
+
if (!options.public) {
|
|
229
|
+
const encryptionTarget = options.recipientPubkey || myPubkey;
|
|
230
|
+
const conversationKey = nip44.getConversationKey(secretKey, encryptionTarget);
|
|
231
|
+
content = nip44.encrypt(content, conversationKey);
|
|
232
|
+
}
|
|
202
233
|
|
|
203
|
-
// 2. Create and Finalize Event
|
|
204
234
|
const eventTemplate = {
|
|
205
235
|
kind: 30058,
|
|
206
236
|
created_at: Math.floor(Date.now() / 1000),
|
|
207
237
|
pubkey: myPubkey,
|
|
208
238
|
tags: [['d', identifier]],
|
|
209
|
-
content:
|
|
239
|
+
content: content,
|
|
210
240
|
};
|
|
211
241
|
|
|
212
242
|
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
213
|
-
|
|
214
|
-
// 3. Publish
|
|
215
243
|
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
216
|
-
|
|
217
244
|
return signedEvent;
|
|
218
245
|
}
|
|
219
246
|
|
|
220
|
-
close(relays: string[]) {
|
|
221
|
-
this.pool.close(relays);
|
|
222
|
-
}
|
|
247
|
+
close(relays: string[]) { this.pool.close(relays); }
|
|
223
248
|
}
|
|
@@ -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
|
|