@konemono/nostr-login 1.11.0 → 1.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.esm.js +15 -20
- package/dist/index.esm.js.map +1 -1
- package/dist/modules/AuthNostrService.d.ts +4 -14
- package/dist/modules/Nip46.d.ts +26 -64
- package/dist/modules/Signer.d.ts +6 -12
- package/dist/unpkg.js +15 -20
- package/dist/utils/index.d.ts +3 -6
- package/dist/utils/nip44.d.ts +3 -3
- package/package.json +8 -8
- package/src/modules/AuthNostrService.ts +109 -210
- package/src/modules/ModalManager.ts +3 -3
- package/src/modules/Nip46.ts +185 -390
- package/src/modules/NostrExtensionService.ts +0 -2
- package/src/modules/Signer.ts +12 -35
- package/src/utils/index.ts +23 -73
- package/src/utils/nip44.ts +7 -12
- package/test-relay-management.html +0 -407
package/src/modules/Nip46.ts
CHANGED
|
@@ -1,138 +1,58 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { validateEvent, verifyEvent, type Event as NostrEventSDK } from 'nostr-tools';
|
|
1
|
+
import NDK, { NDKEvent, NDKFilter, NDKNip46Signer, NDKNostrRpc, NDKRpcRequest, NDKRpcResponse, NDKSubscription, NDKSubscriptionCacheUsage, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
|
|
2
|
+
import { validateEvent, verifySignature } from 'nostr-tools';
|
|
4
3
|
import { PrivateKeySigner } from './Signer';
|
|
5
4
|
import { NIP46_REQUEST_TIMEOUT, NIP46_CONNECT_TIMEOUT } from '../const';
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
6
|
|
|
7
|
+
// タイムアウト付きPromiseラッパー
|
|
7
8
|
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage: string): Promise<T> {
|
|
8
9
|
return Promise.race([promise, new Promise<T>((_, reject) => setTimeout(() => reject(new Error(errorMessage)), timeoutMs))]);
|
|
9
10
|
}
|
|
10
|
-
// nostr-toolsに基づく厳格な型定義
|
|
11
|
-
type NostrFilter = {
|
|
12
|
-
ids?: string[];
|
|
13
|
-
kinds?: number[];
|
|
14
|
-
authors?: string[];
|
|
15
|
-
since?: number;
|
|
16
|
-
until?: number;
|
|
17
|
-
limit?: number;
|
|
18
|
-
search?: string;
|
|
19
|
-
[key: `#${string}`]: string[] | undefined; // タグフィルター (#e, #p, etc.)
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type NostrEvent = {
|
|
23
|
-
id?: string;
|
|
24
|
-
kind: number;
|
|
25
|
-
pubkey: string;
|
|
26
|
-
content: string;
|
|
27
|
-
tags: string[][];
|
|
28
|
-
created_at: number;
|
|
29
|
-
sig?: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
type NostrSubscription = {
|
|
33
|
-
on: (event: string, cb: any) => void;
|
|
34
|
-
stop: () => void;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
type NDKRpcResponse = {
|
|
38
|
-
id: string;
|
|
39
|
-
pubkey: string;
|
|
40
|
-
content: string;
|
|
41
|
-
result?: string;
|
|
42
|
-
error?: string;
|
|
43
|
-
[key: string]: any;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
type NDKRpcRequest = {
|
|
47
|
-
id: string;
|
|
48
|
-
pubkey: string;
|
|
49
|
-
method: string;
|
|
50
|
-
params: any[];
|
|
51
|
-
[key: string]: any;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Helper to wrap rx-nostr subscription
|
|
55
|
-
class RxReqAdapter extends EventEmitter {
|
|
56
|
-
private sub: any;
|
|
57
|
-
constructor(rxNostr: RxNostr, filters: NostrFilter[], relays?: string[]) {
|
|
58
|
-
super();
|
|
59
|
-
const req = createRxForwardReq();
|
|
60
|
-
this.sub = rxNostr.use(req).subscribe((packet) => {
|
|
61
|
-
this.emit('event', packet.event);
|
|
62
|
-
});
|
|
63
|
-
// If relays are provided, we might want to ensure they are used?
|
|
64
|
-
// rx-nostr manages relays globally usually, but we can set default relays in the instance.
|
|
65
|
-
req.emit(filters as any);
|
|
66
|
-
}
|
|
67
|
-
stop() {
|
|
68
|
-
this.sub.unsubscribe();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
11
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
public rxNostr: RxNostr;
|
|
12
|
+
class NostrRpc extends NDKNostrRpc {
|
|
13
|
+
protected _ndk: NDK;
|
|
75
14
|
protected _signer: PrivateKeySigner;
|
|
76
15
|
protected requests: Set<string> = new Set();
|
|
77
|
-
|
|
78
|
-
private sub?: RxReqAdapter;
|
|
16
|
+
private sub?: NDKSubscription;
|
|
79
17
|
protected _useNip44: boolean = false;
|
|
80
|
-
private
|
|
81
|
-
private maxReconnectAttempts: number = 3;
|
|
82
|
-
private reconnectDelay: number = 2000;
|
|
18
|
+
private eventEmitter: EventEmitter = new EventEmitter();
|
|
83
19
|
|
|
84
|
-
public constructor(
|
|
85
|
-
super();
|
|
86
|
-
this.
|
|
20
|
+
public constructor(ndk: NDK, signer: PrivateKeySigner) {
|
|
21
|
+
super(ndk, signer, ndk.debug.extend('nip46:signer:rpc'));
|
|
22
|
+
this._ndk = ndk;
|
|
87
23
|
this._signer = signer;
|
|
88
|
-
this.setupConnectionMonitoring();
|
|
89
24
|
}
|
|
90
25
|
|
|
91
|
-
public async subscribe(filter:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
this.sub = new RxReqAdapter(this.rxNostr, [f]);
|
|
26
|
+
public async subscribe(filter: NDKFilter): Promise<NDKSubscription> {
|
|
27
|
+
// NOTE: fixing ndk
|
|
28
|
+
filter.kinds = filter.kinds?.filter(k => k === 24133);
|
|
29
|
+
this.sub = await super.subscribe(filter);
|
|
98
30
|
return this.sub;
|
|
99
31
|
}
|
|
100
32
|
|
|
101
|
-
public stop()
|
|
33
|
+
public stop() {
|
|
102
34
|
if (this.sub) {
|
|
103
35
|
this.sub.stop();
|
|
104
36
|
this.sub = undefined;
|
|
105
37
|
}
|
|
106
38
|
}
|
|
107
39
|
|
|
108
|
-
public setUseNip44(useNip44: boolean)
|
|
40
|
+
public setUseNip44(useNip44: boolean) {
|
|
109
41
|
this._useNip44 = useNip44;
|
|
110
42
|
}
|
|
111
43
|
|
|
112
|
-
private isNip04(ciphertext: string)
|
|
44
|
+
private isNip04(ciphertext: string) {
|
|
113
45
|
const l = ciphertext.length;
|
|
114
46
|
if (l < 28) return false;
|
|
115
47
|
return ciphertext[l - 28] === '?' && ciphertext[l - 27] === 'i' && ciphertext[l - 26] === 'v' && ciphertext[l - 25] === '=';
|
|
116
48
|
}
|
|
117
49
|
|
|
118
50
|
// override to auto-decrypt nip04/nip44
|
|
119
|
-
public async parseEvent(event:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// 2. Check content for NIP-04 pattern (?iv=...).
|
|
123
|
-
// 3. Call signer.decrypt(remoteUser, content) or signer.decryptNip44(remoteUser, content).
|
|
124
|
-
//
|
|
125
|
-
// New Implementation Plan:
|
|
126
|
-
// 1. Use direct NIP-04/NIP-44 decryption functions from local signer.
|
|
127
|
-
// 2. Pass event.pubkey (string) instead of NDKUser object.
|
|
128
|
-
|
|
129
|
-
// const remoteUser = this._ndk.getUser({ pubkey: event.pubkey });
|
|
130
|
-
// remoteUser.ndk = this._ndk;
|
|
131
|
-
|
|
51
|
+
public async parseEvent(event: NDKEvent): Promise<NDKRpcRequest | NDKRpcResponse> {
|
|
52
|
+
const remoteUser = this._ndk.getUser({ pubkey: event.pubkey });
|
|
53
|
+
remoteUser.ndk = this._ndk;
|
|
132
54
|
const decrypt = this.isNip04(event.content) ? this._signer.decrypt : this._signer.decryptNip44;
|
|
133
|
-
|
|
134
|
-
// NOTE: We need to adjust _signer.decrypt signature to accept string pubkey
|
|
135
|
-
const decryptedContent = await decrypt.call(this._signer, event.pubkey as any, event.content);
|
|
55
|
+
const decryptedContent = await decrypt.call(this._signer, remoteUser, event.content);
|
|
136
56
|
const parsedContent = JSON.parse(decryptedContent);
|
|
137
57
|
const { id, method, params, result, error } = parsedContent;
|
|
138
58
|
|
|
@@ -143,8 +63,8 @@ class NostrRpc extends EventEmitter {
|
|
|
143
63
|
}
|
|
144
64
|
}
|
|
145
65
|
|
|
146
|
-
public async parseNostrConnectReply(reply: any, secret: string)
|
|
147
|
-
const event =
|
|
66
|
+
public async parseNostrConnectReply(reply: any, secret: string) {
|
|
67
|
+
const event = new NDKEvent(this._ndk, reply);
|
|
148
68
|
const parsedEvent = await this.parseEvent(event);
|
|
149
69
|
console.log('nostr connect parsedEvent', parsedEvent);
|
|
150
70
|
if (!(parsedEvent as NDKRpcRequest).method) {
|
|
@@ -160,15 +80,14 @@ class NostrRpc extends EventEmitter {
|
|
|
160
80
|
// we just listed to an unsolicited reply to
|
|
161
81
|
// our pubkey and if it's ack/secret - we're fine
|
|
162
82
|
public async listen(nostrConnectSecret: string): Promise<string> {
|
|
163
|
-
// TODO: rx-nostrを使用して実装する
|
|
164
83
|
const pubkey = this._signer.pubkey;
|
|
165
84
|
console.log('nostr-login listening for conn to', pubkey);
|
|
166
85
|
const sub = await this.subscribe({
|
|
167
86
|
'kinds': [24133],
|
|
168
87
|
'#p': [pubkey],
|
|
169
|
-
}
|
|
88
|
+
});
|
|
170
89
|
return new Promise<string>((ok, err) => {
|
|
171
|
-
sub.on('event', async (event:
|
|
90
|
+
sub.on('event', async (event: NDKEvent) => {
|
|
172
91
|
try {
|
|
173
92
|
const parsedEvent = await this.parseEvent(event);
|
|
174
93
|
// console.log('ack parsedEvent', parsedEvent);
|
|
@@ -187,7 +106,7 @@ class NostrRpc extends EventEmitter {
|
|
|
187
106
|
}
|
|
188
107
|
}
|
|
189
108
|
} catch (e) {
|
|
190
|
-
console.log('error parsing event', e,
|
|
109
|
+
console.log('error parsing event', e, event.rawEvent());
|
|
191
110
|
}
|
|
192
111
|
// done
|
|
193
112
|
this.stop();
|
|
@@ -198,8 +117,7 @@ class NostrRpc extends EventEmitter {
|
|
|
198
117
|
// since ndk doesn't yet support perms param
|
|
199
118
|
// we reimplement the 'connect' call here
|
|
200
119
|
// instead of await signer.blockUntilReady();
|
|
201
|
-
public async connect(pubkey: string, token?: string, perms?: string)
|
|
202
|
-
// TODO: nostr-toolsを使用して実装する
|
|
120
|
+
public async connect(pubkey: string, token?: string, perms?: string) {
|
|
203
121
|
return new Promise<void>((ok, err) => {
|
|
204
122
|
const connectParams = [pubkey!, token || '', perms || ''];
|
|
205
123
|
this.sendRequest(pubkey!, 'connect', connectParams, 24133, (response: NDKRpcResponse) => {
|
|
@@ -217,125 +135,22 @@ class NostrRpc extends EventEmitter {
|
|
|
217
135
|
return withTimeout(this.connect(pubkey, token, perms), timeoutMs, `Connection timeout after ${timeoutMs}ms`);
|
|
218
136
|
}
|
|
219
137
|
|
|
220
|
-
//
|
|
221
|
-
public async
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return withTimeout(
|
|
229
|
-
new Promise<NDKRpcResponse>((resolve, reject) => {
|
|
230
|
-
this.sendRequest(remotePubkey, method, params, kind, response => {
|
|
231
|
-
if (response.error) {
|
|
232
|
-
reject(new Error(response.error));
|
|
233
|
-
} else {
|
|
234
|
-
resolve(response);
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
}),
|
|
238
|
-
timeoutMs,
|
|
239
|
-
`Request timeout after ${timeoutMs}ms for method: ${method}`,
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 接続監視のセットアップ
|
|
244
|
-
private setupConnectionMonitoring(): void {
|
|
245
|
-
// アプリがフォアグラウンドに戻ったときの処理
|
|
246
|
-
if (typeof document !== 'undefined') {
|
|
247
|
-
document.addEventListener('visibilitychange', async () => {
|
|
248
|
-
if (document.visibilityState === 'visible') {
|
|
249
|
-
console.log('App visible, checking relay connections...');
|
|
250
|
-
await this.ensureConnected();
|
|
138
|
+
// ping実装
|
|
139
|
+
public async ping(remotePubkey: string): Promise<void> {
|
|
140
|
+
return new Promise<void>((ok, err) => {
|
|
141
|
+
this.sendRequest(remotePubkey, 'ping', [], 24133, (response: NDKRpcResponse) => {
|
|
142
|
+
if (response.result === 'pong') {
|
|
143
|
+
ok();
|
|
144
|
+
} else {
|
|
145
|
+
err(new Error(response.error || 'ping failed'));
|
|
251
146
|
}
|
|
252
147
|
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// オンライン/オフライン検知
|
|
256
|
-
if (typeof window !== 'undefined') {
|
|
257
|
-
window.addEventListener('online', async () => {
|
|
258
|
-
console.log('Network online, reconnecting relays...');
|
|
259
|
-
await this.reconnect();
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// 定期的な接続状態チェック(オプション)
|
|
264
|
-
setInterval(() => {
|
|
265
|
-
const stats = this.rxNostr.getAllRelayStatus();
|
|
266
|
-
const connected = Object.values(stats).filter(s => String(s) === 'connected').length;
|
|
267
|
-
if (connected === 0) {
|
|
268
|
-
console.warn('No relays connected, triggering reconnection...');
|
|
269
|
-
this.ensureConnected();
|
|
270
|
-
}
|
|
271
|
-
}, 60000); // 60秒ごとにチェック
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// 接続を確認して必要なら再接続
|
|
275
|
-
private async ensureConnected(): Promise<void> {
|
|
276
|
-
const states = this.rxNostr.getAllRelayStatus();
|
|
277
|
-
const connectedRelays = Object.values(states).filter(s => String(s) === 'connected');
|
|
278
|
-
|
|
279
|
-
if (connectedRelays.length === 0) {
|
|
280
|
-
console.log('No connected relays, attempting reconnection...');
|
|
281
|
-
await this.reconnect();
|
|
282
|
-
}
|
|
148
|
+
});
|
|
283
149
|
}
|
|
284
150
|
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
console.error('Max reconnection attempts reached');
|
|
289
|
-
this.emit('reconnectFailed');
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
this.reconnectAttempts++;
|
|
294
|
-
console.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
// サブスクリプションを停止
|
|
298
|
-
if (this.sub) {
|
|
299
|
-
this.sub.stop();
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// 指数バックオフ: 2^attempt * baseDelay (最大30秒)
|
|
303
|
-
const backoffDelay = Math.min(
|
|
304
|
-
this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
305
|
-
30000
|
|
306
|
-
);
|
|
307
|
-
console.log(`Waiting ${backoffDelay}ms before reconnection...`);
|
|
308
|
-
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
|
309
|
-
|
|
310
|
-
// 全リレーに再接続を試みる
|
|
311
|
-
const relays = Object.keys(this.rxNostr.getAllRelayStatus());
|
|
312
|
-
if (relays.length > 0) {
|
|
313
|
-
for (const relay of relays) {
|
|
314
|
-
try {
|
|
315
|
-
this.rxNostr.reconnect(relay);
|
|
316
|
-
console.log(`Reconnected to ${relay}`);
|
|
317
|
-
} catch (error) {
|
|
318
|
-
console.warn(`Failed to reconnect to ${relay}:`, error);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// 再接続成功後、カウンターをリセット
|
|
324
|
-
setTimeout(() => {
|
|
325
|
-
const states = this.rxNostr.getAllRelayStatus();
|
|
326
|
-
const connected = Object.values(states).filter(s => String(s) === 'connected').length;
|
|
327
|
-
if (connected > 0) {
|
|
328
|
-
console.log('Reconnection successful, resetting attempt counter');
|
|
329
|
-
this.reconnectAttempts = 0;
|
|
330
|
-
this.emit('reconnected');
|
|
331
|
-
}
|
|
332
|
-
}, 2000);
|
|
333
|
-
} catch (error) {
|
|
334
|
-
console.error('Reconnection error:', error);
|
|
335
|
-
this.emit('reconnectError', error);
|
|
336
|
-
// 次の再接続を試みる
|
|
337
|
-
setTimeout(() => this.reconnect(), this.reconnectDelay);
|
|
338
|
-
}
|
|
151
|
+
// タイムアウト対応のping
|
|
152
|
+
public async pingWithTimeout(remotePubkey: string, timeoutMs: number = 10000): Promise<void> {
|
|
153
|
+
return withTimeout(this.ping(remotePubkey), timeoutMs, `Ping timeout after ${timeoutMs}ms`);
|
|
339
154
|
}
|
|
340
155
|
|
|
341
156
|
protected getId(): string {
|
|
@@ -353,11 +168,7 @@ class NostrRpc extends EventEmitter {
|
|
|
353
168
|
console.log('sendRequest', { event, method, remotePubkey, params });
|
|
354
169
|
|
|
355
170
|
// send to relays
|
|
356
|
-
|
|
357
|
-
this.rxNostr.send(event).subscribe({
|
|
358
|
-
error: (err) => console.error('Nip46 publish error', err),
|
|
359
|
-
complete: () => console.log('Nip46 publish complete')
|
|
360
|
-
});
|
|
171
|
+
await event.publish();
|
|
361
172
|
|
|
362
173
|
// NOTE: ndk returns a promise that never resolves and
|
|
363
174
|
// in fact REQUIRES cb to be provided (otherwise no way
|
|
@@ -368,17 +179,16 @@ class NostrRpc extends EventEmitter {
|
|
|
368
179
|
return undefined as NDKRpcResponse;
|
|
369
180
|
}
|
|
370
181
|
|
|
371
|
-
protected setResponseHandler(id: string, cb?: (res: NDKRpcResponse) => void)
|
|
372
|
-
// TODO: 再実装する
|
|
182
|
+
protected setResponseHandler(id: string, cb?: (res: NDKRpcResponse) => void) {
|
|
373
183
|
let authUrlSent = false;
|
|
374
184
|
const now = Date.now();
|
|
375
185
|
return new Promise<NDKRpcResponse>(() => {
|
|
376
186
|
const responseHandler = (response: NDKRpcResponse) => {
|
|
377
187
|
if (response.result === 'auth_url') {
|
|
378
|
-
this.once(`response-${id}`, responseHandler);
|
|
188
|
+
this.eventEmitter.once(`response-${id}`, responseHandler);
|
|
379
189
|
if (!authUrlSent) {
|
|
380
190
|
authUrlSent = true;
|
|
381
|
-
this.emit('authUrl', response.error);
|
|
191
|
+
this.eventEmitter.emit('authUrl', response.error);
|
|
382
192
|
}
|
|
383
193
|
} else if (cb) {
|
|
384
194
|
if (this.requests.has(id)) {
|
|
@@ -389,108 +199,76 @@ class NostrRpc extends EventEmitter {
|
|
|
389
199
|
}
|
|
390
200
|
};
|
|
391
201
|
|
|
392
|
-
this.once(`response-${id}`, responseHandler);
|
|
202
|
+
this.eventEmitter.once(`response-${id}`, responseHandler);
|
|
393
203
|
});
|
|
394
204
|
}
|
|
395
205
|
|
|
396
|
-
protected async createRequestEvent(id: string, remotePubkey: string, method: string, params: string[] = [], kind = 24133)
|
|
397
|
-
// TODO: 元のNDKロジック:
|
|
398
|
-
// const event = new NDKEvent(this._ndk, { ... });
|
|
399
|
-
// await event.sign(this._signer);
|
|
400
|
-
//
|
|
401
|
-
// New Implementation Plan:
|
|
402
|
-
// 1. Create a plain event object:
|
|
403
|
-
// { kind, content, tags, pubkey, created_at: Math.floor(Date.now()/1000) }
|
|
404
|
-
// 2. Encrypt content:
|
|
405
|
-
// if (useNip44) await signer.encryptNip44(remotePubkey, content)
|
|
406
|
-
// else await signer.encrypt(remotePubkey, content)
|
|
407
|
-
// 3. Sign:
|
|
408
|
-
// event.id = getEventHash(event)
|
|
409
|
-
// event.sig = await signer.signEvent(event)
|
|
410
|
-
// 4. Return the signed event object.
|
|
411
|
-
|
|
206
|
+
protected async createRequestEvent(id: string, remotePubkey: string, method: string, params: string[] = [], kind = 24133) {
|
|
412
207
|
this.requests.add(id);
|
|
413
|
-
const localUser = await this._signer.user();
|
|
414
|
-
|
|
208
|
+
const localUser = await this._signer.user();
|
|
209
|
+
const remoteUser = this._ndk.getUser({ pubkey: remotePubkey });
|
|
415
210
|
const request = { id, method, params };
|
|
416
211
|
|
|
417
|
-
|
|
418
|
-
const event = {
|
|
212
|
+
const event = new NDKEvent(this._ndk, {
|
|
419
213
|
kind,
|
|
420
214
|
content: JSON.stringify(request),
|
|
421
215
|
tags: [['p', remotePubkey]],
|
|
422
216
|
pubkey: localUser.pubkey,
|
|
423
|
-
|
|
424
|
-
} as any;
|
|
217
|
+
} as NostrEvent);
|
|
425
218
|
|
|
426
219
|
const useNip44 = this._useNip44 && method !== 'create_account';
|
|
427
220
|
const encrypt = useNip44 ? this._signer.encryptNip44 : this._signer.encrypt;
|
|
428
|
-
|
|
429
|
-
// NOTE: Adjust encrypt to accept string pubkey
|
|
430
|
-
event.content = await encrypt.call(this._signer, remotePubkey as any, event.content);
|
|
431
|
-
|
|
432
|
-
// NOTE: This assumes _signer has a sign() method compatible with the event
|
|
221
|
+
event.content = await encrypt.call(this._signer, remoteUser, event.content);
|
|
433
222
|
await event.sign(this._signer);
|
|
434
223
|
|
|
435
224
|
return event;
|
|
436
225
|
}
|
|
226
|
+
|
|
227
|
+
// EventEmitter互換メソッド
|
|
228
|
+
public override on = <EventKey extends string | symbol = string>(
|
|
229
|
+
event: EventKey,
|
|
230
|
+
listener: (...args: any[]) => void
|
|
231
|
+
): this => {
|
|
232
|
+
this.eventEmitter.on(event as string, listener);
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public override emit = <EventKey extends string | symbol = string>(
|
|
237
|
+
event: EventKey,
|
|
238
|
+
...args: any[]
|
|
239
|
+
): boolean => {
|
|
240
|
+
return this.eventEmitter.emit(event as string, ...args);
|
|
241
|
+
}
|
|
437
242
|
}
|
|
438
243
|
|
|
439
244
|
export class IframeNostrRpc extends NostrRpc {
|
|
440
245
|
private peerOrigin?: string;
|
|
441
246
|
private iframePort?: MessagePort;
|
|
442
247
|
private iframeRequests = new Map<string, { id: string; pubkey: string }>();
|
|
443
|
-
private heartbeatInterval?: number;
|
|
444
|
-
private lastResponseTime: number = Date.now();
|
|
445
|
-
private heartbeatTimeoutMs: number = 30000; // 30秒応答がなければ再接続
|
|
446
248
|
|
|
447
|
-
public constructor(
|
|
448
|
-
super(
|
|
249
|
+
public constructor(ndk: NDK, localSigner: PrivateKeySigner, iframePeerOrigin?: string) {
|
|
250
|
+
super(ndk, localSigner);
|
|
251
|
+
this._ndk = ndk;
|
|
449
252
|
this.peerOrigin = iframePeerOrigin;
|
|
450
253
|
}
|
|
451
254
|
|
|
452
|
-
public async subscribe(filter:
|
|
255
|
+
public async subscribe(filter: NDKFilter): Promise<NDKSubscription> {
|
|
453
256
|
if (!this.peerOrigin) return super.subscribe(filter);
|
|
454
|
-
return super.subscribe(filter);
|
|
455
|
-
/*
|
|
456
257
|
return new NDKSubscription(
|
|
457
258
|
this._ndk,
|
|
458
259
|
{},
|
|
459
260
|
{
|
|
460
261
|
// don't send to relay
|
|
461
262
|
closeOnEose: true,
|
|
462
|
-
cacheUsage:
|
|
263
|
+
cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE,
|
|
463
264
|
},
|
|
464
265
|
);
|
|
465
|
-
*/
|
|
466
266
|
}
|
|
467
267
|
|
|
468
|
-
|
|
469
|
-
private startHeartbeat(): void {
|
|
470
|
-
this.stopHeartbeat();
|
|
471
|
-
|
|
472
|
-
this.heartbeatInterval = window.setInterval(async () => {
|
|
473
|
-
const timeSinceLastResponse = Date.now() - this.lastResponseTime;
|
|
474
|
-
|
|
475
|
-
if (timeSinceLastResponse > this.heartbeatTimeoutMs) {
|
|
476
|
-
console.warn('No response from relay for too long, reconnecting...');
|
|
477
|
-
await this.reconnect();
|
|
478
|
-
}
|
|
479
|
-
}, 10000); // 10秒ごとにチェック
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
private stopHeartbeat(): void {
|
|
483
|
-
if (this.heartbeatInterval) {
|
|
484
|
-
clearInterval(this.heartbeatInterval);
|
|
485
|
-
this.heartbeatInterval = undefined;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
public setWorkerIframePort(port: MessagePort): void {
|
|
268
|
+
public setWorkerIframePort(port: MessagePort) {
|
|
490
269
|
if (!this.peerOrigin) throw new Error('Unexpected iframe port');
|
|
491
270
|
|
|
492
271
|
this.iframePort = port;
|
|
493
|
-
this.startHeartbeat();
|
|
494
272
|
|
|
495
273
|
// to make sure Chrome doesn't terminate the channel
|
|
496
274
|
setInterval(() => {
|
|
@@ -512,11 +290,9 @@ export class IframeNostrRpc extends NostrRpc {
|
|
|
512
290
|
const event = ev.data;
|
|
513
291
|
|
|
514
292
|
if (!validateEvent(event)) throw new Error('Invalid event from iframe');
|
|
515
|
-
if (!
|
|
516
|
-
const nevent =
|
|
293
|
+
if (!verifySignature(event)) throw new Error('Invalid event signature from iframe');
|
|
294
|
+
const nevent = new NDKEvent(this._ndk, event);
|
|
517
295
|
const parsedEvent = await this.parseEvent(nevent);
|
|
518
|
-
// レスポンス受信時にタイムスタンプを更新
|
|
519
|
-
this.lastResponseTime = Date.now();
|
|
520
296
|
// we're only implementing client-side rpc
|
|
521
297
|
if (!(parsedEvent as NDKRpcRequest).method) {
|
|
522
298
|
console.log('parsed response', parsedEvent);
|
|
@@ -534,22 +310,23 @@ export class IframeNostrRpc extends NostrRpc {
|
|
|
534
310
|
// create and sign request event
|
|
535
311
|
const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
|
|
536
312
|
|
|
537
|
-
// set response handler
|
|
313
|
+
// set response handler, it will dedup auth urls,
|
|
314
|
+
// and also dedup response handlers - we're sending
|
|
315
|
+
// to relays and to iframe
|
|
538
316
|
this.setResponseHandler(id, cb);
|
|
539
317
|
|
|
540
318
|
if (this.iframePort) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
319
|
+
// map request event id to request id, if iframe
|
|
320
|
+
// has no key it will reply with error:event_id (it can't
|
|
321
|
+
// decrypt the request id without keys)
|
|
322
|
+
this.iframeRequests.set(event.id, { id, pubkey: remotePubkey });
|
|
323
|
+
|
|
324
|
+
// send to iframe
|
|
325
|
+
console.log('iframe-nip46 sending request to', this.peerOrigin, event.rawEvent());
|
|
326
|
+
this.iframePort.postMessage(event.rawEvent());
|
|
544
327
|
} else {
|
|
545
|
-
// send to relays
|
|
546
|
-
|
|
547
|
-
next: (packet) => {
|
|
548
|
-
console.log('Nip46 request published to', packet.from);
|
|
549
|
-
},
|
|
550
|
-
error: (err) => console.error('Nip46 publish error', err),
|
|
551
|
-
complete: () => console.log('Nip46 publish complete')
|
|
552
|
-
});
|
|
328
|
+
// send to relays
|
|
329
|
+
await event.publish();
|
|
553
330
|
}
|
|
554
331
|
|
|
555
332
|
// see notes in 'super'
|
|
@@ -591,53 +368,80 @@ export class ReadyListener {
|
|
|
591
368
|
async wait(): Promise<any> {
|
|
592
369
|
console.log(new Date(), 'waiting for', this.messages);
|
|
593
370
|
const r = await this.promise;
|
|
594
|
-
// NOTE: timer here doesn't help bcs it must be activated when
|
|
595
|
-
// user "confirms", but that's happening on a different
|
|
596
|
-
// origin and we can't really know.
|
|
597
|
-
// await new Promise<any>((ok, err) => {
|
|
598
|
-
// // 10 sec should be more than enough
|
|
599
|
-
// setTimeout(() => err(new Date() + ' timeout for ' + this.message), 10000);
|
|
600
|
-
|
|
601
|
-
// // if promise already resolved or will resolve in the future
|
|
602
|
-
// this.promise.then(ok);
|
|
603
|
-
// });
|
|
604
|
-
|
|
605
371
|
console.log(new Date(), 'finished waiting for', this.messages, r);
|
|
606
372
|
return r;
|
|
607
373
|
}
|
|
608
374
|
}
|
|
609
375
|
|
|
610
|
-
|
|
611
|
-
export class Nip46Signer extends EventEmitter {
|
|
612
|
-
public remotePubkey: string = '';
|
|
613
|
-
// @ts-ignore
|
|
614
|
-
public rpc: NostrRpc;
|
|
376
|
+
export class Nip46Signer extends NDKNip46Signer {
|
|
615
377
|
private _userPubkey: string = '';
|
|
616
378
|
private _rpc: IframeNostrRpc;
|
|
379
|
+
private lastPingTime: number = 0;
|
|
380
|
+
private pingCacheDuration: number = 30000; // 30秒
|
|
381
|
+
private _remotePubkey?: string;
|
|
617
382
|
|
|
618
|
-
constructor(
|
|
619
|
-
super();
|
|
620
|
-
// super(ndk, signerPubkey, localSigner);
|
|
383
|
+
constructor(ndk: NDK, localSigner: PrivateKeySigner, signerPubkey: string, iframeOrigin?: string) {
|
|
384
|
+
super(ndk, signerPubkey, localSigner);
|
|
621
385
|
|
|
622
386
|
// override with our own rpc implementation
|
|
623
|
-
this._rpc = new IframeNostrRpc(
|
|
624
|
-
this._rpc.setUseNip44(true);
|
|
387
|
+
this._rpc = new IframeNostrRpc(ndk, localSigner, iframeOrigin);
|
|
388
|
+
this._rpc.setUseNip44(true);
|
|
625
389
|
this._rpc.on('authUrl', (url: string) => {
|
|
626
390
|
this.emit('authUrl', url);
|
|
627
391
|
});
|
|
628
392
|
|
|
629
393
|
this.rpc = this._rpc;
|
|
394
|
+
this._remotePubkey = signerPubkey;
|
|
630
395
|
}
|
|
631
396
|
|
|
632
|
-
get userPubkey()
|
|
397
|
+
get userPubkey() {
|
|
633
398
|
return this._userPubkey;
|
|
634
399
|
}
|
|
635
400
|
|
|
636
|
-
|
|
401
|
+
// Use a different name to avoid conflict with base class property
|
|
402
|
+
get remotePubkeyAccessor() {
|
|
403
|
+
return this._remotePubkey;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
set remotePubkeyAccessor(value: string | undefined) {
|
|
407
|
+
this._remotePubkey = value;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 接続確認(必要時のみping)
|
|
411
|
+
private async ensureConnection(retries: number = 2): Promise<void> {
|
|
412
|
+
if (!this._remotePubkey) return;
|
|
413
|
+
|
|
414
|
+
const now = Date.now();
|
|
415
|
+
|
|
416
|
+
// 最近ping成功していればスキップ
|
|
417
|
+
if (now - this.lastPingTime < this.pingCacheDuration) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (let i = 0; i <= retries; i++) {
|
|
422
|
+
try {
|
|
423
|
+
await this._rpc.pingWithTimeout(this._remotePubkey, 10000);
|
|
424
|
+
this.lastPingTime = now;
|
|
425
|
+
console.log('Connection check OK');
|
|
426
|
+
return;
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (i === retries) {
|
|
429
|
+
console.error('Connection check failed after retries', error);
|
|
430
|
+
throw new Error('NIP-46 connection lost');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const delay = Math.min(1000 * Math.pow(2, i), 5000);
|
|
434
|
+
console.log(`Ping failed, retrying in ${delay}ms...`);
|
|
435
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private async setSignerPubkey(signerPubkey: string, sameAsUser: boolean = false) {
|
|
637
441
|
console.log('setSignerPubkey', signerPubkey);
|
|
638
442
|
|
|
639
443
|
// ensure it's set
|
|
640
|
-
this.
|
|
444
|
+
this._remotePubkey = signerPubkey;
|
|
641
445
|
|
|
642
446
|
// when we're sure it's known
|
|
643
447
|
this._rpc.on(`iframeRestart-${signerPubkey}`, () => {
|
|
@@ -648,7 +452,7 @@ export class Nip46Signer extends EventEmitter {
|
|
|
648
452
|
await this.initUserPubkey(sameAsUser ? signerPubkey : '');
|
|
649
453
|
}
|
|
650
454
|
|
|
651
|
-
public async initUserPubkey(hintPubkey?: string)
|
|
455
|
+
public async initUserPubkey(hintPubkey?: string) {
|
|
652
456
|
if (this._userPubkey) throw new Error('Already called initUserPubkey');
|
|
653
457
|
|
|
654
458
|
if (hintPubkey) {
|
|
@@ -658,14 +462,14 @@ export class Nip46Signer extends EventEmitter {
|
|
|
658
462
|
|
|
659
463
|
this._userPubkey = await withTimeout(
|
|
660
464
|
new Promise<string>((ok, err) => {
|
|
661
|
-
if (!this.
|
|
465
|
+
if (!this._remotePubkey) throw new Error('Signer pubkey not set');
|
|
662
466
|
|
|
663
|
-
console.log('get_public_key', this.
|
|
664
|
-
this._rpc.sendRequest(this.
|
|
467
|
+
console.log('get_public_key', this._remotePubkey);
|
|
468
|
+
this._rpc.sendRequest(this._remotePubkey, 'get_public_key', [], 24133, (response: NDKRpcResponse) => {
|
|
665
469
|
if (response.error) {
|
|
666
470
|
err(new Error(response.error));
|
|
667
471
|
} else {
|
|
668
|
-
ok(response.result
|
|
472
|
+
ok(response.result);
|
|
669
473
|
}
|
|
670
474
|
});
|
|
671
475
|
}),
|
|
@@ -674,23 +478,48 @@ export class Nip46Signer extends EventEmitter {
|
|
|
674
478
|
);
|
|
675
479
|
}
|
|
676
480
|
|
|
677
|
-
public async listen(nostrConnectSecret: string)
|
|
481
|
+
public async listen(nostrConnectSecret: string) {
|
|
678
482
|
const signerPubkey = await (this.rpc as IframeNostrRpc).listen(nostrConnectSecret);
|
|
679
483
|
await this.setSignerPubkey(signerPubkey);
|
|
484
|
+
|
|
485
|
+
// ログイン完了後に接続確認
|
|
486
|
+
await this.ensureConnection();
|
|
680
487
|
}
|
|
681
488
|
|
|
682
|
-
public async connect(token?: string, perms?: string)
|
|
683
|
-
if (!this.
|
|
684
|
-
await
|
|
685
|
-
await this.setSignerPubkey(this.
|
|
489
|
+
public async connect(token?: string, perms?: string) {
|
|
490
|
+
if (!this._remotePubkey) throw new Error('No signer pubkey');
|
|
491
|
+
await this._rpc.connectWithTimeout(this._remotePubkey, token, perms, NIP46_CONNECT_TIMEOUT);
|
|
492
|
+
await this.setSignerPubkey(this._remotePubkey);
|
|
493
|
+
|
|
494
|
+
// ログイン完了後に接続確認
|
|
495
|
+
await this.ensureConnection();
|
|
686
496
|
}
|
|
687
497
|
|
|
688
|
-
public async setListenReply(reply: any, nostrConnectSecret: string)
|
|
498
|
+
public async setListenReply(reply: any, nostrConnectSecret: string) {
|
|
689
499
|
const signerPubkey = await this._rpc.parseNostrConnectReply(reply, nostrConnectSecret);
|
|
690
500
|
await this.setSignerPubkey(signerPubkey, true);
|
|
501
|
+
|
|
502
|
+
// ログイン完了後に接続確認
|
|
503
|
+
await this.ensureConnection();
|
|
691
504
|
}
|
|
692
505
|
|
|
693
|
-
|
|
506
|
+
// 署名メソッドのオーバーライド - 署名前に接続確認
|
|
507
|
+
async sign(event: NostrEvent): Promise<string> {
|
|
508
|
+
await this.ensureConnection();
|
|
509
|
+
return super.sign(event);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async encrypt(recipient: NDKUser, value: string): Promise<string> {
|
|
513
|
+
await this.ensureConnection();
|
|
514
|
+
return super.encrypt(recipient, value);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async decrypt(sender: NDKUser, value: string): Promise<string> {
|
|
518
|
+
await this.ensureConnection();
|
|
519
|
+
return super.decrypt(sender, value);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
public async createAccount2({ bunkerPubkey, name, domain, perms = '' }: { bunkerPubkey: string; name: string; domain: string; perms?: string }) {
|
|
694
523
|
const params = [
|
|
695
524
|
name,
|
|
696
525
|
domain,
|
|
@@ -703,52 +532,18 @@ export class Nip46Signer extends EventEmitter {
|
|
|
703
532
|
});
|
|
704
533
|
|
|
705
534
|
console.log('create_account pubkey', r);
|
|
706
|
-
if (r.result === 'error'
|
|
707
|
-
throw new Error(r.error
|
|
535
|
+
if (r.result === 'error') {
|
|
536
|
+
throw new Error(r.error);
|
|
708
537
|
}
|
|
709
538
|
|
|
710
539
|
return r.result;
|
|
711
540
|
}
|
|
712
|
-
// TODO: 必要であればNDKNip46Signerから不足しているメソッドを実装する
|
|
713
|
-
public async encrypt(recipient: any, value: string): Promise<string> {
|
|
714
|
-
const recipientPubkey = typeof recipient === 'string' ? recipient : recipient.pubkey;
|
|
715
|
-
return await this.rpcSend('nip04_encrypt', [recipientPubkey, value]);
|
|
716
|
-
}
|
|
717
541
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
542
|
+
// EventEmitter互換メソッド
|
|
543
|
+
public override emit = <EventKey extends string | symbol = string>(
|
|
544
|
+
event: EventKey,
|
|
545
|
+
...args: any[]
|
|
546
|
+
): boolean => {
|
|
547
|
+
return this._rpc.emit(event as string, ...args);
|
|
721
548
|
}
|
|
722
|
-
|
|
723
|
-
public async encryptNip44(recipient: any, value: string): Promise<string> {
|
|
724
|
-
const recipientPubkey = typeof recipient === 'string' ? recipient : recipient.pubkey;
|
|
725
|
-
return await this.rpcSend('nip44_encrypt', [recipientPubkey, value]);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
public async decryptNip44(sender: any, value: string): Promise<string> {
|
|
729
|
-
const senderPubkey = typeof sender === 'string' ? sender : sender.pubkey;
|
|
730
|
-
return await this.rpcSend('nip44_decrypt', [senderPubkey, value]);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
public async sign(event: any): Promise<string> {
|
|
734
|
-
const eventString = JSON.stringify(event);
|
|
735
|
-
const res = await this.rpcSend('sign_event', [eventString]);
|
|
736
|
-
// The result matches NIP-46 sign_event response which is the signed event (stringified json)
|
|
737
|
-
const signedEvent = JSON.parse(res);
|
|
738
|
-
event.sig = signedEvent.sig;
|
|
739
|
-
return signedEvent.sig;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
public async user(): Promise<any> {
|
|
743
|
-
return { pubkey: this.userPubkey };
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
private async rpcSend(method: string, params: any[]): Promise<string> {
|
|
747
|
-
return new Promise<string>((resolve, reject) => {
|
|
748
|
-
this.rpc.sendRequest(this.remotePubkey, method, params, 24133, (response: any) => {
|
|
749
|
-
if (response.error) reject(new Error(response.error));
|
|
750
|
-
else resolve(response.result);
|
|
751
|
-
});
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
}
|
|
549
|
+
}
|