@konemono/nostr-login 1.8.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.esm.js +1 -1
- package/dist/modules/nip46/Nip46Adapter.d.ts +10 -2
- package/dist/modules/nip46/Nip46Client.d.ts +16 -1
- package/dist/modules/nip46/types.d.ts +8 -0
- package/dist/unpkg.js +1 -1
- package/package.json +1 -1
- package/src/modules/AmberDirectSigner.ts +6 -2
- package/src/modules/AuthNostrService.ts +65 -22
- package/src/modules/nip46/Nip46Adapter.ts +83 -26
- package/src/modules/nip46/Nip46Client.ts +112 -4
- package/src/modules/nip46/types.ts +23 -15
package/package.json
CHANGED
|
@@ -30,12 +30,14 @@ export class AmberDirectSigner implements Signer {
|
|
|
30
30
|
public async getPublicKey(id?: string): Promise<string> {
|
|
31
31
|
id = id || Math.random().toString(36).substring(7);
|
|
32
32
|
const url = this.generateUrl('', 'get_public_key', id);
|
|
33
|
-
console.log('
|
|
33
|
+
console.log('[AmberDirectSigner] Requesting public key, URL:', url.substring(0, 100) + '...');
|
|
34
34
|
window.open(url, '_blank', 'width=400,height=600');
|
|
35
35
|
return new Promise((resolve, reject) => {
|
|
36
36
|
const timer = setTimeout(() => {
|
|
37
37
|
AmberDirectSigner.pendingResolves.delete(id!);
|
|
38
|
-
|
|
38
|
+
const error = new Error('[AmberDirectSigner] getPublicKey timeout after 20s');
|
|
39
|
+
console.error(error.message);
|
|
40
|
+
reject(error);
|
|
39
41
|
}, 20000);
|
|
40
42
|
AmberDirectSigner.pendingResolves.set(id!, { resolve, timer });
|
|
41
43
|
});
|
|
@@ -208,9 +210,11 @@ export class AmberDirectSigner implements Signer {
|
|
|
208
210
|
if (entry) {
|
|
209
211
|
this.pendingResolves.delete(id);
|
|
210
212
|
if (entry.timer) clearTimeout(entry.timer);
|
|
213
|
+
console.log('[AmberDirectSigner] Resolving pending:', { id, type, resultLen: result.length });
|
|
211
214
|
entry.resolve(result);
|
|
212
215
|
return true;
|
|
213
216
|
}
|
|
217
|
+
console.warn('[AmberDirectSigner] No pending resolve found for:', id);
|
|
214
218
|
return false;
|
|
215
219
|
}
|
|
216
220
|
|
|
@@ -76,26 +76,31 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
76
76
|
decrypt: this.decrypt44.bind(this),
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// Amberレスポンス検出: ポップアップから戻った時
|
|
81
80
|
const check = () => {
|
|
81
|
+
console.log('[AuthNostrService] Checking Amber response');
|
|
82
82
|
this.checkAmberResponse();
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
// 主要なイベントで検出(ポーリングは廃止)
|
|
85
86
|
window.addEventListener('focus', check);
|
|
86
87
|
window.addEventListener('visibilitychange', () => {
|
|
87
|
-
if (document.visibilityState === 'visible')
|
|
88
|
+
if (document.visibilityState === 'visible') {
|
|
89
|
+
console.log('[AuthNostrService] Page visible, checking Amber response');
|
|
90
|
+
check();
|
|
91
|
+
}
|
|
88
92
|
});
|
|
89
93
|
window.addEventListener('popstate', check);
|
|
90
94
|
window.addEventListener('hashchange', check);
|
|
91
95
|
|
|
92
|
-
//
|
|
93
|
-
|
|
96
|
+
// 初期チェック
|
|
97
|
+
setTimeout(check, 100);
|
|
94
98
|
|
|
99
|
+
// ポップアップからのメッセージ受信
|
|
95
100
|
window.addEventListener('message', event => {
|
|
96
101
|
if (event.data && event.data.method === 'amberResponse') {
|
|
97
102
|
const { id, type, result } = event.data;
|
|
98
|
-
console.log('Amber response received via message', { id, type, result });
|
|
103
|
+
console.log('[AuthNostrService] Amber response received via message', { id, type, result });
|
|
99
104
|
this.handleAmberResponse({ id, type, result });
|
|
100
105
|
}
|
|
101
106
|
});
|
|
@@ -104,9 +109,11 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
104
109
|
private checkAmberResponse() {
|
|
105
110
|
const response = AmberDirectSigner.parseResponse();
|
|
106
111
|
if (response) {
|
|
112
|
+
console.log('[AuthNostrService] Amber response detected', response);
|
|
113
|
+
|
|
107
114
|
// If we have an opener and it's not the same window, we are in a popup
|
|
108
115
|
if (window.opener && window.opener !== window) {
|
|
109
|
-
console.log('
|
|
116
|
+
console.log('[AuthNostrService] In popup, sending response back to opener');
|
|
110
117
|
window.opener.postMessage({ method: 'amberResponse', ...response }, window.location.origin);
|
|
111
118
|
window.close();
|
|
112
119
|
return;
|
|
@@ -119,10 +126,13 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
119
126
|
private handledAmberIds: Set<string> = new Set();
|
|
120
127
|
|
|
121
128
|
private handleAmberResponse(response: { id: string; type: string; result: string }) {
|
|
122
|
-
if (this.handledAmberIds.has(response.id))
|
|
129
|
+
if (this.handledAmberIds.has(response.id)) {
|
|
130
|
+
console.log('[AuthNostrService] Amber response already handled:', response.id);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
123
133
|
this.handledAmberIds.add(response.id);
|
|
124
134
|
|
|
125
|
-
console.log('Handling Amber response', response);
|
|
135
|
+
console.log('[AuthNostrService] Handling Amber response:', response);
|
|
126
136
|
|
|
127
137
|
// Stop the "Connecting..." spinner
|
|
128
138
|
this.emit('onAuthUrl', { url: '' });
|
|
@@ -130,7 +140,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
130
140
|
// Resolve pending promises if any (for non-reload cases)
|
|
131
141
|
const resolved = AmberDirectSigner.resolvePending(response.id, response.type, response.result);
|
|
132
142
|
if (resolved) {
|
|
133
|
-
console.log('Resolved pending Amber promise
|
|
143
|
+
console.log('[AuthNostrService] Resolved pending Amber promise');
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
if (response.type === 'get_public_key' || response.type.includes('pub')) {
|
|
@@ -141,7 +151,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
141
151
|
relays: [],
|
|
142
152
|
signerPubkey: '',
|
|
143
153
|
};
|
|
144
|
-
console.log('Amber login success', info);
|
|
154
|
+
console.log('[AuthNostrService] Amber login success:', info);
|
|
145
155
|
this.onAuth('login', info);
|
|
146
156
|
}
|
|
147
157
|
|
|
@@ -162,7 +172,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
162
172
|
}
|
|
163
173
|
|
|
164
174
|
if (changed) {
|
|
165
|
-
console.log('Cleaning up Amber response URL'
|
|
175
|
+
console.log('[AuthNostrService] Cleaning up Amber response URL');
|
|
166
176
|
window.history.replaceState({}, '', url.toString());
|
|
167
177
|
}
|
|
168
178
|
}
|
|
@@ -419,8 +429,16 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
419
429
|
}
|
|
420
430
|
|
|
421
431
|
public async setAmber(info: Info) {
|
|
432
|
+
console.log('setAmber', info);
|
|
433
|
+
|
|
434
|
+
// クリア
|
|
422
435
|
this.releaseSigner();
|
|
423
436
|
this.amberSigner = new AmberDirectSigner(info.pubkey);
|
|
437
|
+
|
|
438
|
+
// Amberは署名リクエストをウォレットに投げるだけなので、特別な初期化は不要
|
|
439
|
+
// signEventなどが直接amberSignerを使用する
|
|
440
|
+
|
|
441
|
+
// 認証完了
|
|
424
442
|
this.onAuth('login', info);
|
|
425
443
|
}
|
|
426
444
|
|
|
@@ -656,7 +674,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
656
674
|
}
|
|
657
675
|
|
|
658
676
|
private async listen(info: Info) {
|
|
659
|
-
if (!info.iframeUrl) return this.signer!.listen(this.nostrConnectSecret);
|
|
677
|
+
if (!info.iframeUrl) return this.signer!.listen(this.nostrConnectSecret, 60000);
|
|
660
678
|
const r = await this.starterReady!.wait();
|
|
661
679
|
if (r[0] === 'starterError') throw new Error(r[1]);
|
|
662
680
|
return this.signer!.setListenReply(r[1], this.nostrConnectSecret);
|
|
@@ -717,6 +735,11 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
717
735
|
remotePubkey: info.signerPubkey!,
|
|
718
736
|
relays: info.relays || [],
|
|
719
737
|
timeoutMs: 30000,
|
|
738
|
+
useNip44: true,
|
|
739
|
+
retryConfig: {
|
|
740
|
+
maxRetries: 3,
|
|
741
|
+
retryDelayMs: 1000,
|
|
742
|
+
},
|
|
720
743
|
});
|
|
721
744
|
|
|
722
745
|
const adapter = new Nip46Adapter(client, localSigner);
|
|
@@ -773,24 +796,44 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
773
796
|
if (domain) info.domain = domain;
|
|
774
797
|
if (iframeUrl) info.iframeUrl = iframeUrl;
|
|
775
798
|
|
|
776
|
-
|
|
799
|
+
console.log('authNip46', type, info);
|
|
800
|
+
|
|
777
801
|
if (!info.signerPubkey || !info.sk || !info.relays || info.relays.length === 0) {
|
|
778
|
-
throw new Error(`
|
|
802
|
+
throw new Error(`Invalid bunker URL format`);
|
|
779
803
|
}
|
|
780
804
|
|
|
781
805
|
const eventToAddAccount = Boolean(this.params.userInfo);
|
|
782
|
-
|
|
806
|
+
|
|
807
|
+
// 接続モードに応じた処理
|
|
808
|
+
const connectMode = type === 'login' && !info.token ? 'listen' : 'connect';
|
|
809
|
+
console.log('authNip46 connection mode:', connectMode);
|
|
783
810
|
|
|
784
811
|
// updates the info
|
|
785
|
-
await this.initSigner(info, {
|
|
812
|
+
await this.initSigner(info, {
|
|
813
|
+
listen: connectMode === 'listen',
|
|
814
|
+
connect: connectMode === 'connect',
|
|
815
|
+
eventToAddAccount,
|
|
816
|
+
});
|
|
786
817
|
|
|
787
818
|
// callback
|
|
788
819
|
this.onAuth(type, info);
|
|
789
|
-
} catch (
|
|
790
|
-
console.
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
820
|
+
} catch (error: any) {
|
|
821
|
+
console.error('nostr-login auth failed:', {
|
|
822
|
+
type,
|
|
823
|
+
error: error.message,
|
|
824
|
+
stack: error.stack,
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
// エラーの種類に応じた処理
|
|
828
|
+
if (error.message.includes('timeout') || error.message.includes('timed out')) {
|
|
829
|
+
throw new Error('接続がタイムアウトしました。ネットワークを確認してください。');
|
|
830
|
+
} else if (error.message.includes('Invalid bunker URL')) {
|
|
831
|
+
throw new Error('無効なbunker URL形式です。');
|
|
832
|
+
} else if (error.message.includes('publish')) {
|
|
833
|
+
throw new Error('リレーへの接続に失敗しました。再度お試しください。');
|
|
834
|
+
} else {
|
|
835
|
+
throw new Error(`認証に失敗しました: ${error.message}`);
|
|
836
|
+
}
|
|
794
837
|
}
|
|
795
838
|
}
|
|
796
839
|
|
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
id: string;
|
|
3
|
+
method: string;
|
|
4
|
+
params: string[];
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export interface Nip46Response {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
id: string;
|
|
9
|
+
result?: string;
|
|
10
|
+
error?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface PendingRequest {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
}
|