@konemono/nostr-login 1.7.69 → 1.8.0
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.esm.js +10 -12
- package/dist/index.esm.js.map +1 -1
- package/dist/modules/AmberDirectSigner.d.ts +1 -0
- package/dist/modules/AuthNostrService.d.ts +5 -5
- package/dist/modules/Nip46.d.ts +47 -31
- package/dist/modules/NostrExtensionService.d.ts +5 -0
- package/dist/modules/Signer.d.ts +11 -4
- package/dist/modules/nip46/Nip46Adapter.d.ts +26 -0
- package/dist/modules/nip46/Nip46Client.d.ts +52 -0
- package/dist/modules/nip46/types.d.ts +24 -0
- package/dist/unpkg.js +10 -12
- package/dist/utils/index.d.ts +2 -3
- package/package.json +7 -4
- package/src/index.ts +1 -1
- package/src/modules/AmberDirectSigner.ts +204 -147
- package/src/modules/AuthNostrService.ts +151 -122
- package/src/modules/Nip46.iframe.test.ts +124 -0
- package/src/modules/Nip46.test.ts +31 -0
- package/src/modules/Nip46.ts +212 -261
- package/src/modules/NostrExtensionService.ts +6 -0
- package/src/modules/Signer.ts +39 -8
- package/src/modules/nip46/Nip46Adapter.ts +123 -0
- package/src/modules/nip46/Nip46Client.ts +248 -0
- package/src/modules/nip46/types.ts +26 -0
- package/src/utils/index.ts +67 -21
- package/tsconfig.json +2 -2
- package/vitest.config.ts +9 -0
package/src/modules/Signer.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
|
|
2
1
|
import { Nip44 } from '../utils/nip44';
|
|
3
|
-
import { getPublicKey } from 'nostr-tools';
|
|
2
|
+
import { getPublicKey, getEventHash, getSignature, nip04 } from 'nostr-tools';
|
|
4
3
|
|
|
5
|
-
export class PrivateKeySigner
|
|
4
|
+
export class PrivateKeySigner {
|
|
6
5
|
private nip44: Nip44 = new Nip44();
|
|
7
6
|
private _pubkey: string;
|
|
7
|
+
public privateKey: string;
|
|
8
8
|
|
|
9
9
|
constructor(privateKey: string) {
|
|
10
|
-
|
|
10
|
+
this.privateKey = privateKey;
|
|
11
11
|
this._pubkey = getPublicKey(privateKey);
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -15,11 +15,42 @@ export class PrivateKeySigner extends NDKPrivateKeySigner {
|
|
|
15
15
|
return this._pubkey;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
return Promise.resolve(
|
|
18
|
+
async blockUntilReady() {
|
|
19
|
+
return Promise.resolve();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
return
|
|
22
|
+
async user() {
|
|
23
|
+
return { pubkey: this.pubkey };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async sign(event: any): Promise<string> {
|
|
27
|
+
// ensure event has created_at
|
|
28
|
+
if (!event.created_at) event.created_at = Math.floor(Date.now() / 1000);
|
|
29
|
+
// compute id and signature
|
|
30
|
+
const id = getEventHash(event as any);
|
|
31
|
+
event.id = id;
|
|
32
|
+
const sig = getSignature(event as any, this.privateKey);
|
|
33
|
+
event.sig = sig;
|
|
34
|
+
return sig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async encrypt(recipient: any, plaintext: string): Promise<string> {
|
|
38
|
+
const pubkey = typeof recipient === 'string' ? recipient : recipient.pubkey;
|
|
39
|
+
return nip04.encrypt(this.privateKey, pubkey, plaintext);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async decrypt(sender: any, ciphertext: string): Promise<string> {
|
|
43
|
+
const pubkey = typeof sender === 'string' ? sender : sender.pubkey;
|
|
44
|
+
return nip04.decrypt(this.privateKey, pubkey, ciphertext);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
encryptNip44(recipient: any, value: string): Promise<string> {
|
|
48
|
+
const pubkey = typeof recipient === 'string' ? recipient : recipient.pubkey;
|
|
49
|
+
return Promise.resolve(this.nip44.encrypt(this.privateKey, pubkey, value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
decryptNip44(sender: any, value: string): Promise<string> {
|
|
53
|
+
const pubkey = typeof sender === 'string' ? sender : sender.pubkey;
|
|
54
|
+
return Promise.resolve(this.nip44.decrypt(this.privateKey, pubkey, value));
|
|
24
55
|
}
|
|
25
56
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { EventEmitter } from 'tseep';
|
|
2
|
+
import { Nip46Client } from './Nip46Client';
|
|
3
|
+
import { PrivateKeySigner } from '../Signer';
|
|
4
|
+
|
|
5
|
+
export class Nip46Adapter extends EventEmitter {
|
|
6
|
+
private client: Nip46Client;
|
|
7
|
+
private localSigner: PrivateKeySigner;
|
|
8
|
+
public userPubkey: string = '';
|
|
9
|
+
public remotePubkey: string;
|
|
10
|
+
|
|
11
|
+
constructor(client: Nip46Client, localSigner: PrivateKeySigner) {
|
|
12
|
+
super();
|
|
13
|
+
this.client = client;
|
|
14
|
+
this.localSigner = localSigner;
|
|
15
|
+
this.remotePubkey = (client as any).remotePubkey || '';
|
|
16
|
+
|
|
17
|
+
// forward events
|
|
18
|
+
this.client.on('authUrl', (url: string) => {
|
|
19
|
+
this.emit('authUrl', url);
|
|
20
|
+
});
|
|
21
|
+
this.client.on('response', ({ response, pubkey }: any) => {
|
|
22
|
+
this.emit('response', response, pubkey);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async initUserPubkey(hintPubkey?: string) {
|
|
27
|
+
if (this.userPubkey) throw new Error('Already called initUserPubkey');
|
|
28
|
+
if (hintPubkey) {
|
|
29
|
+
this.userPubkey = hintPubkey;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const res = await this.client.sendRequest('get_public_key', []);
|
|
34
|
+
if (!res) throw new Error('No public key returned');
|
|
35
|
+
this.userPubkey = res;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async listen(nostrConnectSecret: string): Promise<string> {
|
|
39
|
+
return new Promise<string>((ok, err) => {
|
|
40
|
+
const onResponse = ({ response, pubkey }: any) => {
|
|
41
|
+
if (!response) return;
|
|
42
|
+
if (response.result === 'auth_url') return;
|
|
43
|
+
if (response.result === 'ack' || response.result === nostrConnectSecret) {
|
|
44
|
+
this.client.off('response', onResponse);
|
|
45
|
+
ok(pubkey);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
this.client.on('response', onResponse);
|
|
50
|
+
|
|
51
|
+
// also add a timeout
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
this.client.off('response', onResponse);
|
|
54
|
+
err(new Error('Listen timeout'));
|
|
55
|
+
}, 30000);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async connect(token?: string, perms?: string) {
|
|
60
|
+
const result = await this.client.sendRequest('connect', [this.localSigner.pubkey, token || '', perms || '']);
|
|
61
|
+
if (result !== 'ack') throw new Error(result || 'connect failed');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async setListenReply(reply: any, nostrConnectSecret: string) {
|
|
65
|
+
// reply is expected to be a raw event object; we'll try to parse its content
|
|
66
|
+
// Attempt to decrypt via the client flow by treating it as a response
|
|
67
|
+
try {
|
|
68
|
+
const decoded = reply && reply.content ? JSON.parse(reply.content) : null;
|
|
69
|
+
if (!decoded) throw new Error('Bad reply');
|
|
70
|
+
if (decoded.result === nostrConnectSecret) {
|
|
71
|
+
this.userPubkey = reply.pubkey;
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error('Bad reply');
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
throw new Error('Failed to set listen reply');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async createAccount2({ bunkerPubkey, name, domain, perms = '' }: { bunkerPubkey: string; name: string; domain: string; perms?: string }) {
|
|
81
|
+
const params = [name, domain, '', perms];
|
|
82
|
+
|
|
83
|
+
const r = await this.client.sendRequest('create_account', params);
|
|
84
|
+
if (!r) throw new Error('create_account failed');
|
|
85
|
+
if (r === 'error') throw new Error('create_account error');
|
|
86
|
+
return r;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async encrypt(recipientPubkey: string, plaintext: string) {
|
|
90
|
+
const r = await this.client.sendRequest('nip04_encrypt', [recipientPubkey, plaintext]);
|
|
91
|
+
return r;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async decrypt(recipientPubkey: string, ciphertext: string) {
|
|
95
|
+
const r = await this.client.sendRequest('nip04_decrypt', [recipientPubkey, ciphertext]);
|
|
96
|
+
return r;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async sign(event: any) {
|
|
100
|
+
const r = await this.client.sendRequest('sign_event', [JSON.stringify(event)]);
|
|
101
|
+
try {
|
|
102
|
+
const parsed = typeof r === 'string' ? JSON.parse(r) : r;
|
|
103
|
+
if (parsed && parsed.sig) return parsed.sig;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// not JSON
|
|
106
|
+
}
|
|
107
|
+
return r;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// provide rpc compatibility
|
|
111
|
+
get rpc() {
|
|
112
|
+
return {
|
|
113
|
+
sendRequest: async (remotePubkey: string, method: string, params: string[], kind: number, cb: (res: any) => void) => {
|
|
114
|
+
try {
|
|
115
|
+
const res = await this.client.sendRequest(method, params);
|
|
116
|
+
cb({ result: res });
|
|
117
|
+
} catch (err: any) {
|
|
118
|
+
cb({ error: err.message });
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { SimplePool, Event as NostrEvent, getPublicKey, nip04 } from 'nostr-tools';
|
|
2
|
+
import { Nip44 } from '../../utils/nip44';
|
|
3
|
+
import { EventEmitter } from 'tseep';
|
|
4
|
+
import { Nip46Request, Nip46Response, PendingRequest, Nip46ClientOptions } from './types';
|
|
5
|
+
import { getEventHash, getSignature } from 'nostr-tools';
|
|
6
|
+
|
|
7
|
+
export class Nip46Client extends EventEmitter {
|
|
8
|
+
private pool: SimplePool;
|
|
9
|
+
private localPrivateKey: string;
|
|
10
|
+
private remotePubkey: string;
|
|
11
|
+
private relays: string[];
|
|
12
|
+
private pendingRequests: Map<string, PendingRequest> = new Map();
|
|
13
|
+
private defaultTimeoutMs: number;
|
|
14
|
+
private useNip44: boolean;
|
|
15
|
+
private subscription: any = null;
|
|
16
|
+
private isSubscribed: boolean = false;
|
|
17
|
+
private nip44Codec: Nip44 = new Nip44();
|
|
18
|
+
|
|
19
|
+
constructor(options: Nip46ClientOptions) {
|
|
20
|
+
super();
|
|
21
|
+
this.pool = new SimplePool();
|
|
22
|
+
this.localPrivateKey = options.localPrivateKey;
|
|
23
|
+
this.remotePubkey = options.remotePubkey;
|
|
24
|
+
this.relays = options.relays;
|
|
25
|
+
this.defaultTimeoutMs = options.timeoutMs || 30000;
|
|
26
|
+
this.useNip44 = options.useNip44 || false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get localPubkey(): string {
|
|
30
|
+
return getPublicKey(this.localPrivateKey);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* NIP-46リクエストを送信
|
|
35
|
+
*/
|
|
36
|
+
async sendRequest(method: string, params: string[] = [], timeoutMs?: number): Promise<string> {
|
|
37
|
+
const timeout = timeoutMs || this.defaultTimeoutMs;
|
|
38
|
+
const id = this.generateId();
|
|
39
|
+
const request: Nip46Request = { id, method, params };
|
|
40
|
+
|
|
41
|
+
console.log('[Nip46Client] Sending request:', { id, method, params });
|
|
42
|
+
|
|
43
|
+
// レスポンス購読を開始(まだの場合)
|
|
44
|
+
if (!this.isSubscribed) {
|
|
45
|
+
this.subscribeToResponses();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// リクエストイベントを作成・送信
|
|
49
|
+
await this.publishRequest(request);
|
|
50
|
+
|
|
51
|
+
// レスポンスを待つPromise
|
|
52
|
+
return new Promise<string>((resolve, reject) => {
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
this.pendingRequests.delete(id);
|
|
55
|
+
const error = new Error(`Request ${id} (${method}) timed out after ${timeout}ms`);
|
|
56
|
+
console.error('[Nip46Client]', error.message);
|
|
57
|
+
reject(error);
|
|
58
|
+
}, timeout);
|
|
59
|
+
|
|
60
|
+
this.pendingRequests.set(id, {
|
|
61
|
+
resolve,
|
|
62
|
+
reject,
|
|
63
|
+
timer,
|
|
64
|
+
method,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* リクエストイベントを作成して送信
|
|
71
|
+
*/
|
|
72
|
+
private async publishRequest(request: Nip46Request): Promise<void> {
|
|
73
|
+
const content = JSON.stringify(request);
|
|
74
|
+
|
|
75
|
+
// 暗号化
|
|
76
|
+
const encrypted = this.useNip44
|
|
77
|
+
? await this.nip44Codec.encrypt(this.localPrivateKey, this.remotePubkey, content)
|
|
78
|
+
: await nip04.encrypt(this.localPrivateKey, this.remotePubkey, content);
|
|
79
|
+
|
|
80
|
+
// イベント作成
|
|
81
|
+
const event: NostrEvent = {
|
|
82
|
+
kind: 24133,
|
|
83
|
+
pubkey: this.localPubkey,
|
|
84
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
85
|
+
tags: [['p', this.remotePubkey]],
|
|
86
|
+
content: encrypted,
|
|
87
|
+
id: '',
|
|
88
|
+
sig: '',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ID計算
|
|
92
|
+
event.id = getEventHash(event);
|
|
93
|
+
|
|
94
|
+
// 署名
|
|
95
|
+
event.sig = getSignature(event, this.localPrivateKey);
|
|
96
|
+
|
|
97
|
+
// リレーに送信
|
|
98
|
+
await Promise.any(this.pool.publish(this.relays, event));
|
|
99
|
+
console.log('[Nip46Client] Request published:', request.id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* レスポンスイベントを購読
|
|
104
|
+
*/
|
|
105
|
+
private subscribeToResponses(): void {
|
|
106
|
+
if (this.isSubscribed) return;
|
|
107
|
+
|
|
108
|
+
const filter = {
|
|
109
|
+
'kinds': [24133],
|
|
110
|
+
'#p': [this.localPubkey],
|
|
111
|
+
'since': Math.floor(Date.now() / 1000) - 60,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
console.log('[Nip46Client] Subscribing to responses');
|
|
115
|
+
|
|
116
|
+
// SimplePool subscription
|
|
117
|
+
this.subscription = this.pool.sub(this.relays, [filter]);
|
|
118
|
+
this.subscription.on('event', async (event: NostrEvent) => {
|
|
119
|
+
await this.handleResponseEvent(event);
|
|
120
|
+
});
|
|
121
|
+
this.subscription.on('eose', () => {
|
|
122
|
+
console.log('[Nip46Client] EOSE received');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this.isSubscribed = true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* レスポンスイベントを処理
|
|
130
|
+
*/
|
|
131
|
+
private async handleResponseEvent(event: NostrEvent): Promise<void> {
|
|
132
|
+
try {
|
|
133
|
+
// 復号化
|
|
134
|
+
const decrypted = this.isNip04(event.content)
|
|
135
|
+
? await nip04.decrypt(this.localPrivateKey, event.pubkey, event.content)
|
|
136
|
+
: await this.nip44Codec.decrypt(this.localPrivateKey, event.pubkey, event.content);
|
|
137
|
+
|
|
138
|
+
const response: Nip46Response = JSON.parse(decrypted);
|
|
139
|
+
|
|
140
|
+
console.log('[Nip46Client] Response received:', {
|
|
141
|
+
id: response.id,
|
|
142
|
+
hasResult: !!response.result,
|
|
143
|
+
hasError: !!response.error,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Emit response event for consumers (include sender pubkey)
|
|
147
|
+
this.emit('response', { response, pubkey: event.pubkey });
|
|
148
|
+
|
|
149
|
+
// auth_urlの特別処理
|
|
150
|
+
if (response.result === 'auth_url') {
|
|
151
|
+
console.log('[Nip46Client] Auth URL received:', response.error);
|
|
152
|
+
this.emit('authUrl', response.error);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 保留中のリクエストを解決
|
|
157
|
+
const pending = this.pendingRequests.get(response.id);
|
|
158
|
+
if (pending) {
|
|
159
|
+
clearTimeout(pending.timer);
|
|
160
|
+
this.pendingRequests.delete(response.id);
|
|
161
|
+
|
|
162
|
+
if (response.error) {
|
|
163
|
+
console.error('[Nip46Client] Request failed:', {
|
|
164
|
+
id: response.id,
|
|
165
|
+
method: pending.method,
|
|
166
|
+
error: response.error,
|
|
167
|
+
});
|
|
168
|
+
pending.reject(new Error(response.error));
|
|
169
|
+
} else if (response.result !== undefined) {
|
|
170
|
+
console.log('[Nip46Client] Request succeeded:', {
|
|
171
|
+
id: response.id,
|
|
172
|
+
method: pending.method,
|
|
173
|
+
});
|
|
174
|
+
pending.resolve(response.result);
|
|
175
|
+
} else {
|
|
176
|
+
pending.reject(new Error('Invalid response: no result or error'));
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
console.warn('[Nip46Client] Received response for unknown request:', response.id);
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('[Nip46Client] Failed to parse response event:', error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* NIP-04かNIP-44かを判定
|
|
188
|
+
*/
|
|
189
|
+
private isNip04(ciphertext: string): boolean {
|
|
190
|
+
const l = ciphertext.length;
|
|
191
|
+
if (l < 28) return false;
|
|
192
|
+
return ciphertext[l - 28] === '?' && ciphertext[l - 27] === 'i' && ciphertext[l - 26] === 'v' && ciphertext[l - 25] === '=';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* ランダムIDを生成
|
|
197
|
+
*/
|
|
198
|
+
private generateId(): string {
|
|
199
|
+
return Math.random().toString(36).substring(2, 15);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* NIP-44を使用するかどうかを設定
|
|
204
|
+
*/
|
|
205
|
+
setUseNip44(useNip44: boolean): void {
|
|
206
|
+
this.useNip44 = useNip44;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* クリーンアップ
|
|
211
|
+
*/
|
|
212
|
+
cleanup(): void {
|
|
213
|
+
console.log('[Nip46Client] Cleaning up');
|
|
214
|
+
|
|
215
|
+
// すべての保留中リクエストをキャンセル
|
|
216
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
217
|
+
clearTimeout(pending.timer);
|
|
218
|
+
pending.reject(new Error('Client cleanup'));
|
|
219
|
+
}
|
|
220
|
+
this.pendingRequests.clear();
|
|
221
|
+
|
|
222
|
+
// 購読を停止
|
|
223
|
+
if (this.subscription) {
|
|
224
|
+
// SimplePool subscriptions may use `unsub` or `close`
|
|
225
|
+
try {
|
|
226
|
+
if (typeof this.subscription.unsub === 'function') this.subscription.unsub();
|
|
227
|
+
if (typeof this.subscription.close === 'function') this.subscription.close();
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// ignore
|
|
230
|
+
}
|
|
231
|
+
this.subscription = null;
|
|
232
|
+
this.isSubscribed = false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// リレー接続を閉じる
|
|
236
|
+
this.pool.close(this.relays);
|
|
237
|
+
|
|
238
|
+
// イベントリスナーをクリア
|
|
239
|
+
this.removeAllListeners();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 接続状態を確認
|
|
244
|
+
*/
|
|
245
|
+
isConnected(): boolean {
|
|
246
|
+
return this.isSubscribed;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface Nip46Request {
|
|
2
|
+
id: string;
|
|
3
|
+
method: string;
|
|
4
|
+
params: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Nip46Response {
|
|
8
|
+
id: string;
|
|
9
|
+
result?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PendingRequest {
|
|
14
|
+
resolve: (result: string) => void;
|
|
15
|
+
reject: (error: Error) => void;
|
|
16
|
+
timer: NodeJS.Timeout;
|
|
17
|
+
method: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Nip46ClientOptions {
|
|
21
|
+
localPrivateKey: string;
|
|
22
|
+
remotePubkey: string;
|
|
23
|
+
relays: string[];
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
useNip44?: boolean;
|
|
26
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Info, RecentType } from 'nostr-login-components/dist/types/types';
|
|
2
|
-
import
|
|
3
|
-
import { generatePrivateKey } from 'nostr-tools';
|
|
2
|
+
import { generatePrivateKey, SimplePool, getEventHash } from 'nostr-tools';
|
|
4
3
|
import { NostrLoginOptions } from '../types';
|
|
5
4
|
|
|
6
5
|
const LOCAL_STORE_KEY = '__nostrlogin_nip46';
|
|
@@ -29,12 +28,53 @@ export const localStorageRemoveItem = (key: string) => {
|
|
|
29
28
|
localStorage.removeItem(key);
|
|
30
29
|
};
|
|
31
30
|
|
|
32
|
-
export const fetchProfile = async (info: Info,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
export const fetchProfile = async (info: Info, profileNdkOrRelays?: any) => {
|
|
32
|
+
// support legacy external profile fetcher (NDK compatibility)
|
|
33
|
+
if (profileNdkOrRelays && typeof profileNdkOrRelays.fetchEvents === 'function') {
|
|
34
|
+
try {
|
|
35
|
+
const user = (profileNdkOrRelays as any).getUser({ pubkey: info.pubkey });
|
|
36
|
+
user.ndk = profileNdkOrRelays;
|
|
37
|
+
return await user.fetchProfile();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// fallthrough to SimplePool-based fetch
|
|
40
|
+
}
|
|
41
|
+
}
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
// fallback: use SimplePool to fetch latest kind:0 metadata for the pubkey
|
|
44
|
+
const relays = Array.isArray(profileNdkOrRelays) && profileNdkOrRelays.length ? profileNdkOrRelays : DEFAULT_SIGNUP_RELAYS;
|
|
45
|
+
const pool = new SimplePool();
|
|
46
|
+
const sub = pool.sub(relays, [{ kinds: [0], authors: [info.pubkey], limit: 1 }]);
|
|
47
|
+
|
|
48
|
+
return await new Promise(resolve => {
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
try {
|
|
51
|
+
sub.unsub();
|
|
52
|
+
} catch {}
|
|
53
|
+
resolve(null);
|
|
54
|
+
}, 3000);
|
|
55
|
+
|
|
56
|
+
sub.on('event', (event: any) => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
try {
|
|
59
|
+
sub.unsub();
|
|
60
|
+
} catch {}
|
|
61
|
+
if (!event || !event.content) return resolve(null);
|
|
62
|
+
try {
|
|
63
|
+
const profile = JSON.parse(event.content);
|
|
64
|
+
resolve(profile);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
resolve(null);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
sub.on('eose', () => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
try {
|
|
73
|
+
sub.unsub();
|
|
74
|
+
} catch {}
|
|
75
|
+
resolve(null);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
38
78
|
};
|
|
39
79
|
|
|
40
80
|
export const prepareSignupRelays = (signupRelays?: string) => {
|
|
@@ -46,43 +86,46 @@ export const prepareSignupRelays = (signupRelays?: string) => {
|
|
|
46
86
|
return relays;
|
|
47
87
|
};
|
|
48
88
|
|
|
49
|
-
export const createProfile = async (info: Info,
|
|
89
|
+
export const createProfile = async (info: Info, signer: any, signupRelays?: string, outboxRelays?: string[]) => {
|
|
50
90
|
const meta = {
|
|
51
91
|
name: info.name,
|
|
52
92
|
};
|
|
53
93
|
|
|
54
|
-
const profileEvent =
|
|
94
|
+
const profileEvent: any = {
|
|
55
95
|
kind: 0,
|
|
56
96
|
created_at: Math.floor(Date.now() / 1000),
|
|
57
97
|
pubkey: info.pubkey,
|
|
58
98
|
content: JSON.stringify(meta),
|
|
59
99
|
tags: [],
|
|
60
|
-
}
|
|
100
|
+
};
|
|
61
101
|
if (window.location.hostname) profileEvent.tags.push(['client', window.location.hostname]);
|
|
62
102
|
|
|
63
|
-
const relaysEvent =
|
|
103
|
+
const relaysEvent: any = {
|
|
64
104
|
kind: 10002,
|
|
65
105
|
created_at: Math.floor(Date.now() / 1000),
|
|
66
106
|
pubkey: info.pubkey,
|
|
67
107
|
content: '',
|
|
68
108
|
tags: [],
|
|
69
|
-
}
|
|
109
|
+
};
|
|
70
110
|
|
|
71
|
-
const relays = prepareSignupRelays(signupRelays)
|
|
111
|
+
const relays = prepareSignupRelays(signupRelays);
|
|
72
112
|
for (const r of relays) {
|
|
73
113
|
relaysEvent.tags.push(['r', r]);
|
|
74
114
|
}
|
|
75
115
|
|
|
76
|
-
|
|
116
|
+
// signer is expected to implement sign(event)
|
|
117
|
+
await signer.sign(profileEvent);
|
|
77
118
|
console.log('signed profile', profileEvent);
|
|
78
|
-
await
|
|
119
|
+
await signer.sign(relaysEvent);
|
|
79
120
|
console.log('signed relays', relaysEvent);
|
|
80
121
|
|
|
81
122
|
const outboxRelaysFinal = outboxRelays && outboxRelays.length ? outboxRelays : OUTBOX_RELAYS;
|
|
82
123
|
|
|
83
|
-
|
|
124
|
+
// publish using SimplePool
|
|
125
|
+
const pool = new SimplePool();
|
|
126
|
+
await Promise.any(pool.publish(outboxRelaysFinal, profileEvent));
|
|
84
127
|
console.log('published profile', profileEvent);
|
|
85
|
-
await
|
|
128
|
+
await Promise.any(pool.publish(outboxRelaysFinal, relaysEvent));
|
|
86
129
|
console.log('published relays', relaysEvent);
|
|
87
130
|
};
|
|
88
131
|
|
|
@@ -113,7 +156,7 @@ export const getBunkerUrl = async (value: string, optionsModal: NostrLoginOption
|
|
|
113
156
|
const origin = optionsModal.devOverrideBunkerOrigin || `https://${domain}`;
|
|
114
157
|
|
|
115
158
|
const bunkerUrl = `${origin}/.well-known/nostr.json?name=_`;
|
|
116
|
-
const userUrl
|
|
159
|
+
const userUrl = `${origin}/.well-known/nostr.json?name=${name}`;
|
|
117
160
|
|
|
118
161
|
const bunkerRes = await fetch(bunkerUrl);
|
|
119
162
|
const bunkerData = await bunkerRes.json();
|
|
@@ -128,9 +171,7 @@ export const getBunkerUrl = async (value: string, optionsModal: NostrLoginOption
|
|
|
128
171
|
throw new Error('Bunker relay not provided');
|
|
129
172
|
}
|
|
130
173
|
|
|
131
|
-
const relayParams = bunkerRelays
|
|
132
|
-
.map(r => `relay=${encodeURIComponent(r)}`)
|
|
133
|
-
.join('&');
|
|
174
|
+
const relayParams = bunkerRelays.map(r => `relay=${encodeURIComponent(r)}`).join('&');
|
|
134
175
|
|
|
135
176
|
return `bunker://${userPubkey}?${relayParams}`;
|
|
136
177
|
}
|
|
@@ -197,6 +238,11 @@ const upgradeInfo = (info: Info | RecentType) => {
|
|
|
197
238
|
if (info.authMethod === 'connect' && !info.signerPubkey) {
|
|
198
239
|
info.signerPubkey = info.pubkey;
|
|
199
240
|
}
|
|
241
|
+
|
|
242
|
+
if (info.authMethod === ('amber' as any)) {
|
|
243
|
+
if (!info.signerPubkey) info.signerPubkey = '';
|
|
244
|
+
if (!(info as any).relays) (info as any).relays = [];
|
|
245
|
+
}
|
|
200
246
|
};
|
|
201
247
|
|
|
202
248
|
export const localStorageAddAccount = (info: Info) => {
|
package/tsconfig.json
CHANGED