ncc-05 1.0.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 +28 -9
- package/dist/index.js +80 -43
- package/dist/mock-relay.d.ts +6 -0
- package/dist/mock-relay.js +47 -0
- package/dist/test.js +79 -8
- package/package.json +12 -4
- package/src/index.ts +116 -54
- package/src/mock-relay.ts +51 -0
- package/src/test.ts +90 -8
package/dist/index.d.ts
CHANGED
|
@@ -18,16 +18,33 @@ export interface ResolverOptions {
|
|
|
18
18
|
timeout?: number;
|
|
19
19
|
websocketImplementation?: any;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Advanced multi-recipient "wrapping" structure.
|
|
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
|
+
}
|
|
30
|
+
export declare class NCC05Group {
|
|
31
|
+
static createGroupIdentity(): {
|
|
32
|
+
nsec: `nsec1${string}`;
|
|
33
|
+
sk: Uint8Array<ArrayBufferLike>;
|
|
34
|
+
pk: string;
|
|
35
|
+
npub: `npub1${string}`;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a record that was published using a group's shared identity.
|
|
39
|
+
*/
|
|
40
|
+
static resolveAsGroup(resolver: NCC05Resolver, groupPubkey: string, groupSecretKey: Uint8Array, identifier?: string): Promise<NCC05Payload | null>;
|
|
41
|
+
}
|
|
21
42
|
export declare class NCC05Resolver {
|
|
22
43
|
private pool;
|
|
23
44
|
private bootstrapRelays;
|
|
24
45
|
private timeout;
|
|
25
46
|
constructor(options?: ResolverOptions);
|
|
26
|
-
|
|
27
|
-
* Resolve a locator record for a given pubkey.
|
|
28
|
-
* Supports both hex and npub strings.
|
|
29
|
-
*/
|
|
30
|
-
resolve(targetPubkey: string, secretKey: Uint8Array, identifier?: string, options?: {
|
|
47
|
+
resolve(targetPubkey: string, secretKey?: Uint8Array, identifier?: string, options?: {
|
|
31
48
|
strict?: boolean;
|
|
32
49
|
gossip?: boolean;
|
|
33
50
|
}): Promise<NCC05Payload | null>;
|
|
@@ -38,9 +55,11 @@ export declare class NCC05Publisher {
|
|
|
38
55
|
constructor(options?: {
|
|
39
56
|
websocketImplementation?: any;
|
|
40
57
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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>;
|
|
45
64
|
close(relays: string[]): void;
|
|
46
65
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
import { SimplePool, nip44, nip19, finalizeEvent, verifyEvent, getPublicKey } from 'nostr-tools';
|
|
1
|
+
import { SimplePool, nip44, nip19, finalizeEvent, verifyEvent, getPublicKey, generateSecretKey } from 'nostr-tools';
|
|
2
|
+
export class NCC05Group {
|
|
3
|
+
static createGroupIdentity() {
|
|
4
|
+
const sk = generateSecretKey();
|
|
5
|
+
const pk = getPublicKey(sk);
|
|
6
|
+
return {
|
|
7
|
+
nsec: nip19.nsecEncode(sk),
|
|
8
|
+
sk: sk,
|
|
9
|
+
pk: pk,
|
|
10
|
+
npub: nip19.npubEncode(pk)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a record that was published using a group's shared identity.
|
|
15
|
+
*/
|
|
16
|
+
static async resolveAsGroup(resolver, groupPubkey, groupSecretKey, identifier = 'addr') {
|
|
17
|
+
return resolver.resolve(groupPubkey, groupSecretKey, identifier);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
2
20
|
export class NCC05Resolver {
|
|
3
21
|
pool;
|
|
4
22
|
bootstrapRelays;
|
|
@@ -6,16 +24,12 @@ export class NCC05Resolver {
|
|
|
6
24
|
constructor(options = {}) {
|
|
7
25
|
this.pool = new SimplePool();
|
|
8
26
|
if (options.websocketImplementation) {
|
|
9
|
-
// @ts-ignore
|
|
27
|
+
// @ts-ignore
|
|
10
28
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
11
29
|
}
|
|
12
30
|
this.bootstrapRelays = options.bootstrapRelays || ['wss://relay.damus.io', 'wss://nos.lol'];
|
|
13
31
|
this.timeout = options.timeout || 10000;
|
|
14
32
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Resolve a locator record for a given pubkey.
|
|
17
|
-
* Supports both hex and npub strings.
|
|
18
|
-
*/
|
|
19
33
|
async resolve(targetPubkey, secretKey, identifier = 'addr', options = {}) {
|
|
20
34
|
let hexPubkey = targetPubkey;
|
|
21
35
|
if (targetPubkey.startsWith('npub1')) {
|
|
@@ -23,7 +37,6 @@ export class NCC05Resolver {
|
|
|
23
37
|
hexPubkey = decoded.data;
|
|
24
38
|
}
|
|
25
39
|
let queryRelays = [...this.bootstrapRelays];
|
|
26
|
-
// 1. NIP-65 Gossip Discovery
|
|
27
40
|
if (options.gossip) {
|
|
28
41
|
const relayListEvent = await this.pool.get(this.bootstrapRelays, {
|
|
29
42
|
authors: [hexPubkey],
|
|
@@ -33,9 +46,7 @@ export class NCC05Resolver {
|
|
|
33
46
|
const discoveredRelays = relayListEvent.tags
|
|
34
47
|
.filter(t => t[0] === 'r')
|
|
35
48
|
.map(t => t[1]);
|
|
36
|
-
|
|
37
|
-
queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
|
|
38
|
-
}
|
|
49
|
+
queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
|
|
39
50
|
}
|
|
40
51
|
}
|
|
41
52
|
const filter = {
|
|
@@ -45,77 +56,103 @@ export class NCC05Resolver {
|
|
|
45
56
|
limit: 10
|
|
46
57
|
};
|
|
47
58
|
const queryPromise = this.pool.querySync(queryRelays, filter);
|
|
48
|
-
const timeoutPromise = new Promise((
|
|
59
|
+
const timeoutPromise = new Promise((r) => setTimeout(() => r(null), this.timeout));
|
|
49
60
|
const result = await Promise.race([queryPromise, timeoutPromise]);
|
|
50
61
|
if (!result || (Array.isArray(result) && result.length === 0))
|
|
51
62
|
return null;
|
|
52
|
-
// 2. Filter for valid signatures and sort by created_at
|
|
53
63
|
const validEvents = result
|
|
54
64
|
.filter(e => verifyEvent(e))
|
|
55
65
|
.sort((a, b) => b.created_at - a.created_at);
|
|
56
66
|
if (validEvents.length === 0)
|
|
57
67
|
return null;
|
|
58
68
|
const latestEvent = validEvents[0];
|
|
59
|
-
// 2. Decrypt
|
|
60
69
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|
|
68
85
|
}
|
|
69
|
-
|
|
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;
|
|
70
93
|
const now = Math.floor(Date.now() / 1000);
|
|
71
94
|
if (now > payload.updated_at + payload.ttl) {
|
|
72
|
-
if (options.strict)
|
|
73
|
-
console.warn('Rejecting expired NCC-05 record (strict mode)');
|
|
95
|
+
if (options.strict)
|
|
74
96
|
return null;
|
|
75
|
-
|
|
76
|
-
console.warn('NCC-05 record has expired');
|
|
97
|
+
console.warn('NCC-05 record expired');
|
|
77
98
|
}
|
|
78
99
|
return payload;
|
|
79
100
|
}
|
|
80
101
|
catch (e) {
|
|
81
|
-
console.error('Failed to decrypt or parse NCC-05 record:', e);
|
|
82
102
|
return null;
|
|
83
103
|
}
|
|
84
104
|
}
|
|
85
|
-
close() {
|
|
86
|
-
this.pool.close(this.bootstrapRelays);
|
|
87
|
-
}
|
|
105
|
+
close() { this.pool.close(this.bootstrapRelays); }
|
|
88
106
|
}
|
|
89
107
|
export class NCC05Publisher {
|
|
90
108
|
pool;
|
|
91
109
|
constructor(options = {}) {
|
|
92
110
|
this.pool = new SimplePool();
|
|
93
111
|
if (options.websocketImplementation) {
|
|
94
|
-
// @ts-ignore
|
|
112
|
+
// @ts-ignore
|
|
95
113
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
96
114
|
}
|
|
97
115
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
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 };
|
|
107
127
|
const eventTemplate = {
|
|
108
128
|
kind: 30058,
|
|
109
129
|
created_at: Math.floor(Date.now() / 1000),
|
|
110
130
|
tags: [['d', identifier]],
|
|
111
|
-
content:
|
|
131
|
+
content: JSON.stringify(wrappedContent),
|
|
112
132
|
};
|
|
113
133
|
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
114
|
-
// 3. Publish
|
|
115
134
|
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
116
135
|
return signedEvent;
|
|
117
136
|
}
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
async publish(relays, secretKey, payload, options = {}) {
|
|
138
|
+
const myPubkey = getPublicKey(secretKey);
|
|
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
|
+
}
|
|
146
|
+
const eventTemplate = {
|
|
147
|
+
kind: 30058,
|
|
148
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
149
|
+
pubkey: myPubkey,
|
|
150
|
+
tags: [['d', identifier]],
|
|
151
|
+
content: content,
|
|
152
|
+
};
|
|
153
|
+
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
154
|
+
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
155
|
+
return signedEvent;
|
|
120
156
|
}
|
|
157
|
+
close(relays) { this.pool.close(relays); }
|
|
121
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
|
-
import { NCC05Publisher, NCC05Resolver } from './index.js';
|
|
2
|
-
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
|
1
|
+
import { NCC05Publisher, NCC05Resolver, NCC05Group } from './index.js';
|
|
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.');
|
|
@@ -73,8 +76,76 @@ async function test() {
|
|
|
73
76
|
console.error('FAILED: npub resolution did not find record.');
|
|
74
77
|
process.exit(1);
|
|
75
78
|
}
|
|
79
|
+
// Test Friend-to-Friend resolution
|
|
80
|
+
console.log('Testing Friend-to-Friend resolution...');
|
|
81
|
+
const skA = generateSecretKey();
|
|
82
|
+
const pkA = getPublicKey(skA);
|
|
83
|
+
const skB = generateSecretKey();
|
|
84
|
+
const pkB = getPublicKey(skB);
|
|
85
|
+
const payloadFriend = {
|
|
86
|
+
v: 1, ttl: 60, updated_at: Math.floor(Date.now() / 1000),
|
|
87
|
+
endpoints: [{ type: 'tcp', uri: 'friend:7777', priority: 1, family: 'ipv4' }]
|
|
88
|
+
};
|
|
89
|
+
// User A publishes for User B
|
|
90
|
+
console.log('User A publishing for User B...');
|
|
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
|
+
}
|
|
102
|
+
// Test Group Resolution Utility
|
|
103
|
+
console.log('Testing NCC05Group utility...');
|
|
104
|
+
const groupIdentity = NCC05Group.createGroupIdentity();
|
|
105
|
+
const payloadGroup = {
|
|
106
|
+
v: 1, ttl: 60, updated_at: Math.floor(Date.now() / 1000),
|
|
107
|
+
endpoints: [{ type: 'tcp', uri: 'group-service:8888', priority: 1, family: 'ipv4' }]
|
|
108
|
+
};
|
|
109
|
+
console.log('Publishing as Group...');
|
|
110
|
+
await publisher.publish(relays, groupIdentity.sk, payloadGroup, { identifier: 'group-test' });
|
|
111
|
+
console.log('Resolving as Group Member...');
|
|
112
|
+
const groupResult = await NCC05Group.resolveAsGroup(resolver, groupIdentity.pk, groupIdentity.sk, 'group-test');
|
|
113
|
+
if (groupResult && groupResult.endpoints[0].uri === 'group-service:8888') {
|
|
114
|
+
console.log('NCC05Group resolution successful.');
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.error('FAILED: NCC05Group resolution.');
|
|
118
|
+
process.exit(1);
|
|
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
|
+
}
|
|
76
145
|
publisher.close(relays);
|
|
77
146
|
resolver.close();
|
|
147
|
+
relay.stop();
|
|
148
|
+
console.log('Local Mock Test Suite Passed.');
|
|
78
149
|
process.exit(0);
|
|
79
150
|
}
|
|
80
151
|
test().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ncc-05",
|
|
3
|
-
"version": "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",
|
|
@@ -9,14 +9,22 @@
|
|
|
9
9
|
"build": "tsc",
|
|
10
10
|
"prepublishOnly": "npm run build"
|
|
11
11
|
},
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"nostr",
|
|
14
|
+
"dns",
|
|
15
|
+
"identity",
|
|
16
|
+
"resolution",
|
|
17
|
+
"privacy"
|
|
18
|
+
],
|
|
13
19
|
"author": "lostcause",
|
|
14
20
|
"license": "MIT",
|
|
15
21
|
"dependencies": {
|
|
16
22
|
"nostr-tools": "^2.10.0"
|
|
17
23
|
},
|
|
18
24
|
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.0.0",
|
|
26
|
+
"@types/ws": "^8.18.1",
|
|
19
27
|
"typescript": "^5.0.0",
|
|
20
|
-
"
|
|
28
|
+
"ws": "^8.18.3"
|
|
21
29
|
}
|
|
22
|
-
}
|
|
30
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
finalizeEvent,
|
|
6
6
|
verifyEvent,
|
|
7
7
|
Event,
|
|
8
|
-
getPublicKey
|
|
8
|
+
getPublicKey,
|
|
9
|
+
generateSecretKey
|
|
9
10
|
} from 'nostr-tools';
|
|
10
11
|
|
|
11
12
|
export interface NCC05Endpoint {
|
|
@@ -27,7 +28,42 @@ export interface NCC05Payload {
|
|
|
27
28
|
export interface ResolverOptions {
|
|
28
29
|
bootstrapRelays?: string[];
|
|
29
30
|
timeout?: number;
|
|
30
|
-
websocketImplementation?: any;
|
|
31
|
+
websocketImplementation?: any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Advanced multi-recipient "wrapping" structure.
|
|
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
|
+
|
|
44
|
+
export class NCC05Group {
|
|
45
|
+
static createGroupIdentity() {
|
|
46
|
+
const sk = generateSecretKey();
|
|
47
|
+
const pk = getPublicKey(sk);
|
|
48
|
+
return {
|
|
49
|
+
nsec: nip19.nsecEncode(sk),
|
|
50
|
+
sk: sk,
|
|
51
|
+
pk: pk,
|
|
52
|
+
npub: nip19.npubEncode(pk)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a record that was published using a group's shared identity.
|
|
58
|
+
*/
|
|
59
|
+
static async resolveAsGroup(
|
|
60
|
+
resolver: NCC05Resolver,
|
|
61
|
+
groupPubkey: string,
|
|
62
|
+
groupSecretKey: Uint8Array,
|
|
63
|
+
identifier: string = 'addr'
|
|
64
|
+
): Promise<NCC05Payload | null> {
|
|
65
|
+
return resolver.resolve(groupPubkey, groupSecretKey, identifier);
|
|
66
|
+
}
|
|
31
67
|
}
|
|
32
68
|
|
|
33
69
|
export class NCC05Resolver {
|
|
@@ -38,20 +74,16 @@ export class NCC05Resolver {
|
|
|
38
74
|
constructor(options: ResolverOptions = {}) {
|
|
39
75
|
this.pool = new SimplePool();
|
|
40
76
|
if (options.websocketImplementation) {
|
|
41
|
-
// @ts-ignore
|
|
77
|
+
// @ts-ignore
|
|
42
78
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
43
79
|
}
|
|
44
80
|
this.bootstrapRelays = options.bootstrapRelays || ['wss://relay.damus.io', 'wss://nos.lol'];
|
|
45
81
|
this.timeout = options.timeout || 10000;
|
|
46
82
|
}
|
|
47
83
|
|
|
48
|
-
/**
|
|
49
|
-
* Resolve a locator record for a given pubkey.
|
|
50
|
-
* Supports both hex and npub strings.
|
|
51
|
-
*/
|
|
52
84
|
async resolve(
|
|
53
85
|
targetPubkey: string,
|
|
54
|
-
secretKey
|
|
86
|
+
secretKey?: Uint8Array,
|
|
55
87
|
identifier: string = 'addr',
|
|
56
88
|
options: { strict?: boolean, gossip?: boolean } = {}
|
|
57
89
|
): Promise<NCC05Payload | null> {
|
|
@@ -63,20 +95,16 @@ export class NCC05Resolver {
|
|
|
63
95
|
|
|
64
96
|
let queryRelays = [...this.bootstrapRelays];
|
|
65
97
|
|
|
66
|
-
// 1. NIP-65 Gossip Discovery
|
|
67
98
|
if (options.gossip) {
|
|
68
99
|
const relayListEvent = await this.pool.get(this.bootstrapRelays, {
|
|
69
100
|
authors: [hexPubkey],
|
|
70
101
|
kinds: [10002]
|
|
71
102
|
});
|
|
72
|
-
|
|
73
103
|
if (relayListEvent) {
|
|
74
104
|
const discoveredRelays = relayListEvent.tags
|
|
75
105
|
.filter(t => t[0] === 'r')
|
|
76
106
|
.map(t => t[1]);
|
|
77
|
-
|
|
78
|
-
queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
|
|
79
|
-
}
|
|
107
|
+
queryRelays = [...new Set([...queryRelays, ...discoveredRelays])];
|
|
80
108
|
}
|
|
81
109
|
}
|
|
82
110
|
|
|
@@ -88,15 +116,11 @@ export class NCC05Resolver {
|
|
|
88
116
|
};
|
|
89
117
|
|
|
90
118
|
const queryPromise = this.pool.querySync(queryRelays, filter);
|
|
91
|
-
const timeoutPromise = new Promise<null>((
|
|
92
|
-
setTimeout(() => resolve(null), this.timeout)
|
|
93
|
-
);
|
|
94
|
-
|
|
119
|
+
const timeoutPromise = new Promise<null>((r) => setTimeout(() => r(null), this.timeout));
|
|
95
120
|
const result = await Promise.race([queryPromise, timeoutPromise]);
|
|
96
121
|
|
|
97
122
|
if (!result || (Array.isArray(result) && result.length === 0)) return null;
|
|
98
123
|
|
|
99
|
-
// 2. Filter for valid signatures and sort by created_at
|
|
100
124
|
const validEvents = (result as Event[])
|
|
101
125
|
.filter(e => verifyEvent(e))
|
|
102
126
|
.sort((a, b) => b.created_at - a.created_at);
|
|
@@ -104,38 +128,47 @@ export class NCC05Resolver {
|
|
|
104
128
|
if (validEvents.length === 0) return null;
|
|
105
129
|
const latestEvent = validEvents[0];
|
|
106
130
|
|
|
107
|
-
// 2. Decrypt
|
|
108
131
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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);
|
|
117
154
|
}
|
|
118
155
|
|
|
119
|
-
|
|
156
|
+
const payload = JSON.parse(content) as NCC05Payload;
|
|
157
|
+
if (!payload.endpoints) return null;
|
|
158
|
+
|
|
120
159
|
const now = Math.floor(Date.now() / 1000);
|
|
121
160
|
if (now > payload.updated_at + payload.ttl) {
|
|
122
|
-
if (options.strict)
|
|
123
|
-
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
console.warn('NCC-05 record has expired');
|
|
161
|
+
if (options.strict) return null;
|
|
162
|
+
console.warn('NCC-05 record expired');
|
|
127
163
|
}
|
|
128
164
|
|
|
129
165
|
return payload;
|
|
130
166
|
} catch (e) {
|
|
131
|
-
console.error('Failed to decrypt or parse NCC-05 record:', e);
|
|
132
167
|
return null;
|
|
133
168
|
}
|
|
134
169
|
}
|
|
135
170
|
|
|
136
|
-
close() {
|
|
137
|
-
this.pool.close(this.bootstrapRelays);
|
|
138
|
-
}
|
|
171
|
+
close() { this.pool.close(this.bootstrapRelays); }
|
|
139
172
|
}
|
|
140
173
|
|
|
141
174
|
export class NCC05Publisher {
|
|
@@ -144,43 +177,72 @@ export class NCC05Publisher {
|
|
|
144
177
|
constructor(options: { websocketImplementation?: any } = {}) {
|
|
145
178
|
this.pool = new SimplePool();
|
|
146
179
|
if (options.websocketImplementation) {
|
|
147
|
-
// @ts-ignore
|
|
180
|
+
// @ts-ignore
|
|
148
181
|
this.pool.websocketImplementation = options.websocketImplementation;
|
|
149
182
|
}
|
|
150
183
|
}
|
|
151
184
|
|
|
152
|
-
|
|
153
|
-
* Create and publish a locator record.
|
|
154
|
-
*/
|
|
155
|
-
async publish(
|
|
185
|
+
async publishWrapped(
|
|
156
186
|
relays: string[],
|
|
157
187
|
secretKey: Uint8Array,
|
|
188
|
+
recipients: string[],
|
|
158
189
|
payload: NCC05Payload,
|
|
159
190
|
identifier: string = 'addr'
|
|
160
191
|
): Promise<Event> {
|
|
161
|
-
const
|
|
192
|
+
const sessionKey = generateSecretKey();
|
|
193
|
+
const sessionKeyHex = Array.from(sessionKey).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
162
194
|
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
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 };
|
|
166
205
|
|
|
167
|
-
// 2. Create and Finalize Event
|
|
168
206
|
const eventTemplate = {
|
|
169
207
|
kind: 30058,
|
|
170
208
|
created_at: Math.floor(Date.now() / 1000),
|
|
171
209
|
tags: [['d', identifier]],
|
|
172
|
-
content:
|
|
210
|
+
content: JSON.stringify(wrappedContent),
|
|
173
211
|
};
|
|
174
212
|
|
|
175
213
|
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
176
|
-
|
|
177
|
-
// 3. Publish
|
|
178
214
|
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
179
|
-
|
|
180
215
|
return signedEvent;
|
|
181
216
|
}
|
|
182
217
|
|
|
183
|
-
|
|
184
|
-
|
|
218
|
+
async publish(
|
|
219
|
+
relays: string[],
|
|
220
|
+
secretKey: Uint8Array,
|
|
221
|
+
payload: NCC05Payload,
|
|
222
|
+
options: { identifier?: string, recipientPubkey?: string, public?: boolean } = {}
|
|
223
|
+
): Promise<Event> {
|
|
224
|
+
const myPubkey = getPublicKey(secretKey);
|
|
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
|
+
}
|
|
233
|
+
|
|
234
|
+
const eventTemplate = {
|
|
235
|
+
kind: 30058,
|
|
236
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
237
|
+
pubkey: myPubkey,
|
|
238
|
+
tags: [['d', identifier]],
|
|
239
|
+
content: content,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
|
243
|
+
await Promise.all(this.pool.publish(relays, signedEvent));
|
|
244
|
+
return signedEvent;
|
|
185
245
|
}
|
|
186
|
-
|
|
246
|
+
|
|
247
|
+
close(relays: string[]) { this.pool.close(relays); }
|
|
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
|
-
import { NCC05Publisher, NCC05Resolver, NCC05Payload } from './index.js';
|
|
2
|
-
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
|
1
|
+
import { NCC05Publisher, NCC05Resolver, NCC05Payload, NCC05Group } from './index.js';
|
|
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.');
|
|
@@ -81,8 +85,86 @@ async function test() {
|
|
|
81
85
|
process.exit(1);
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
// Test Friend-to-Friend resolution
|
|
89
|
+
console.log('Testing Friend-to-Friend resolution...');
|
|
90
|
+
const skA = generateSecretKey();
|
|
91
|
+
const pkA = getPublicKey(skA);
|
|
92
|
+
const skB = generateSecretKey();
|
|
93
|
+
const pkB = getPublicKey(skB);
|
|
94
|
+
|
|
95
|
+
const payloadFriend: NCC05Payload = {
|
|
96
|
+
v: 1, ttl: 60, updated_at: Math.floor(Date.now() / 1000),
|
|
97
|
+
endpoints: [{ type: 'tcp', uri: 'friend:7777', priority: 1, family: 'ipv4' }]
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// User A publishes for User B
|
|
101
|
+
console.log('User A publishing for User B...');
|
|
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
|
+
}
|
|
113
|
+
|
|
114
|
+
// Test Group Resolution Utility
|
|
115
|
+
console.log('Testing NCC05Group utility...');
|
|
116
|
+
const groupIdentity = NCC05Group.createGroupIdentity();
|
|
117
|
+
const payloadGroup: NCC05Payload = {
|
|
118
|
+
v: 1, ttl: 60, updated_at: Math.floor(Date.now() / 1000),
|
|
119
|
+
endpoints: [{ type: 'tcp', uri: 'group-service:8888', priority: 1, family: 'ipv4' }]
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
console.log('Publishing as Group...');
|
|
123
|
+
await publisher.publish(relays, groupIdentity.sk, payloadGroup, { identifier: 'group-test' });
|
|
124
|
+
|
|
125
|
+
console.log('Resolving as Group Member...');
|
|
126
|
+
const groupResult = await NCC05Group.resolveAsGroup(resolver, groupIdentity.pk, groupIdentity.sk, 'group-test');
|
|
127
|
+
if (groupResult && groupResult.endpoints[0].uri === 'group-service:8888') {
|
|
128
|
+
console.log('NCC05Group resolution successful.');
|
|
129
|
+
} else {
|
|
130
|
+
console.error('FAILED: NCC05Group resolution.');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
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
|
+
|
|
84
164
|
publisher.close(relays);
|
|
85
165
|
resolver.close();
|
|
166
|
+
relay.stop();
|
|
167
|
+
console.log('Local Mock Test Suite Passed.');
|
|
86
168
|
process.exit(0);
|
|
87
169
|
}
|
|
88
170
|
|