@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/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/AuthNostrService.ts +35 -10
- 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
|
@@ -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
|
-
|
|
781
|
+
console.log('authNip46', type, info);
|
|
782
|
+
|
|
777
783
|
if (!info.signerPubkey || !info.sk || !info.relays || info.relays.length === 0) {
|
|
778
|
-
throw new Error(`
|
|
784
|
+
throw new Error(`Invalid bunker URL format`);
|
|
779
785
|
}
|
|
780
786
|
|
|
781
787
|
const eventToAddAccount = Boolean(this.params.userInfo);
|
|
782
|
-
|
|
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, {
|
|
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 (
|
|
790
|
-
console.
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
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
|
}
|