@konemono/nostr-login 1.7.70 → 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.
@@ -1,13 +1,13 @@
1
- import { NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
2
1
  import { Nip44 } from '../utils/nip44';
3
- import { getPublicKey } from 'nostr-tools';
2
+ import { getPublicKey, getEventHash, getSignature, nip04 } from 'nostr-tools';
4
3
 
5
- export class PrivateKeySigner extends NDKPrivateKeySigner {
4
+ export class PrivateKeySigner {
6
5
  private nip44: Nip44 = new Nip44();
7
6
  private _pubkey: string;
7
+ public privateKey: string;
8
8
 
9
9
  constructor(privateKey: string) {
10
- super(privateKey);
10
+ this.privateKey = privateKey;
11
11
  this._pubkey = getPublicKey(privateKey);
12
12
  }
13
13
 
@@ -15,11 +15,42 @@ export class PrivateKeySigner extends NDKPrivateKeySigner {
15
15
  return this._pubkey;
16
16
  }
17
17
 
18
- encryptNip44(recipient: NDKUser, value: string): Promise<string> {
19
- return Promise.resolve(this.nip44.encrypt(this.privateKey!, recipient.pubkey, value));
18
+ async blockUntilReady() {
19
+ return Promise.resolve();
20
20
  }
21
21
 
22
- decryptNip44(sender: NDKUser, value: string): Promise<string> {
23
- return Promise.resolve(this.nip44.decrypt(this.privateKey!, sender.pubkey, value));
22
+ async user() {
23
+ return { pubkey: this.pubkey };
24
+ }
25
+
26
+ async sign(event: any): Promise<string> {
27
+ // ensure event has created_at
28
+ if (!event.created_at) event.created_at = Math.floor(Date.now() / 1000);
29
+ // compute id and signature
30
+ const id = getEventHash(event as any);
31
+ event.id = id;
32
+ const sig = getSignature(event as any, this.privateKey);
33
+ event.sig = sig;
34
+ return sig;
35
+ }
36
+
37
+ async encrypt(recipient: any, plaintext: string): Promise<string> {
38
+ const pubkey = typeof recipient === 'string' ? recipient : recipient.pubkey;
39
+ return nip04.encrypt(this.privateKey, pubkey, plaintext);
40
+ }
41
+
42
+ async decrypt(sender: any, ciphertext: string): Promise<string> {
43
+ const pubkey = typeof sender === 'string' ? sender : sender.pubkey;
44
+ return nip04.decrypt(this.privateKey, pubkey, ciphertext);
45
+ }
46
+
47
+ encryptNip44(recipient: any, value: string): Promise<string> {
48
+ const pubkey = typeof recipient === 'string' ? recipient : recipient.pubkey;
49
+ return Promise.resolve(this.nip44.encrypt(this.privateKey, pubkey, value));
50
+ }
51
+
52
+ decryptNip44(sender: any, value: string): Promise<string> {
53
+ const pubkey = typeof sender === 'string' ? sender : sender.pubkey;
54
+ return Promise.resolve(this.nip44.decrypt(this.privateKey, pubkey, value));
24
55
  }
25
56
  }
@@ -0,0 +1,180 @@
1
+ import { EventEmitter } from 'tseep';
2
+ import { Nip46Client } from './Nip46Client';
3
+ import { PrivateKeySigner } from '../Signer';
4
+
5
+ export class Nip46Adapter extends EventEmitter {
6
+ private client: Nip46Client;
7
+ private localSigner: PrivateKeySigner;
8
+ public userPubkey: string = '';
9
+ public remotePubkey: string;
10
+
11
+ constructor(client: Nip46Client, localSigner: PrivateKeySigner) {
12
+ super();
13
+ this.client = client;
14
+ this.localSigner = localSigner;
15
+ this.remotePubkey = (client as any).remotePubkey || '';
16
+
17
+ // forward events
18
+ this.client.on('authUrl', (url: string) => {
19
+ this.emit('authUrl', url);
20
+ });
21
+ this.client.on('response', ({ response, pubkey }: any) => {
22
+ this.emit('response', response, pubkey);
23
+ });
24
+ }
25
+
26
+ async initUserPubkey(hintPubkey?: string) {
27
+ if (this.userPubkey) throw new Error('Already called initUserPubkey');
28
+ if (hintPubkey) {
29
+ this.userPubkey = hintPubkey;
30
+ console.log('[Nip46Adapter] User pubkey set from hint:', hintPubkey);
31
+ return;
32
+ }
33
+
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
+ }
44
+ }
45
+
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
+
66
+ const onResponse = ({ response, pubkey }: any) => {
67
+ if (!response) return;
68
+
69
+ // auth_urlは無視(別のハンドラが処理)
70
+ if (response.result === 'auth_url') return;
71
+
72
+ // 成功: ack または secret が返される
73
+ if (response.result === 'ack' || response.result === nostrConnectSecret) {
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));
81
+ }
82
+ };
83
+
84
+ this.client.on('response', onResponse);
85
+ });
86
+ }
87
+
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
+ }
107
+ }
108
+
109
+ async setListenReply(reply: any, nostrConnectSecret: string) {
110
+ // reply is expected to be a raw event object; we'll try to parse its content
111
+ // Attempt to decrypt via the client flow by treating it as a response
112
+ try {
113
+ const decoded = reply && reply.content ? JSON.parse(reply.content) : null;
114
+ if (!decoded) throw new Error('Bad reply');
115
+ if (decoded.result === nostrConnectSecret) {
116
+ this.userPubkey = reply.pubkey;
117
+ } else {
118
+ throw new Error('Bad reply');
119
+ }
120
+ } catch (e) {
121
+ throw new Error('Failed to set listen reply');
122
+ }
123
+ }
124
+
125
+ async createAccount2({ bunkerPubkey, name, domain, perms = '' }: { bunkerPubkey: string; name: string; domain: string; perms?: string }) {
126
+ const params = [name, domain, '', perms];
127
+
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
+ }
139
+ }
140
+
141
+ async encrypt(recipientPubkey: string, plaintext: string) {
142
+ const r = await this.client.sendRequest('nip04_encrypt', [recipientPubkey, plaintext]);
143
+ return r;
144
+ }
145
+
146
+ async decrypt(recipientPubkey: string, ciphertext: string) {
147
+ const r = await this.client.sendRequest('nip04_decrypt', [recipientPubkey, ciphertext]);
148
+ return r;
149
+ }
150
+
151
+ async sign(event: any) {
152
+ try {
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;
164
+ }
165
+ }
166
+
167
+ // provide rpc compatibility
168
+ get rpc() {
169
+ return {
170
+ sendRequest: async (remotePubkey: string, method: string, params: string[], kind: number, cb: (res: any) => void) => {
171
+ try {
172
+ const res = await this.client.sendRequest(method, params);
173
+ cb({ result: res });
174
+ } catch (err: any) {
175
+ cb({ error: err.message });
176
+ }
177
+ },
178
+ };
179
+ }
180
+ }
@@ -0,0 +1,356 @@
1
+ import { SimplePool, Event as NostrEvent, getPublicKey, nip04 } from 'nostr-tools';
2
+ import { Nip44 } from '../../utils/nip44';
3
+ import { EventEmitter } from 'tseep';
4
+ import { Nip46Request, Nip46Response, PendingRequest, Nip46ClientOptions } from './types';
5
+ import { getEventHash, getSignature } from 'nostr-tools';
6
+
7
+ export class Nip46Client extends EventEmitter {
8
+ private pool: SimplePool;
9
+ private localPrivateKey: string;
10
+ private remotePubkey: string;
11
+ private relays: string[];
12
+ private pendingRequests: Map<string, PendingRequest> = new Map();
13
+ private defaultTimeoutMs: number;
14
+ private useNip44: boolean;
15
+ private subscription: any = null;
16
+ private isSubscribed: boolean = false;
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;
21
+
22
+ constructor(options: Nip46ClientOptions) {
23
+ super();
24
+ this.pool = new SimplePool();
25
+ this.localPrivateKey = options.localPrivateKey;
26
+ this.remotePubkey = options.remotePubkey;
27
+ this.relays = options.relays;
28
+ this.defaultTimeoutMs = options.timeoutMs || 30000;
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
+ }
37
+ }
38
+
39
+ get localPubkey(): string {
40
+ return getPublicKey(this.localPrivateKey);
41
+ }
42
+
43
+ /**
44
+ * NIP-46リクエストを送信(リトライ機能付き)
45
+ */
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> {
78
+ const timeout = timeoutMs || this.defaultTimeoutMs;
79
+ const id = this.generateId();
80
+ const request: Nip46Request = { id, method, params };
81
+
82
+ console.log('[Nip46Client] Sending request:', { id, method, params });
83
+
84
+ // レスポンス購読を開始(まだの場合)
85
+ if (!this.isSubscribed) {
86
+ this.subscribeToResponses();
87
+ }
88
+
89
+ // リクエストイベントを作成・送信
90
+ await this.publishRequest(request);
91
+
92
+ // レスポンスを待つPromise
93
+ return new Promise<string>((resolve, reject) => {
94
+ const timer = setTimeout(() => {
95
+ this.pendingRequests.delete(id);
96
+ const error = new Error(`Request ${id} (${method}) timed out after ${timeout}ms`);
97
+ console.error('[Nip46Client]', error.message);
98
+ reject(error);
99
+ }, timeout);
100
+
101
+ this.pendingRequests.set(id, {
102
+ resolve,
103
+ reject,
104
+ timer,
105
+ method,
106
+ });
107
+ });
108
+ }
109
+
110
+ /**
111
+ * リクエストイベントを作成して送信
112
+ */
113
+ private async publishRequest(request: Nip46Request): Promise<void> {
114
+ const content = JSON.stringify(request);
115
+
116
+ // 暗号化
117
+ const encrypted = this.useNip44
118
+ ? await this.nip44Codec.encrypt(this.localPrivateKey, this.remotePubkey, content)
119
+ : await nip04.encrypt(this.localPrivateKey, this.remotePubkey, content);
120
+
121
+ // イベント作成
122
+ const event: NostrEvent = {
123
+ kind: 24133,
124
+ pubkey: this.localPubkey,
125
+ created_at: Math.floor(Date.now() / 1000),
126
+ tags: [['p', this.remotePubkey]],
127
+ content: encrypted,
128
+ id: '',
129
+ sig: '',
130
+ };
131
+
132
+ // ID計算
133
+ event.id = getEventHash(event);
134
+
135
+ // 署名
136
+ event.sig = getSignature(event, this.localPrivateKey);
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
+
149
+ // リレーに送信
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
+ }
157
+ }
158
+
159
+ /**
160
+ * レスポンスイベントを購読
161
+ */
162
+ private subscribeToResponses(): void {
163
+ if (this.isSubscribed) return;
164
+
165
+ const filter = {
166
+ 'kinds': [24133],
167
+ '#p': [this.localPubkey],
168
+ 'since': Math.floor(Date.now() / 1000) - 60,
169
+ };
170
+
171
+ console.log('[Nip46Client] Subscribing to responses');
172
+
173
+ // SimplePool subscription
174
+ this.subscription = this.pool.sub(this.relays, [filter]);
175
+ this.subscription.on('event', async (event: NostrEvent) => {
176
+ await this.handleResponseEvent(event);
177
+ });
178
+ this.subscription.on('eose', () => {
179
+ console.log('[Nip46Client] EOSE received');
180
+ });
181
+
182
+ this.isSubscribed = true;
183
+ }
184
+
185
+ /**
186
+ * レスポンスイベントを処理
187
+ */
188
+ private async handleResponseEvent(event: NostrEvent): Promise<void> {
189
+ try {
190
+ // 復号化
191
+ const decrypted = this.isNip04(event.content)
192
+ ? await nip04.decrypt(this.localPrivateKey, event.pubkey, event.content)
193
+ : await this.nip44Codec.decrypt(this.localPrivateKey, event.pubkey, event.content);
194
+
195
+ const response: Nip46Response = JSON.parse(decrypted);
196
+
197
+ console.log('[Nip46Client] Response received:', {
198
+ id: response.id,
199
+ hasResult: !!response.result,
200
+ hasError: !!response.error,
201
+ });
202
+
203
+ // Emit response event for consumers (include sender pubkey)
204
+ this.emit('response', { response, pubkey: event.pubkey });
205
+
206
+ // auth_urlの特別処理: OAuth完了後に実際のレスポンスが来るまで待つ
207
+ if (response.result === 'auth_url') {
208
+ console.log('[Nip46Client] Auth URL received:', response.error);
209
+ this.emit('authUrl', response.error);
210
+ // 注意: pendingRequestsは削除しない
211
+ // OAuth完了後に同じIDで実際のレスポンスが返ってくる
212
+ return;
213
+ }
214
+
215
+ // 保留中のリクエストを解決
216
+ const pending = this.pendingRequests.get(response.id);
217
+ if (pending) {
218
+ clearTimeout(pending.timer);
219
+ this.pendingRequests.delete(response.id);
220
+
221
+ if (response.error) {
222
+ console.error('[Nip46Client] Request failed:', {
223
+ id: response.id,
224
+ method: pending.method,
225
+ error: response.error,
226
+ });
227
+ pending.reject(new Error(response.error));
228
+ } else if (response.result !== undefined) {
229
+ console.log('[Nip46Client] Request succeeded:', {
230
+ id: response.id,
231
+ method: pending.method,
232
+ });
233
+ pending.resolve(response.result);
234
+ } else {
235
+ pending.reject(new Error('Invalid response: no result or error'));
236
+ }
237
+ } else {
238
+ console.warn('[Nip46Client] Received response for unknown request:', response.id);
239
+ }
240
+ } catch (error) {
241
+ console.error('[Nip46Client] Failed to parse response event:', error);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * NIP-04かNIP-44かを判定
247
+ */
248
+ private isNip04(ciphertext: string): boolean {
249
+ const l = ciphertext.length;
250
+ if (l < 28) return false;
251
+ return ciphertext[l - 28] === '?' && ciphertext[l - 27] === 'i' && ciphertext[l - 26] === 'v' && ciphertext[l - 25] === '=';
252
+ }
253
+
254
+ /**
255
+ * ランダムIDを生成
256
+ */
257
+ private generateId(): string {
258
+ return Math.random().toString(36).substring(2, 15);
259
+ }
260
+
261
+ /**
262
+ * NIP-44を使用するかどうかを設定
263
+ */
264
+ setUseNip44(useNip44: boolean): void {
265
+ this.useNip44 = useNip44;
266
+ }
267
+
268
+ /**
269
+ * クリーンアップ
270
+ */
271
+ cleanup(): void {
272
+ console.log('[Nip46Client] Cleaning up');
273
+
274
+ // すべての保留中リクエストをキャンセル
275
+ for (const [id, pending] of this.pendingRequests) {
276
+ clearTimeout(pending.timer);
277
+ pending.reject(new Error('Client cleanup'));
278
+ }
279
+ this.pendingRequests.clear();
280
+
281
+ // 購読を停止
282
+ if (this.subscription) {
283
+ // SimplePool subscriptions may use `unsub` or `close`
284
+ try {
285
+ if (typeof this.subscription.unsub === 'function') this.subscription.unsub();
286
+ if (typeof this.subscription.close === 'function') this.subscription.close();
287
+ } catch (e) {
288
+ // ignore
289
+ }
290
+ this.subscription = null;
291
+ this.isSubscribed = false;
292
+ }
293
+
294
+ // iframeのkeepaliveをクリア
295
+ if (this.iframeKeepaliveInterval) {
296
+ clearInterval(this.iframeKeepaliveInterval);
297
+ this.iframeKeepaliveInterval = undefined;
298
+ }
299
+
300
+ // リレー接続を閉じる
301
+ this.pool.close(this.relays);
302
+
303
+ // イベントリスナーをクリア
304
+ this.removeAllListeners();
305
+ }
306
+
307
+ /**
308
+ * 接続状態を確認
309
+ */
310
+ isConnected(): boolean {
311
+ return this.isSubscribed;
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
+ }
356
+ }
@@ -0,0 +1,34 @@
1
+ export interface Nip46Request {
2
+ id: string;
3
+ method: string;
4
+ params: string[];
5
+ }
6
+
7
+ export interface Nip46Response {
8
+ id: string;
9
+ result?: string;
10
+ error?: string;
11
+ }
12
+
13
+ export interface PendingRequest {
14
+ resolve: (result: string) => void;
15
+ reject: (error: Error) => void;
16
+ timer: NodeJS.Timeout;
17
+ method: string;
18
+ }
19
+
20
+ export interface Nip46ClientOptions {
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
+ };
34
+ }