@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.
@@ -1,138 +1,58 @@
1
- import { createRxForwardReq, createRxNostr, RxNostr } from 'rx-nostr';
2
- import { EventEmitter } from 'tseep';
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
- // TODO: NDKの継承を削除する
73
- class NostrRpc extends EventEmitter {
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
- // private sub?: NDKSubscription;
78
- private sub?: RxReqAdapter;
16
+ private sub?: NDKSubscription;
79
17
  protected _useNip44: boolean = false;
80
- private reconnectAttempts: number = 0;
81
- private maxReconnectAttempts: number = 3;
82
- private reconnectDelay: number = 2000;
18
+ private eventEmitter: EventEmitter = new EventEmitter();
83
19
 
84
- public constructor(rxNostr: RxNostr, signer: PrivateKeySigner) {
85
- super();
86
- this.rxNostr = rxNostr;
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: NostrFilter): Promise<NostrSubscription> {
92
- const f: any = { ...filter };
93
- if (f.kinds) {
94
- f.kinds = f.kinds.filter((k: number) => k === 24133);
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(): void {
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): void {
40
+ public setUseNip44(useNip44: boolean) {
109
41
  this._useNip44 = useNip44;
110
42
  }
111
43
 
112
- private isNip04(ciphertext: string): boolean {
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: NostrEvent): Promise<any> {
120
- // TODO: 元のNDKロジック:
121
- // 1. NDKUser.from(event.pubkey) to get remote user.
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): Promise<string> {
147
- const event = reply as NostrEvent;
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
- } as any); // Cast to any because RxReqAdapter expects NostrFilter but we pass explicit strict filters that might differ slightly in type def
88
+ });
170
89
  return new Promise<string>((ok, err) => {
171
- sub.on('event', async (event: NostrEvent) => {
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, (event as any).rawEvent?.());
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): Promise<void> {
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
- // タイムアウト対応のsendRequest
221
- public async sendRequestWithTimeout(
222
- remotePubkey: string,
223
- method: string,
224
- params: string[] = [],
225
- kind = 24133,
226
- timeoutMs: number = NIP46_REQUEST_TIMEOUT,
227
- ): Promise<NDKRpcResponse> {
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
- protected async reconnect(): Promise<void> {
287
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
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
- // await event.publish();
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): Promise<NDKRpcResponse> {
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): Promise<NostrEvent> {
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(); // TODO: signerプロパティから取得する
414
- // const remoteUser = this._ndk.getUser({ pubkey: remotePubkey });
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
- // Placeholder event construction
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
- created_at: Math.floor(Date.now() / 1000),
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(rxNostr: RxNostr, localSigner: PrivateKeySigner, iframePeerOrigin?: string) {
448
- super(rxNostr, localSigner);
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: NostrFilter): Promise<NostrSubscription> {
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: 'ONLY_CACHE',
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 (!verifyEvent(event)) throw new Error('Invalid event signature from iframe');
516
- const nevent = event as NostrEvent;
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
- this.iframeRequests.set(event.id!, { id, pubkey: remotePubkey });
542
- console.log('iframe-nip46 sending request to', this.peerOrigin, event);
543
- this.iframePort.postMessage(event);
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 using rx-nostr
546
- this.rxNostr.send(event).subscribe({
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
- // TODO: NDKの継承を削除する
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(rxNostr: RxNostr, localSigner: PrivateKeySigner, signerPubkey: string, iframeOrigin?: string) {
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(rxNostr, localSigner, iframeOrigin);
624
- this._rpc.setUseNip44(true); // !!this.params.optionsModal.dev);
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(): string {
397
+ get userPubkey() {
633
398
  return this._userPubkey;
634
399
  }
635
400
 
636
- private async setSignerPubkey(signerPubkey: string, sameAsUser: boolean = false): Promise<void> {
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.remotePubkey = signerPubkey;
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): Promise<void> {
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.remotePubkey) throw new Error('Signer pubkey not set');
465
+ if (!this._remotePubkey) throw new Error('Signer pubkey not set');
662
466
 
663
- console.log('get_public_key', this.remotePubkey);
664
- this._rpc.sendRequest(this.remotePubkey, 'get_public_key', [], 24133, (response: NDKRpcResponse) => {
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): Promise<void> {
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): Promise<void> {
683
- if (!this.remotePubkey) throw new Error('No signer pubkey');
684
- await (this._rpc as any).connectWithTimeout(this.remotePubkey, token, perms, NIP46_CONNECT_TIMEOUT);
685
- await this.setSignerPubkey(this.remotePubkey);
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): Promise<void> {
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
- public async createAccount2({ bunkerPubkey, name, domain, perms = '' }: { bunkerPubkey: string; name: string; domain: string; perms?: string }): Promise<string> {
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' || !r.result) {
707
- throw new Error(r.error || 'Unknown 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
- public async decrypt(sender: any, value: string): Promise<string> {
719
- const senderPubkey = typeof sender === 'string' ? sender : sender.pubkey;
720
- return await this.rpcSend('nip04_decrypt', [senderPubkey, value]);
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
+ }