@konemono/nostr-login 1.8.0 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konemono/nostr-login",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.esm.js",
6
6
  "types": "./dist/index.d.ts",
@@ -656,7 +656,7 @@ class AuthNostrService extends EventEmitter implements Signer {
656
656
  }
657
657
 
658
658
  private async listen(info: Info) {
659
- if (!info.iframeUrl) return this.signer!.listen(this.nostrConnectSecret);
659
+ if (!info.iframeUrl) return this.signer!.listen(this.nostrConnectSecret, 60000);
660
660
  const r = await this.starterReady!.wait();
661
661
  if (r[0] === 'starterError') throw new Error(r[1]);
662
662
  return this.signer!.setListenReply(r[1], this.nostrConnectSecret);
@@ -717,6 +717,11 @@ class AuthNostrService extends EventEmitter implements Signer {
717
717
  remotePubkey: info.signerPubkey!,
718
718
  relays: info.relays || [],
719
719
  timeoutMs: 30000,
720
+ useNip44: true,
721
+ retryConfig: {
722
+ maxRetries: 3,
723
+ retryDelayMs: 1000,
724
+ },
720
725
  });
721
726
 
722
727
  const adapter = new Nip46Adapter(client, localSigner);
@@ -773,24 +778,44 @@ class AuthNostrService extends EventEmitter implements Signer {
773
778
  if (domain) info.domain = domain;
774
779
  if (iframeUrl) info.iframeUrl = iframeUrl;
775
780
 
776
- // console.log('nostr login auth info', info);
781
+ console.log('authNip46', type, info);
782
+
777
783
  if (!info.signerPubkey || !info.sk || !info.relays || info.relays.length === 0) {
778
- throw new Error(`Bad bunker url ${bunkerUrl}`);
784
+ throw new Error(`Invalid bunker URL format`);
779
785
  }
780
786
 
781
787
  const eventToAddAccount = Boolean(this.params.userInfo);
782
- console.log('authNip46', type, info);
788
+
789
+ // 接続モードに応じた処理
790
+ const connectMode = type === 'login' && !info.token ? 'listen' : 'connect';
791
+ console.log('authNip46 connection mode:', connectMode);
783
792
 
784
793
  // updates the info
785
- await this.initSigner(info, { connect: true, eventToAddAccount });
794
+ await this.initSigner(info, {
795
+ listen: connectMode === 'listen',
796
+ connect: connectMode === 'connect',
797
+ eventToAddAccount,
798
+ });
786
799
 
787
800
  // callback
788
801
  this.onAuth(type, info);
789
- } catch (e) {
790
- console.log('nostr login auth failed', e);
791
- // make ure it's closed
792
- // this.popupManager.closePopup();
793
- throw e;
802
+ } catch (error: any) {
803
+ console.error('nostr-login auth failed:', {
804
+ type,
805
+ error: error.message,
806
+ stack: error.stack,
807
+ });
808
+
809
+ // エラーの種類に応じた処理
810
+ if (error.message.includes('timeout') || error.message.includes('timed out')) {
811
+ throw new Error('接続がタイムアウトしました。ネットワークを確認してください。');
812
+ } else if (error.message.includes('Invalid bunker URL')) {
813
+ throw new Error('無効なbunker URL形式です。');
814
+ } else if (error.message.includes('publish')) {
815
+ throw new Error('リレーへの接続に失敗しました。再度お試しください。');
816
+ } else {
817
+ throw new Error(`認証に失敗しました: ${error.message}`);
818
+ }
794
819
  }
795
820
  }
796
821
 
@@ -27,38 +27,83 @@ export class Nip46Adapter extends EventEmitter {
27
27
  if (this.userPubkey) throw new Error('Already called initUserPubkey');
28
28
  if (hintPubkey) {
29
29
  this.userPubkey = hintPubkey;
30
+ console.log('[Nip46Adapter] User pubkey set from hint:', hintPubkey);
30
31
  return;
31
32
  }
32
33
 
33
- const res = await this.client.sendRequest('get_public_key', []);
34
- if (!res) throw new Error('No public key returned');
35
- this.userPubkey = res;
34
+ console.log('[Nip46Adapter] Fetching user pubkey');
35
+ try {
36
+ const res = await this.client.sendRequest('get_public_key', []);
37
+ if (!res) throw new Error('No public key returned');
38
+ this.userPubkey = res;
39
+ console.log('[Nip46Adapter] User pubkey fetched:', res);
40
+ } catch (error: any) {
41
+ console.error('[Nip46Adapter] Failed to get user pubkey:', error.message);
42
+ throw error;
43
+ }
36
44
  }
37
45
 
38
- async listen(nostrConnectSecret: string): Promise<string> {
39
- return new Promise<string>((ok, err) => {
46
+ /**
47
+ * nostrconnect:// フロー - 受信待機
48
+ * サイナーからの接続を待つ
49
+ */
50
+ async listen(nostrConnectSecret: string, timeoutMs: number = 60000): Promise<string> {
51
+ console.log('[Nip46Adapter] Starting listen mode, timeout:', timeoutMs);
52
+
53
+ return new Promise<string>((resolve, reject) => {
54
+ const timer = setTimeout(() => {
55
+ cleanup();
56
+ const error = new Error(`Listen timeout after ${timeoutMs}ms`);
57
+ console.error('[Nip46Adapter]', error.message);
58
+ reject(error);
59
+ }, timeoutMs);
60
+
61
+ const cleanup = () => {
62
+ clearTimeout(timer);
63
+ this.client.off('response', onResponse);
64
+ };
65
+
40
66
  const onResponse = ({ response, pubkey }: any) => {
41
67
  if (!response) return;
68
+
69
+ // auth_urlは無視(別のハンドラが処理)
42
70
  if (response.result === 'auth_url') return;
71
+
72
+ // 成功: ack または secret が返される
43
73
  if (response.result === 'ack' || response.result === nostrConnectSecret) {
44
- this.client.off('response', onResponse);
45
- ok(pubkey);
74
+ cleanup();
75
+ console.log('[Nip46Adapter] Listen succeeded, signer pubkey:', pubkey);
76
+ resolve(pubkey);
77
+ } else if (response.error) {
78
+ cleanup();
79
+ console.error('[Nip46Adapter] Listen failed:', response.error);
80
+ reject(new Error(response.error));
46
81
  }
47
82
  };
48
83
 
49
84
  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
85
  });
57
86
  }
58
87
 
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');
88
+ /**
89
+ * bunker:// フロー - 能動的接続
90
+ * サイナーに接続リクエストを送る
91
+ */
92
+ async connect(token?: string, perms?: string, timeoutMs: number = 30000): Promise<void> {
93
+ console.log('[Nip46Adapter] Connecting with token, timeout:', timeoutMs);
94
+
95
+ try {
96
+ const result = await this.client.sendRequest('connect', [this.localSigner.pubkey, token || '', perms || ''], timeoutMs);
97
+
98
+ if (result !== 'ack') {
99
+ throw new Error(`Connect failed: ${result || 'unknown error'}`);
100
+ }
101
+
102
+ console.log('[Nip46Adapter] Connected successfully');
103
+ } catch (error: any) {
104
+ console.error('[Nip46Adapter] Connect failed:', error.message);
105
+ throw error;
106
+ }
62
107
  }
63
108
 
64
109
  async setListenReply(reply: any, nostrConnectSecret: string) {
@@ -80,10 +125,17 @@ export class Nip46Adapter extends EventEmitter {
80
125
  async createAccount2({ bunkerPubkey, name, domain, perms = '' }: { bunkerPubkey: string; name: string; domain: string; perms?: string }) {
81
126
  const params = [name, domain, '', perms];
82
127
 
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;
128
+ console.log('[Nip46Adapter] Creating account:', { name, domain });
129
+ try {
130
+ const r = await this.client.sendRequest('create_account', params);
131
+ if (!r) throw new Error('create_account returned empty result');
132
+ if (r === 'error') throw new Error('create_account failed');
133
+ console.log('[Nip46Adapter] Account created successfully');
134
+ return r;
135
+ } catch (error: any) {
136
+ console.error('[Nip46Adapter] Failed to create account:', error.message);
137
+ throw error;
138
+ }
87
139
  }
88
140
 
89
141
  async encrypt(recipientPubkey: string, plaintext: string) {
@@ -97,14 +149,19 @@ export class Nip46Adapter extends EventEmitter {
97
149
  }
98
150
 
99
151
  async sign(event: any) {
100
- const r = await this.client.sendRequest('sign_event', [JSON.stringify(event)]);
101
152
  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
153
+ const r = await this.client.sendRequest('sign_event', [JSON.stringify(event)]);
154
+ try {
155
+ const parsed = typeof r === 'string' ? JSON.parse(r) : r;
156
+ if (parsed && parsed.sig) return parsed.sig;
157
+ } catch (e) {
158
+ // not JSON
159
+ }
160
+ return r;
161
+ } catch (error: any) {
162
+ console.error('[Nip46Adapter] Failed to sign event:', error.message);
163
+ throw error;
106
164
  }
107
- return r;
108
165
  }
109
166
 
110
167
  // provide rpc compatibility
@@ -15,6 +15,9 @@ export class Nip46Client extends EventEmitter {
15
15
  private subscription: any = null;
16
16
  private isSubscribed: boolean = false;
17
17
  private nip44Codec: Nip44 = new Nip44();
18
+ private iframeConfig?: { origin: string; port?: MessagePort };
19
+ private retryConfig: { maxRetries: number; retryDelayMs: number };
20
+ private iframeKeepaliveInterval?: NodeJS.Timeout;
18
21
 
19
22
  constructor(options: Nip46ClientOptions) {
20
23
  super();
@@ -24,6 +27,13 @@ export class Nip46Client extends EventEmitter {
24
27
  this.relays = options.relays;
25
28
  this.defaultTimeoutMs = options.timeoutMs || 30000;
26
29
  this.useNip44 = options.useNip44 || false;
30
+ this.iframeConfig = options.iframeConfig;
31
+ this.retryConfig = options.retryConfig || { maxRetries: 3, retryDelayMs: 1000 };
32
+
33
+ // iframe用のメッセージハンドラを設定
34
+ if (this.iframeConfig?.port) {
35
+ this.setupIframePort(this.iframeConfig.port);
36
+ }
27
37
  }
28
38
 
29
39
  get localPubkey(): string {
@@ -31,9 +41,40 @@ export class Nip46Client extends EventEmitter {
31
41
  }
32
42
 
33
43
  /**
34
- * NIP-46リクエストを送信
44
+ * NIP-46リクエストを送信(リトライ機能付き)
35
45
  */
36
46
  async sendRequest(method: string, params: string[] = [], timeoutMs?: number): Promise<string> {
47
+ let lastError: Error | null = null;
48
+
49
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
50
+ try {
51
+ if (attempt > 0) {
52
+ const delay = this.retryConfig.retryDelayMs * attempt;
53
+ console.log(`[Nip46Client] Retry attempt ${attempt}/${this.retryConfig.maxRetries} for ${method} after ${delay}ms`);
54
+ await new Promise(resolve => setTimeout(resolve, delay));
55
+ }
56
+
57
+ return await this.sendRequestInternal(method, params, timeoutMs);
58
+ } catch (error: any) {
59
+ lastError = error;
60
+
61
+ // タイムアウトまたはネットワークエラーの場合のみリトライ
62
+ const shouldRetry = error.message.includes('timed out') || error.message.includes('network') || error.message.includes('publish');
63
+
64
+ if (!shouldRetry || attempt === this.retryConfig.maxRetries) {
65
+ console.error(`[Nip46Client] Request failed after ${attempt + 1} attempts:`, error.message);
66
+ throw error;
67
+ }
68
+ }
69
+ }
70
+
71
+ throw lastError || new Error('Request failed after retries');
72
+ }
73
+
74
+ /**
75
+ * NIP-46リクエストを送信(内部実装)
76
+ */
77
+ private async sendRequestInternal(method: string, params: string[] = [], timeoutMs?: number): Promise<string> {
37
78
  const timeout = timeoutMs || this.defaultTimeoutMs;
38
79
  const id = this.generateId();
39
80
  const request: Nip46Request = { id, method, params };
@@ -94,9 +135,25 @@ export class Nip46Client extends EventEmitter {
94
135
  // 署名
95
136
  event.sig = getSignature(event, this.localPrivateKey);
96
137
 
138
+ // iframeがある場合は優先的に使用
139
+ if (this.iframeConfig?.port) {
140
+ try {
141
+ this.iframeConfig.port.postMessage(event);
142
+ console.log('[Nip46Client] Request sent via iframe:', request.id);
143
+ return;
144
+ } catch (e) {
145
+ console.warn('[Nip46Client] Iframe send failed, falling back to relays:', e);
146
+ }
147
+ }
148
+
97
149
  // リレーに送信
98
- await Promise.any(this.pool.publish(this.relays, event));
99
- console.log('[Nip46Client] Request published:', request.id);
150
+ try {
151
+ await Promise.any(this.pool.publish(this.relays, event));
152
+ console.log('[Nip46Client] Request published to relays:', request.id);
153
+ } catch (e) {
154
+ console.error('[Nip46Client] Failed to publish to relays:', e);
155
+ throw new Error('Failed to publish request to relays');
156
+ }
100
157
  }
101
158
 
102
159
  /**
@@ -146,10 +203,12 @@ export class Nip46Client extends EventEmitter {
146
203
  // Emit response event for consumers (include sender pubkey)
147
204
  this.emit('response', { response, pubkey: event.pubkey });
148
205
 
149
- // auth_urlの特別処理
206
+ // auth_urlの特別処理: OAuth完了後に実際のレスポンスが来るまで待つ
150
207
  if (response.result === 'auth_url') {
151
208
  console.log('[Nip46Client] Auth URL received:', response.error);
152
209
  this.emit('authUrl', response.error);
210
+ // 注意: pendingRequestsは削除しない
211
+ // OAuth完了後に同じIDで実際のレスポンスが返ってくる
153
212
  return;
154
213
  }
155
214
 
@@ -232,6 +291,12 @@ export class Nip46Client extends EventEmitter {
232
291
  this.isSubscribed = false;
233
292
  }
234
293
 
294
+ // iframeのkeepaliveをクリア
295
+ if (this.iframeKeepaliveInterval) {
296
+ clearInterval(this.iframeKeepaliveInterval);
297
+ this.iframeKeepaliveInterval = undefined;
298
+ }
299
+
235
300
  // リレー接続を閉じる
236
301
  this.pool.close(this.relays);
237
302
 
@@ -245,4 +310,47 @@ export class Nip46Client extends EventEmitter {
245
310
  isConnected(): boolean {
246
311
  return this.isSubscribed;
247
312
  }
313
+
314
+ /**
315
+ * iframeポートを設定
316
+ */
317
+ setIframePort(port: MessagePort): void {
318
+ if (!this.iframeConfig) {
319
+ throw new Error('Iframe config not set');
320
+ }
321
+ this.iframeConfig.port = port;
322
+ this.setupIframePort(port);
323
+ }
324
+
325
+ /**
326
+ * iframe用のメッセージハンドラを設定
327
+ */
328
+ private setupIframePort(port: MessagePort): void {
329
+ port.onmessage = async (ev: MessageEvent) => {
330
+ if (ev.data === 'ping') return;
331
+
332
+ try {
333
+ const event = ev.data;
334
+ // validate and handle event
335
+ if (!event || typeof event !== 'object') {
336
+ console.warn('[Nip46Client] Invalid message from iframe');
337
+ return;
338
+ }
339
+ await this.handleResponseEvent(event);
340
+ } catch (e) {
341
+ console.error('[Nip46Client] Iframe message error:', e);
342
+ }
343
+ };
344
+
345
+ // Keep alive
346
+ this.iframeKeepaliveInterval = setInterval(() => {
347
+ try {
348
+ port.postMessage('ping');
349
+ } catch (e) {
350
+ console.warn('[Nip46Client] Failed to send keepalive ping:', e);
351
+ }
352
+ }, 5000);
353
+
354
+ console.log('[Nip46Client] Iframe port setup complete');
355
+ }
248
356
  }
@@ -1,26 +1,34 @@
1
1
  export interface Nip46Request {
2
- id: string;
3
- method: string;
4
- params: string[];
2
+ id: string;
3
+ method: string;
4
+ params: string[];
5
5
  }
6
6
 
7
7
  export interface Nip46Response {
8
- id: string;
9
- result?: string;
10
- error?: string;
8
+ id: string;
9
+ result?: string;
10
+ error?: string;
11
11
  }
12
12
 
13
13
  export interface PendingRequest {
14
- resolve: (result: string) => void;
15
- reject: (error: Error) => void;
16
- timer: NodeJS.Timeout;
17
- method: string;
14
+ resolve: (result: string) => void;
15
+ reject: (error: Error) => void;
16
+ timer: NodeJS.Timeout;
17
+ method: string;
18
18
  }
19
19
 
20
20
  export interface Nip46ClientOptions {
21
- localPrivateKey: string;
22
- remotePubkey: string;
23
- relays: string[];
24
- timeoutMs?: number;
25
- useNip44?: boolean;
21
+ localPrivateKey: string;
22
+ remotePubkey: string;
23
+ relays: string[];
24
+ timeoutMs?: number;
25
+ useNip44?: boolean;
26
+ iframeConfig?: {
27
+ origin: string;
28
+ port?: MessagePort;
29
+ };
30
+ retryConfig?: {
31
+ maxRetries: number;
32
+ retryDelayMs: number;
33
+ };
26
34
  }