@konemono/nostr-login 1.9.14 → 1.10.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,257 +1,425 @@
1
- import { EventEmitter } from 'tseep';
2
- import { validateEvent, verifySignature, nip04, getEventHash, getSignature } from 'nostr-tools';
3
- import { SimplePool } from 'nostr-tools';
1
+ import NDK, { NDKEvent, NDKFilter, NDKNip46Signer, NDKNostrRpc, NDKRpcRequest, NDKRpcResponse, NDKSubscription, NDKSubscriptionCacheUsage, NostrEvent } from '@nostr-dev-kit/ndk';
2
+ import { validateEvent, verifySignature } from 'nostr-tools';
4
3
  import { PrivateKeySigner } from './Signer';
4
+ import { NIP46_REQUEST_TIMEOUT, NIP46_CONNECT_TIMEOUT } from '../const';
5
5
 
6
- // Lightweight Nostr RPC for NIP-46 that does not rely on NDK
7
-
8
- export type RpcRequest = { id: string; pubkey: string; method: string; params: any[]; event?: any };
9
- export type RpcResponse = { id: string; result?: any; error?: any; event?: any };
10
-
11
- export class NostrRpc extends EventEmitter {
12
- protected localSigner: PrivateKeySigner;
13
- protected localPubkey: string;
14
- protected localPrivateKey: string;
15
- protected remotePubkey: string = '';
16
- protected pool: SimplePool;
17
- protected relays: string[] = [];
18
- protected subscription: any;
19
- protected isSubscribed: boolean = false;
20
- protected useNip44: boolean = false;
6
+ // タイムアウト付きPromiseラッパー
7
+ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage: string): Promise<T> {
8
+ return Promise.race([promise, new Promise<T>((_, reject) => setTimeout(() => reject(new Error(errorMessage)), timeoutMs))]);
9
+ }
21
10
 
11
+ class NostrRpc extends NDKNostrRpc {
12
+ protected _ndk: NDK;
13
+ protected _signer: PrivateKeySigner;
22
14
  protected requests: Set<string> = new Set();
15
+ private sub?: NDKSubscription;
16
+ protected _useNip44: boolean = false;
17
+ private reconnectAttempts: number = 0;
18
+ private maxReconnectAttempts: number = 3;
19
+ private reconnectDelay: number = 2000;
20
+
21
+ public constructor(ndk: NDK, signer: PrivateKeySigner) {
22
+ super(ndk, signer, ndk.debug.extend('nip46:signer:rpc'));
23
+ this._ndk = ndk;
24
+ this._signer = signer;
25
+ this.setupConnectionMonitoring();
26
+ }
23
27
 
24
- constructor(localSigner: PrivateKeySigner, relays: string[] = []) {
25
- super();
26
- this.localSigner = localSigner;
27
- this.localPubkey = localSigner.pubkey;
28
- this.localPrivateKey = (localSigner as any).privateKey;
29
- this.pool = new SimplePool();
30
- this.relays = relays && relays.length ? relays : [];
28
+ public async subscribe(filter: NDKFilter): Promise<NDKSubscription> {
29
+ // NOTE: fixing ndk
30
+ filter.kinds = filter.kinds?.filter(k => k === 24133);
31
+ this.sub = await super.subscribe(filter);
32
+ return this.sub;
33
+ }
34
+
35
+ public stop() {
36
+ if (this.sub) {
37
+ this.sub.stop();
38
+ this.sub = undefined;
39
+ }
31
40
  }
32
41
 
33
- public setUseNip44(use: boolean) {
34
- this.useNip44 = use;
42
+ public setUseNip44(useNip44: boolean) {
43
+ this._useNip44 = useNip44;
35
44
  }
36
45
 
37
- protected isNip04(ciphertext: string) {
46
+ private isNip04(ciphertext: string) {
38
47
  const l = ciphertext.length;
39
48
  if (l < 28) return false;
40
49
  return ciphertext[l - 28] === '?' && ciphertext[l - 27] === 'i' && ciphertext[l - 26] === 'v' && ciphertext[l - 25] === '=';
41
50
  }
42
51
 
43
- protected async decryptEventContent(event: any) {
44
- const decrypt = this.isNip04(event.content)
45
- ? this.localSigner.decrypt.bind(this.localSigner)
46
- : this.localSigner.decryptNip44?.bind(this.localSigner) ?? this.localSigner.decrypt.bind(this.localSigner);
47
- try {
48
- const decrypted = await decrypt(event.pubkey || event.pubkey, event.content);
49
- return JSON.parse(decrypted);
50
- } catch (e) {
51
- throw e;
52
+ // override to auto-decrypt nip04/nip44
53
+ public async parseEvent(event: NDKEvent): Promise<NDKRpcRequest | NDKRpcResponse> {
54
+ const remoteUser = this._ndk.getUser({ pubkey: event.pubkey });
55
+ remoteUser.ndk = this._ndk;
56
+ const decrypt = this.isNip04(event.content) ? this._signer.decrypt : this._signer.decryptNip44;
57
+ const decryptedContent = await decrypt.call(this._signer, remoteUser, event.content);
58
+ const parsedContent = JSON.parse(decryptedContent);
59
+ const { id, method, params, result, error } = parsedContent;
60
+
61
+ if (method) {
62
+ return { id, pubkey: event.pubkey, method, params, event };
63
+ } else {
64
+ return { id, result, error, event };
52
65
  }
53
66
  }
54
67
 
55
- protected async parseEvent(event: any): Promise<RpcRequest | RpcResponse> {
56
- const parsed = await this.decryptEventContent(event);
57
- const { id, method, params, result, error } = parsed;
58
- if (method) {
59
- return { id, pubkey: event.pubkey, method, params, event } as RpcRequest;
68
+ public async parseNostrConnectReply(reply: any, secret: string) {
69
+ const event = new NDKEvent(this._ndk, reply);
70
+ const parsedEvent = await this.parseEvent(event);
71
+ console.log('nostr connect parsedEvent', parsedEvent);
72
+ if (!(parsedEvent as NDKRpcRequest).method) {
73
+ const response = parsedEvent as NDKRpcResponse;
74
+ if (response.result !== secret) throw new Error(response.error);
75
+ return event.pubkey;
60
76
  } else {
61
- return { id, result, error, event } as RpcResponse;
77
+ throw new Error('Bad nostr connect reply');
62
78
  }
63
79
  }
64
80
 
65
- public subscribe(relays: string[], filter: any) {
66
- if (this.isSubscribed) return;
67
- this.relays = relays && relays.length ? relays : this.relays;
68
- const since = Math.floor(Date.now() / 1000) - 60;
69
- const filters = [{ ...filter, since }];
70
- this.subscription = this.pool.sub(this.relays, filters);
71
- this.subscription.on('event', async (ev: any) => {
72
- try {
73
- const parsed = await this.parseEvent(ev);
74
- if (!(parsed as RpcRequest).method) {
75
- this.emit(`response-${(parsed as RpcResponse).id}`, parsed as RpcResponse);
76
- } else {
77
- this.emit('request', parsed as RpcRequest);
78
- }
79
- } catch (e) {
80
- // ignore parse errors
81
- }
81
+ // ndk doesn't support nostrconnect:
82
+ // we just listed to an unsolicited reply to
83
+ // our pubkey and if it's ack/secret - we're fine
84
+ public async listen(nostrConnectSecret: string): Promise<string> {
85
+ const pubkey = this._signer.pubkey;
86
+ console.log('nostr-login listening for conn to', pubkey);
87
+ const sub = await this.subscribe({
88
+ 'kinds': [24133],
89
+ '#p': [pubkey],
82
90
  });
83
- this.subscription.on('eose', () => {
84
- /* noop */
91
+ return new Promise<string>((ok, err) => {
92
+ sub.on('event', async (event: NDKEvent) => {
93
+ try {
94
+ const parsedEvent = await this.parseEvent(event);
95
+ // console.log('ack parsedEvent', parsedEvent);
96
+ if (!(parsedEvent as NDKRpcRequest).method) {
97
+ const response = parsedEvent as NDKRpcResponse;
98
+
99
+ // ignore
100
+ if (response.result === 'auth_url') return;
101
+
102
+ // FIXME for now accept 'ack' replies, later on only
103
+ // accept secrets
104
+ if (response.result === 'ack' || response.result === nostrConnectSecret) {
105
+ ok(event.pubkey);
106
+ } else {
107
+ err(response.error);
108
+ }
109
+ }
110
+ } catch (e) {
111
+ console.log('error parsing event', e, event.rawEvent());
112
+ }
113
+ // done
114
+ this.stop();
115
+ });
85
116
  });
86
- this.isSubscribed = true;
87
117
  }
88
118
 
89
- public stop() {
90
- if (this.subscription && this.subscription.unsub) {
91
- try {
92
- this.subscription.unsub();
93
- } catch (e) {}
94
- }
95
- this.isSubscribed = false;
119
+ // since ndk doesn't yet support perms param
120
+ // we reimplement the 'connect' call here
121
+ // instead of await signer.blockUntilReady();
122
+ public async connect(pubkey: string, token?: string, perms?: string) {
123
+ return new Promise<void>((ok, err) => {
124
+ const connectParams = [pubkey!, token || '', perms || ''];
125
+ this.sendRequest(pubkey!, 'connect', connectParams, 24133, (response: NDKRpcResponse) => {
126
+ if (response.result === 'ack') {
127
+ ok();
128
+ } else {
129
+ err(response.error);
130
+ }
131
+ });
132
+ });
96
133
  }
97
134
 
98
- protected getId() {
99
- return Math.random().toString(36).substring(7);
135
+ // タイムアウト対応のconnect
136
+ public async connectWithTimeout(pubkey: string, token?: string, perms?: string, timeoutMs: number = NIP46_CONNECT_TIMEOUT): Promise<void> {
137
+ return withTimeout(this.connect(pubkey, token, perms), timeoutMs, `Connection timeout after ${timeoutMs}ms`);
100
138
  }
101
139
 
102
- protected setResponseHandler(id: string, cb?: (res: RpcResponse) => void) {
103
- let authUrlSent = false;
104
- const now = Date.now();
140
+ // タイムアウト対応のsendRequest
141
+ public async sendRequestWithTimeout(
142
+ remotePubkey: string,
143
+ method: string,
144
+ params: string[] = [],
145
+ kind = 24133,
146
+ timeoutMs: number = NIP46_REQUEST_TIMEOUT,
147
+ ): Promise<NDKRpcResponse> {
148
+ return withTimeout(
149
+ new Promise<NDKRpcResponse>((resolve, reject) => {
150
+ this.sendRequest(remotePubkey, method, params, kind, response => {
151
+ if (response.error) {
152
+ reject(new Error(response.error));
153
+ } else {
154
+ resolve(response);
155
+ }
156
+ });
157
+ }),
158
+ timeoutMs,
159
+ `Request timeout after ${timeoutMs}ms for method: ${method}`,
160
+ );
161
+ }
105
162
 
106
- const responseHandler = (response: RpcResponse) => {
107
- if (response.result === 'auth_url') {
108
- // reattach for auth_url so we can get final response later
109
- this.once(`response-${id}`, responseHandler);
110
- if (!authUrlSent) {
111
- authUrlSent = true;
112
- this.emit('authUrl', response.error);
113
- }
114
- } else if (cb) {
115
- if (this.requests.has(id)) {
116
- this.requests.delete(id);
117
- cb(response);
163
+ // 接続監視のセットアップ
164
+ private setupConnectionMonitoring() {
165
+ // アプリがフォアグラウンドに戻ったときの処理
166
+ if (typeof document !== 'undefined') {
167
+ document.addEventListener('visibilitychange', async () => {
168
+ if (document.visibilityState === 'visible') {
169
+ console.log('App visible, checking relay connections...');
170
+ await this.ensureConnected();
118
171
  }
119
- }
120
- };
172
+ });
173
+ }
121
174
 
122
- this.once(`response-${id}`, responseHandler);
175
+ // オンライン/オフライン検知
176
+ if (typeof window !== 'undefined') {
177
+ window.addEventListener('online', async () => {
178
+ console.log('Network online, reconnecting relays...');
179
+ await this.reconnect();
180
+ });
181
+ }
123
182
  }
124
183
 
125
- protected async createRequestEvent(id: string, remotePubkey: string, method: string, params: any[] = [], kind = 24133) {
126
- this.requests.add(id);
127
- const request = { id, method, params };
128
- // encrypt
129
- const content =
130
- this.useNip44 && method !== 'create_account' && this.localSigner.encryptNip44
131
- ? await this.localSigner.encryptNip44(remotePubkey, JSON.stringify(request))
132
- : await this.localSigner.encrypt(remotePubkey, JSON.stringify(request));
184
+ // 接続を確認して必要なら再接続
185
+ private async ensureConnected(): Promise<void> {
186
+ const connectedRelays = Array.from(this._ndk.pool.relays.values()).filter(r => r.status === 1); // 1 = CONNECTED
133
187
 
134
- const event: any = {
135
- kind,
136
- content,
137
- tags: [['p', remotePubkey]],
138
- pubkey: this.localPubkey,
139
- created_at: Math.floor(Date.now() / 1000),
140
- };
188
+ if (connectedRelays.length === 0) {
189
+ console.log('No connected relays, attempting reconnection...');
190
+ await this.reconnect();
191
+ }
192
+ }
141
193
 
142
- // sign using signer
143
- await this.localSigner.sign(event as any);
194
+ // 再接続処理
195
+ protected async reconnect(): Promise<void> {
196
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
197
+ console.error('Max reconnection attempts reached');
198
+ this.emit('reconnectFailed');
199
+ return;
200
+ }
144
201
 
145
- return event;
146
- }
202
+ this.reconnectAttempts++;
203
+ console.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
147
204
 
148
- protected async publishRequest(event: any) {
149
205
  try {
150
- await Promise.any(this.pool.publish(this.relays, event));
206
+ // サブスクリプションを停止
207
+ if (this.sub) {
208
+ this.sub.stop();
209
+ }
210
+
211
+ // 少し待ってから再接続
212
+ await new Promise(resolve => setTimeout(resolve, this.reconnectDelay));
213
+
214
+ // 再接続
215
+ await this._ndk.connect();
216
+
217
+ // サブスクリプションを再開
218
+ if (this.sub) {
219
+ await this.sub.start();
220
+ }
221
+
222
+ this.reconnectAttempts = 0;
223
+ this.emit('reconnected');
224
+ console.log('Successfully reconnected to relays');
151
225
  } catch (e) {
152
- // swallow publish errors
226
+ console.error('Reconnection failed:', e);
227
+
228
+ // リトライ
229
+ setTimeout(() => this.reconnect(), this.reconnectDelay * this.reconnectAttempts);
153
230
  }
154
231
  }
155
232
 
156
- public async sendRequest(remotePubkey: string, method: string, params: any[] = [], kind = 24133, cb?: (res: RpcResponse) => void): Promise<RpcResponse | undefined> {
233
+ protected getId(): string {
234
+ return Math.random().toString(36).substring(7);
235
+ }
236
+
237
+ public async sendRequest(remotePubkey: string, method: string, params: string[] = [], kind = 24133, cb?: (res: NDKRpcResponse) => void): Promise<NDKRpcResponse> {
157
238
  const id = this.getId();
239
+
240
+ // response handler will deduplicate auth urls and responses
158
241
  this.setResponseHandler(id, cb);
242
+
243
+ // create and sign request
159
244
  const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
160
- await this.publishRequest(event);
161
- return undefined as any;
245
+ console.log('sendRequest', { event, method, remotePubkey, params });
246
+
247
+ // send to relays
248
+ await event.publish();
249
+
250
+ // NOTE: ndk returns a promise that never resolves and
251
+ // in fact REQUIRES cb to be provided (otherwise no way
252
+ // to consume the result), we've already stepped on the bug
253
+ // of waiting for this unresolvable result, so now we return
254
+ // undefined to make sure waiters fail, not hang.
255
+ // @ts-ignore
256
+ return undefined as NDKRpcResponse;
162
257
  }
163
258
 
164
- public async listen(nostrConnectSecret: string, relays?: string[]) {
165
- const pubkey = this.localPubkey;
166
- this.subscribe(relays || this.relays, { 'kinds': [24133], '#p': [pubkey] });
167
- return new Promise<string>((ok, err) => {
168
- const handler = async (event: any) => {
169
- try {
170
- const parsed = await this.parseEvent(event);
171
- if ((parsed as RpcResponse).result === 'auth_url') return; // ignore
172
- const response = parsed as RpcResponse;
173
- if (response.result === 'ack' || response.result === nostrConnectSecret) {
174
- ok(event.pubkey);
175
- this.stop();
176
- } else {
177
- err(response.error);
178
- this.stop();
259
+ protected setResponseHandler(id: string, cb?: (res: NDKRpcResponse) => void) {
260
+ let authUrlSent = false;
261
+ const now = Date.now();
262
+ return new Promise<NDKRpcResponse>(() => {
263
+ const responseHandler = (response: NDKRpcResponse) => {
264
+ if (response.result === 'auth_url') {
265
+ this.once(`response-${id}`, responseHandler);
266
+ if (!authUrlSent) {
267
+ authUrlSent = true;
268
+ this.emit('authUrl', response.error);
269
+ }
270
+ } else if (cb) {
271
+ if (this.requests.has(id)) {
272
+ this.requests.delete(id);
273
+ console.log('nostr-login processed nip46 request in', Date.now() - now, 'ms');
274
+ cb(response);
179
275
  }
180
- } catch (e) {
181
- // ignore
182
276
  }
183
277
  };
184
278
 
185
- this.once('request', handler);
279
+ this.once(`response-${id}`, responseHandler);
186
280
  });
187
281
  }
282
+
283
+ protected async createRequestEvent(id: string, remotePubkey: string, method: string, params: string[] = [], kind = 24133) {
284
+ this.requests.add(id);
285
+ const localUser = await this._signer.user();
286
+ const remoteUser = this._ndk.getUser({ pubkey: remotePubkey });
287
+ const request = { id, method, params };
288
+
289
+ const event = new NDKEvent(this._ndk, {
290
+ kind,
291
+ content: JSON.stringify(request),
292
+ tags: [['p', remotePubkey]],
293
+ pubkey: localUser.pubkey,
294
+ } as NostrEvent);
295
+
296
+ const useNip44 = this._useNip44 && method !== 'create_account';
297
+ const encrypt = useNip44 ? this._signer.encryptNip44 : this._signer.encrypt;
298
+ event.content = await encrypt.call(this._signer, remoteUser, event.content);
299
+ await event.sign(this._signer);
300
+
301
+ return event;
302
+ }
188
303
  }
189
304
 
190
305
  export class IframeNostrRpc extends NostrRpc {
191
306
  private peerOrigin?: string;
192
307
  private iframePort?: MessagePort;
193
308
  private iframeRequests = new Map<string, { id: string; pubkey: string }>();
309
+ private heartbeatInterval?: number;
310
+ private lastResponseTime: number = Date.now();
311
+ private heartbeatTimeoutMs: number = 30000; // 30秒応答がなければ再接続
194
312
 
195
- constructor(localSigner: PrivateKeySigner, iframePeerOrigin?: string, relays: string[] = []) {
196
- super(localSigner, relays);
313
+ public constructor(ndk: NDK, localSigner: PrivateKeySigner, iframePeerOrigin?: string) {
314
+ super(ndk, localSigner);
315
+ this._ndk = ndk;
197
316
  this.peerOrigin = iframePeerOrigin;
198
317
  }
199
318
 
319
+ public async subscribe(filter: NDKFilter): Promise<NDKSubscription> {
320
+ if (!this.peerOrigin) return super.subscribe(filter);
321
+ return new NDKSubscription(
322
+ this._ndk,
323
+ {},
324
+ {
325
+ // don't send to relay
326
+ closeOnEose: true,
327
+ cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE,
328
+ },
329
+ );
330
+ }
331
+
332
+ // ハートビート開始
333
+ private startHeartbeat() {
334
+ this.stopHeartbeat();
335
+
336
+ this.heartbeatInterval = window.setInterval(async () => {
337
+ const timeSinceLastResponse = Date.now() - this.lastResponseTime;
338
+
339
+ if (timeSinceLastResponse > this.heartbeatTimeoutMs) {
340
+ console.warn('No response from relay for too long, reconnecting...');
341
+ await this.reconnect();
342
+ }
343
+ }, 10000); // 10秒ごとにチェック
344
+ }
345
+
346
+ private stopHeartbeat() {
347
+ if (this.heartbeatInterval) {
348
+ clearInterval(this.heartbeatInterval);
349
+ this.heartbeatInterval = undefined;
350
+ }
351
+ }
352
+
200
353
  public setWorkerIframePort(port: MessagePort) {
201
354
  if (!this.peerOrigin) throw new Error('Unexpected iframe port');
355
+
202
356
  this.iframePort = port;
357
+ this.startHeartbeat();
203
358
 
204
- // keep the channel alive
359
+ // to make sure Chrome doesn't terminate the channel
205
360
  setInterval(() => {
206
- try {
207
- this.iframePort!.postMessage('ping');
208
- } catch (e) {}
361
+ console.log('iframe-nip46 ping');
362
+ this.iframePort!.postMessage('ping');
209
363
  }, 5000);
210
364
 
211
- this.iframePort.onmessage = async ev => {
212
- // handle special error reply
365
+ port.onmessage = async ev => {
366
+ console.log('iframe-nip46 got response', ev.data);
213
367
  if (typeof ev.data === 'string' && ev.data.startsWith('errorNoKey')) {
214
368
  const event_id = ev.data.split(':')[1];
215
- const entry = this.iframeRequests.get(event_id) || { id: '', pubkey: '' };
216
- const { id = '', pubkey = '' } = entry;
369
+ const { id = '', pubkey = '' } = this.iframeRequests.get(event_id) || {};
217
370
  if (id && pubkey && this.requests.has(id)) this.emit(`iframeRestart-${pubkey}`);
218
371
  return;
219
372
  }
220
373
 
374
+ // a copy-paste from rpc.subscribe
221
375
  try {
222
376
  const event = ev.data;
377
+
223
378
  if (!validateEvent(event)) throw new Error('Invalid event from iframe');
224
379
  if (!verifySignature(event)) throw new Error('Invalid event signature from iframe');
225
- const parsed = await this.parseEvent(event);
226
- if (!(parsed as RpcRequest).method) {
227
- this.emit(`response-${(parsed as RpcResponse).id}`, parsed as RpcResponse);
380
+ const nevent = new NDKEvent(this._ndk, event);
381
+ const parsedEvent = await this.parseEvent(nevent);
382
+ // レスポンス受信時にタイムスタンプを更新
383
+ this.lastResponseTime = Date.now();
384
+ // we're only implementing client-side rpc
385
+ if (!(parsedEvent as NDKRpcRequest).method) {
386
+ console.log('parsed response', parsedEvent);
387
+ this.emit(`response-${parsedEvent.id}`, parsedEvent);
228
388
  }
229
389
  } catch (e) {
230
- // ignore parse errors
390
+ console.log('error parsing event', e, ev.data);
231
391
  }
232
392
  };
233
393
  }
234
394
 
235
- public async sendRequest(remotePubkey: string, method: string, params: any[] = [], kind = 24133, cb?: (res: RpcResponse) => void): Promise<RpcResponse | undefined> {
395
+ public async sendRequest(remotePubkey: string, method: string, params: string[] = [], kind = 24133, cb?: (res: NDKRpcResponse) => void): Promise<NDKRpcResponse> {
236
396
  const id = this.getId();
237
- this.setResponseHandler(id, cb);
397
+
398
+ // create and sign request event
238
399
  const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
239
400
 
240
- // map request event id -> id for iframe restarts
241
- this.iframeRequests.set(event.id, { id, pubkey: remotePubkey });
401
+ // set response handler, it will dedup auth urls,
402
+ // and also dedup response handlers - we're sending
403
+ // to relays and to iframe
404
+ this.setResponseHandler(id, cb);
242
405
 
243
406
  if (this.iframePort) {
244
- try {
245
- this.iframePort.postMessage(event);
246
- } catch (e) {
247
- // fallthrough to publish to relays as well
248
- await this.publishRequest(event);
249
- }
407
+ // map request event id to request id, if iframe
408
+ // has no key it will reply with error:event_id (it can't
409
+ // decrypt the request id without keys)
410
+ this.iframeRequests.set(event.id, { id, pubkey: remotePubkey });
411
+
412
+ // send to iframe
413
+ console.log('iframe-nip46 sending request to', this.peerOrigin, event.rawEvent());
414
+ this.iframePort.postMessage(event.rawEvent());
250
415
  } else {
251
- await this.publishRequest(event);
416
+ // send to relays
417
+ await event.publish();
252
418
  }
253
419
 
254
- return undefined as any;
420
+ // see notes in 'super'
421
+ // @ts-ignore
422
+ return undefined as NDKRpcResponse;
255
423
  }
256
424
  }
257
425
 
@@ -264,14 +432,20 @@ export class ReadyListener {
264
432
  this.origin = origin;
265
433
  this.messages = messages;
266
434
  this.promise = new Promise<any>(ok => {
435
+ console.log(new Date(), 'started listener for', this.messages);
436
+
437
+ // ready message handler
267
438
  const onReady = async (e: MessageEvent) => {
268
439
  const originHostname = new URL(origin!).hostname;
269
440
  const messageHostname = new URL(e.origin).hostname;
441
+ // same host or subdomain
270
442
  const validHost = messageHostname === originHostname || messageHostname.endsWith('.' + originHostname);
271
443
  if (!validHost || !Array.isArray(e.data) || !e.data.length || !this.messages.includes(e.data[0])) {
444
+ // console.log(new Date(), 'got invalid ready message', e.origin, e.data);
272
445
  return;
273
446
  }
274
447
 
448
+ console.log(new Date(), 'got ready message from', e.origin, e.data);
275
449
  window.removeEventListener('message', onReady);
276
450
  ok(e.data);
277
451
  };
@@ -280,32 +454,39 @@ export class ReadyListener {
280
454
  }
281
455
 
282
456
  async wait(): Promise<any> {
457
+ console.log(new Date(), 'waiting for', this.messages);
283
458
  const r = await this.promise;
459
+ // NOTE: timer here doesn't help bcs it must be activated when
460
+ // user "confirms", but that's happening on a different
461
+ // origin and we can't really know.
462
+ // await new Promise<any>((ok, err) => {
463
+ // // 10 sec should be more than enough
464
+ // setTimeout(() => err(new Date() + ' timeout for ' + this.message), 10000);
465
+
466
+ // // if promise already resolved or will resolve in the future
467
+ // this.promise.then(ok);
468
+ // });
469
+
470
+ console.log(new Date(), 'finished waiting for', this.messages, r);
284
471
  return r;
285
472
  }
286
473
  }
287
474
 
288
- export class Nip46Signer extends EventEmitter {
475
+ export class Nip46Signer extends NDKNip46Signer {
289
476
  private _userPubkey: string = '';
290
- public remotePubkey: string = '';
291
- public rpc: IframeNostrRpc | NostrRpc;
292
- private localSigner: PrivateKeySigner;
293
-
294
- constructor(localSigner: PrivateKeySigner, signerPubkey: string, iframeOrigin?: string, relays: string[] = []) {
295
- super();
296
- this.remotePubkey = signerPubkey;
297
- this.localSigner = localSigner;
477
+ private _rpc: IframeNostrRpc;
298
478
 
299
- if (iframeOrigin) {
300
- this.rpc = new IframeNostrRpc(localSigner, iframeOrigin, relays);
301
- } else {
302
- this.rpc = new NostrRpc(localSigner, relays);
303
- }
304
- (this.rpc as any).setUseNip44(true);
479
+ constructor(ndk: NDK, localSigner: PrivateKeySigner, signerPubkey: string, iframeOrigin?: string) {
480
+ super(ndk, signerPubkey, localSigner);
305
481
 
306
- this.rpc.on('authUrl', (url: string) => {
482
+ // override with our own rpc implementation
483
+ this._rpc = new IframeNostrRpc(ndk, localSigner, iframeOrigin);
484
+ this._rpc.setUseNip44(true); // !!this.params.optionsModal.dev);
485
+ this._rpc.on('authUrl', (url: string) => {
307
486
  this.emit('authUrl', url);
308
487
  });
488
+
489
+ this.rpc = this._rpc;
309
490
  }
310
491
 
311
492
  get userPubkey() {
@@ -313,12 +494,17 @@ export class Nip46Signer extends EventEmitter {
313
494
  }
314
495
 
315
496
  private async setSignerPubkey(signerPubkey: string, sameAsUser: boolean = false) {
497
+ console.log('setSignerPubkey', signerPubkey);
498
+
499
+ // ensure it's set
316
500
  this.remotePubkey = signerPubkey;
317
501
 
318
- this.rpc.on(`iframeRestart-${signerPubkey}`, () => {
502
+ // when we're sure it's known
503
+ this._rpc.on(`iframeRestart-${signerPubkey}`, () => {
319
504
  this.emit('iframeRestart');
320
505
  });
321
506
 
507
+ // now call getPublicKey and swap remotePubkey w/ that
322
508
  await this.initUserPubkey(sameAsUser ? signerPubkey : '');
323
509
  }
324
510
 
@@ -330,51 +516,57 @@ export class Nip46Signer extends EventEmitter {
330
516
  return;
331
517
  }
332
518
 
333
- this._userPubkey = await new Promise<string>((ok, err) => {
334
- if (!this.remotePubkey) throw new Error('Signer pubkey not set');
335
- this.rpc.sendRequest(this.remotePubkey, 'get_public_key', [], 24133, (response: RpcResponse) => {
336
- if (response.error) return err(response.error);
337
- ok(response.result);
338
- });
339
- });
519
+ this._userPubkey = await withTimeout(
520
+ new Promise<string>((ok, err) => {
521
+ if (!this.remotePubkey) throw new Error('Signer pubkey not set');
522
+
523
+ console.log('get_public_key', this.remotePubkey);
524
+ this._rpc.sendRequest(this.remotePubkey, 'get_public_key', [], 24133, (response: NDKRpcResponse) => {
525
+ if (response.error) {
526
+ err(new Error(response.error));
527
+ } else {
528
+ ok(response.result);
529
+ }
530
+ });
531
+ }),
532
+ NIP46_REQUEST_TIMEOUT,
533
+ 'Timeout getting public key',
534
+ );
340
535
  }
341
536
 
342
537
  public async listen(nostrConnectSecret: string) {
343
- const signerPubkey = await (this.rpc as any).listen(nostrConnectSecret, (this.rpc as any).relays);
538
+ const signerPubkey = await (this.rpc as IframeNostrRpc).listen(nostrConnectSecret);
344
539
  await this.setSignerPubkey(signerPubkey);
345
540
  }
346
541
 
347
542
  public async connect(token?: string, perms?: string) {
348
543
  if (!this.remotePubkey) throw new Error('No signer pubkey');
349
- return new Promise<void>((ok, err) => {
350
- const params = [this.localSigner.pubkey, token || '', perms || ''];
351
- this.rpc.sendRequest(this.remotePubkey, 'connect', params, 24133, (response: RpcResponse) => {
352
- if (response.result === 'ack') ok();
353
- else err(response.error);
354
- });
355
- });
544
+ await (this._rpc as any).connectWithTimeout(this.remotePubkey, token, perms, NIP46_CONNECT_TIMEOUT);
545
+ await this.setSignerPubkey(this.remotePubkey);
356
546
  }
357
547
 
358
- // convenience wrappers
359
- public async createAccount2(params: any) {
360
- return new Promise((ok, err) => {
361
- this.rpc.sendRequest(this.remotePubkey, 'create_account', [params], 24133, (response: RpcResponse) => {
362
- if (response.error) err(response.error);
363
- else ok(response.result);
364
- });
365
- });
548
+ public async setListenReply(reply: any, nostrConnectSecret: string) {
549
+ const signerPubkey = await this._rpc.parseNostrConnectReply(reply, nostrConnectSecret);
550
+ await this.setSignerPubkey(signerPubkey, true);
366
551
  }
367
552
 
368
- public async encrypt(pubkey: string, plaintext: string) {
369
- return this.localSigner.encrypt(pubkey, plaintext);
370
- }
553
+ public async createAccount2({ bunkerPubkey, name, domain, perms = '' }: { bunkerPubkey: string; name: string; domain: string; perms?: string }) {
554
+ const params = [
555
+ name,
556
+ domain,
557
+ '', // email
558
+ perms,
559
+ ];
371
560
 
372
- public async decrypt(pubkey: string, ciphertext: string) {
373
- return this.localSigner.decrypt(pubkey, ciphertext);
374
- }
561
+ const r = await new Promise<NDKRpcResponse>(ok => {
562
+ this.rpc.sendRequest(bunkerPubkey, 'create_account', params, undefined, ok);
563
+ });
564
+
565
+ console.log('create_account pubkey', r);
566
+ if (r.result === 'error') {
567
+ throw new Error(r.error);
568
+ }
375
569
 
376
- public async sign(event: any) {
377
- await this.localSigner.sign(event);
378
- return event.sig;
570
+ return r.result;
379
571
  }
380
572
  }