@konemono/nostr-login 1.7.11

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.
Files changed (43) hide show
  1. package/.prettierrc.json +13 -0
  2. package/README.md +167 -0
  3. package/dist/const/index.d.ts +1 -0
  4. package/dist/iife-module.d.ts +1 -0
  5. package/dist/index.d.ts +27 -0
  6. package/dist/index.esm.js +18 -0
  7. package/dist/index.esm.js.map +1 -0
  8. package/dist/modules/AuthNostrService.d.ts +84 -0
  9. package/dist/modules/BannerManager.d.ts +20 -0
  10. package/dist/modules/ModalManager.d.ts +25 -0
  11. package/dist/modules/Nip46.d.ts +56 -0
  12. package/dist/modules/Nostr.d.ts +34 -0
  13. package/dist/modules/NostrExtensionService.d.ts +17 -0
  14. package/dist/modules/NostrParams.d.ts +8 -0
  15. package/dist/modules/Popup.d.ts +7 -0
  16. package/dist/modules/ProcessManager.d.ts +10 -0
  17. package/dist/modules/Signer.d.ts +9 -0
  18. package/dist/modules/index.d.ts +8 -0
  19. package/dist/types.d.ts +72 -0
  20. package/dist/unpkg.js +17 -0
  21. package/dist/utils/index.d.ts +27 -0
  22. package/dist/utils/nip44.d.ts +9 -0
  23. package/index.html +30 -0
  24. package/package.json +28 -0
  25. package/rollup.config.js +55 -0
  26. package/src/const/index.ts +1 -0
  27. package/src/iife-module.ts +81 -0
  28. package/src/index.ts +347 -0
  29. package/src/modules/AuthNostrService.ts +756 -0
  30. package/src/modules/BannerManager.ts +146 -0
  31. package/src/modules/ModalManager.ts +635 -0
  32. package/src/modules/Nip46.ts +441 -0
  33. package/src/modules/Nostr.ts +107 -0
  34. package/src/modules/NostrExtensionService.ts +99 -0
  35. package/src/modules/NostrParams.ts +18 -0
  36. package/src/modules/Popup.ts +27 -0
  37. package/src/modules/ProcessManager.ts +67 -0
  38. package/src/modules/Signer.ts +25 -0
  39. package/src/modules/index.ts +8 -0
  40. package/src/types.ts +124 -0
  41. package/src/utils/index.ts +326 -0
  42. package/src/utils/nip44.ts +185 -0
  43. package/tsconfig.json +15 -0
@@ -0,0 +1,756 @@
1
+ import { localStorageAddAccount, bunkerUrlToInfo, isBunkerUrl, fetchProfile, getBunkerUrl, localStorageRemoveCurrentAccount, createProfile, getIcon } from '../utils';
2
+ import { ConnectionString, Info } from 'nostr-login-components';
3
+ import { generatePrivateKey, getEventHash, getPublicKey, nip19 } from 'nostr-tools';
4
+ import { NostrLoginAuthOptions, Response } from '../types';
5
+ import NDK, { NDKEvent, NDKNip46Signer, NDKRpcResponse, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
6
+ import { NostrParams } from './';
7
+ import { EventEmitter } from 'tseep';
8
+ import { Signer } from './Nostr';
9
+ import { Nip44 } from '../utils/nip44';
10
+ import { IframeNostrRpc, Nip46Signer, ReadyListener } from './Nip46';
11
+ import { PrivateKeySigner } from './Signer';
12
+
13
+ const OUTBOX_RELAYS = ['wss://user.kindpag.es', 'wss://purplepag.es', 'wss://relay.nos.social'];
14
+ const DEFAULT_NOSTRCONNECT_RELAY = 'wss://relay.nsec.app/';
15
+ const NOSTRCONNECT_APPS: ConnectionString[] = [
16
+ {
17
+ name: 'Nsec.app',
18
+ domain: 'nsec.app',
19
+ canImport: true,
20
+ img: 'https://nsec.app/assets/favicon.ico',
21
+ link: 'https://use.nsec.app/<nostrconnect>',
22
+ relay: 'wss://relay.nsec.app/',
23
+ },
24
+ {
25
+ name: 'Amber',
26
+ img: 'https://raw.githubusercontent.com/greenart7c3/Amber/refs/heads/master/assets/android-icon.svg',
27
+ link: '<nostrconnect>',
28
+ relay: 'wss://relay.nsec.app/',
29
+ },
30
+ {
31
+ name: 'Other key stores',
32
+ img: '',
33
+ link: '<nostrconnect>',
34
+ relay: 'wss://relay.nsec.app/',
35
+ },
36
+ ];
37
+
38
+ class AuthNostrService extends EventEmitter implements Signer {
39
+ private ndk: NDK;
40
+ private profileNdk: NDK;
41
+ private signer: Nip46Signer | null = null;
42
+ private localSigner: PrivateKeySigner | null = null;
43
+ private params: NostrParams;
44
+ private signerPromise?: Promise<void>;
45
+ private signerErrCallback?: (err: string) => void;
46
+ private readyPromise?: Promise<void>;
47
+ private readyCallback?: () => void;
48
+ private nip44Codec = new Nip44();
49
+ private nostrConnectKey: string = '';
50
+ private nostrConnectSecret: string = '';
51
+ private iframe?: HTMLIFrameElement;
52
+ private starterReady?: ReadyListener;
53
+
54
+ nip04: {
55
+ encrypt: (pubkey: string, plaintext: string) => Promise<string>;
56
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
57
+ };
58
+ nip44: {
59
+ encrypt: (pubkey: string, plaintext: string) => Promise<string>;
60
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
61
+ };
62
+
63
+ constructor(params: NostrParams) {
64
+ super();
65
+ this.params = params;
66
+ this.ndk = new NDK({
67
+ enableOutboxModel: false,
68
+ });
69
+
70
+ this.profileNdk = new NDK({
71
+ enableOutboxModel: true,
72
+ explicitRelayUrls: OUTBOX_RELAYS,
73
+ });
74
+ this.profileNdk.connect();
75
+
76
+ this.nip04 = {
77
+ encrypt: this.encrypt04.bind(this),
78
+ decrypt: this.decrypt04.bind(this),
79
+ };
80
+ this.nip44 = {
81
+ encrypt: this.encrypt44.bind(this),
82
+ decrypt: this.decrypt44.bind(this),
83
+ };
84
+ }
85
+
86
+ public isIframe() {
87
+ return !!this.iframe;
88
+ }
89
+
90
+ public async waitReady() {
91
+ if (this.signerPromise) {
92
+ try {
93
+ await this.signerPromise;
94
+ } catch {}
95
+ }
96
+
97
+ if (this.readyPromise) {
98
+ try {
99
+ await this.readyPromise;
100
+ } catch {}
101
+ }
102
+ }
103
+
104
+ public cancelNostrConnect() {
105
+ this.releaseSigner();
106
+ this.resetAuth();
107
+ }
108
+
109
+ public async nostrConnect(
110
+ relay?: string,
111
+ {
112
+ domain = '',
113
+ link = '',
114
+ iframeUrl = '',
115
+ importConnect = false,
116
+ }: {
117
+ domain?: string;
118
+ link?: string;
119
+ importConnect?: boolean;
120
+ iframeUrl?: string;
121
+ } = {},
122
+ ) {
123
+ relay = relay || DEFAULT_NOSTRCONNECT_RELAY;
124
+
125
+ const relays = relay
126
+ .split(',')
127
+ .map(r => r.replace(/['" \t\r\n]/g, '')) // Remove quotes, spaces, tabs, newlines
128
+ .filter(r => r)
129
+ .map(r => {
130
+ try {
131
+ const url = new URL(r);
132
+ if (url.protocol === 'wss:' || url.protocol === 'ws:') return url.href;
133
+ } catch {}
134
+ return null;
135
+ })
136
+ .filter(r => r !== null) as string[];
137
+
138
+ const info: Info = {
139
+ authMethod: 'connect',
140
+ pubkey: '', // unknown yet!
141
+ signerPubkey: '', // unknown too!
142
+ sk: this.nostrConnectKey,
143
+ domain: domain,
144
+ relays,
145
+ iframeUrl,
146
+ };
147
+
148
+ console.log('nostrconnect info', info, link);
149
+
150
+ // non-iframe flow
151
+ if (link && !iframeUrl) window.open(link, '_blank', 'width=400,height=700');
152
+
153
+ // init nip46 signer
154
+ await this.initSigner(info, { listen: true });
155
+
156
+ // signer learns the remote pubkey
157
+ if (!info.pubkey || !info.signerPubkey) throw new Error('Bad remote pubkey');
158
+
159
+ let bunkerUrl = `bunker://${info.signerPubkey}?`;
160
+ for (const r of relays) {
161
+ bunkerUrl += `relay=${r}&`;
162
+ }
163
+ info.bunkerUrl = bunkerUrl;
164
+
165
+ // callback
166
+ if (!importConnect) this.onAuth('login', info);
167
+
168
+ return info;
169
+ }
170
+
171
+ public async createNostrConnect(relay?: string) {
172
+ this.nostrConnectKey = generatePrivateKey();
173
+ this.nostrConnectSecret = Math.random().toString(36).substring(7);
174
+
175
+ const pubkey = getPublicKey(this.nostrConnectKey);
176
+ const meta = {
177
+ name: encodeURIComponent(document.location.host),
178
+ url: encodeURIComponent(document.location.origin),
179
+ icon: encodeURIComponent(await getIcon()),
180
+ perms: encodeURIComponent(this.params.optionsModal.perms || ''),
181
+ };
182
+
183
+ return `nostrconnect://${pubkey}?image=${meta.icon}&url=${meta.url}&name=${meta.name}&perms=${meta.perms}&secret=${this.nostrConnectSecret}${relay ? `&relay=${relay}` : ''}`;
184
+ }
185
+
186
+ public async getNostrConnectServices(): Promise<[string, ConnectionString[]]> {
187
+ const nostrconnect = await this.createNostrConnect();
188
+
189
+ // copy defaults
190
+ const apps = NOSTRCONNECT_APPS.map(a => ({ ...a }));
191
+ // if (this.params.optionsModal.dev) {
192
+ // apps.push({
193
+ // name: 'Dev.Nsec.app',
194
+ // domain: 'new.nsec.app',
195
+ // canImport: true,
196
+ // img: 'https://new.nsec.app/assets/favicon.ico',
197
+ // link: 'https://dev.nsec.app/<nostrconnect>',
198
+ // relay: 'wss://relay.nsec.app/',
199
+ // });
200
+ // }
201
+
202
+ for (const a of apps) {
203
+ let relay = DEFAULT_NOSTRCONNECT_RELAY;
204
+ if (a.link.startsWith('https://')) {
205
+ let domain = a.domain || new URL(a.link).hostname;
206
+ try {
207
+ const info = await (await fetch(`https://${domain}/.well-known/nostr.json`)).json();
208
+ const pubkey = info.names['_'];
209
+ const relays = info.nip46[pubkey] as string[];
210
+ if (relays && relays.length) relay = relays[0];
211
+ a.iframeUrl = info.nip46.iframe_url || '';
212
+ } catch (e) {
213
+ console.log('Bad app info', e, a);
214
+ }
215
+ }
216
+ const nc = nostrconnect + '&relay=' + relay;
217
+ if (a.iframeUrl) {
218
+ // pass plain nc url for iframe-based flow
219
+ a.link = nc;
220
+ } else {
221
+ // we will open popup ourselves
222
+ a.link = a.link.replace('<nostrconnect>', nc);
223
+ }
224
+ }
225
+
226
+ return [nostrconnect, apps];
227
+ }
228
+
229
+ public async localSignup(name: string, sk?: string) {
230
+ const signup = !sk;
231
+ sk = sk || generatePrivateKey();
232
+ const pubkey = getPublicKey(sk);
233
+ const info: Info = {
234
+ pubkey,
235
+ sk,
236
+ name,
237
+ authMethod: 'local',
238
+ };
239
+ console.log(`localSignup name: ${name}`);
240
+ await this.setLocal(info, signup);
241
+ }
242
+
243
+ public async setLocal(info: Info, signup?: boolean) {
244
+ this.releaseSigner();
245
+ this.localSigner = new PrivateKeySigner(info.sk!);
246
+
247
+ if (signup) await createProfile(info, this.profileNdk, this.localSigner, this.params.optionsModal.signupRelays, this.params.optionsModal.outboxRelays);
248
+
249
+ this.onAuth(signup ? 'signup' : 'login', info);
250
+ }
251
+
252
+ public prepareImportUrl(url: string) {
253
+ // for OTP we choose interactive import
254
+ if (this.params.userInfo?.authMethod === 'otp') return url + '&import=true';
255
+
256
+ // for local we export our existing key
257
+ if (!this.localSigner || this.params.userInfo?.authMethod !== 'local') throw new Error('Most be local keys');
258
+ return url + '#import=' + nip19.nsecEncode(this.localSigner.privateKey!);
259
+ }
260
+
261
+ public async importAndConnect(cs: ConnectionString) {
262
+ const { relay, domain, link, iframeUrl } = cs;
263
+ if (!domain) throw new Error('Domain required');
264
+
265
+ const info = await this.nostrConnect(relay, { domain, link, importConnect: true, iframeUrl });
266
+
267
+ // logout to remove local keys from storage
268
+ // but keep the connect signer
269
+ await this.logout(/*keepSigner*/ true);
270
+
271
+ // release local one
272
+ this.localSigner = null;
273
+
274
+ // notify app that we've switched to 'connect' keys
275
+ this.onAuth('login', info);
276
+ }
277
+
278
+ public setReadOnly(pubkey: string) {
279
+ const info: Info = { pubkey, authMethod: 'readOnly' };
280
+ this.onAuth('login', info);
281
+ }
282
+
283
+ public setExtension(pubkey: string) {
284
+ const info: Info = { pubkey, authMethod: 'extension' };
285
+ this.onAuth('login', info);
286
+ }
287
+
288
+ public setOTP(pubkey: string, data: string) {
289
+ const info: Info = { pubkey, authMethod: 'otp', otpData: data };
290
+ this.onAuth('login', info);
291
+ }
292
+
293
+ public async setConnect(info: Info) {
294
+ this.releaseSigner();
295
+ await this.startAuth();
296
+ await this.initSigner(info);
297
+ this.onAuth('login', info);
298
+ await this.endAuth();
299
+ }
300
+
301
+ public async createAccount(nip05: string) {
302
+ const [name, domain] = nip05.split('@');
303
+
304
+ // bunker's own url
305
+ const bunkerUrl = await getBunkerUrl(`_@${domain}`, this.params.optionsModal);
306
+ console.log("create account bunker's url", bunkerUrl);
307
+
308
+ // parse bunker url and generate local nsec
309
+ const info = bunkerUrlToInfo(bunkerUrl);
310
+ if (!info.signerPubkey) throw new Error('Bad bunker url');
311
+
312
+ const eventToAddAccount = Boolean(this.params.userInfo);
313
+
314
+ // init signer to talk to the bunker (not the user!)
315
+ await this.initSigner(info, { eventToAddAccount });
316
+
317
+ const userPubkey = await this.signer!.createAccount2({ bunkerPubkey: info.signerPubkey!, name, domain, perms: this.params.optionsModal.perms });
318
+
319
+ return {
320
+ bunkerUrl: `bunker://${userPubkey}?relay=${info.relays?.[0]}`,
321
+ sk: info.sk, // reuse the same local key
322
+ };
323
+ }
324
+
325
+ private releaseSigner() {
326
+ this.signer = null;
327
+ this.signerErrCallback?.('cancelled');
328
+ this.localSigner = null;
329
+
330
+ // disconnect from signer relays
331
+ for (const r of this.ndk.pool.relays.keys()) {
332
+ this.ndk.pool.removeRelay(r);
333
+ }
334
+ }
335
+
336
+ public async logout(keepSigner = false) {
337
+ if (!keepSigner) this.releaseSigner();
338
+
339
+ // move current to recent
340
+ localStorageRemoveCurrentAccount();
341
+
342
+ // notify everyone
343
+ this.onAuth('logout');
344
+
345
+ this.emit('updateAccounts');
346
+ }
347
+
348
+ private setUserInfo(userInfo: Info | null) {
349
+ this.params.userInfo = userInfo;
350
+ this.emit('onUserInfo', userInfo);
351
+
352
+ if (userInfo) {
353
+ localStorageAddAccount(userInfo);
354
+ this.emit('updateAccounts');
355
+ }
356
+ }
357
+
358
+ public exportKeys() {
359
+ if (!this.params.userInfo) return '';
360
+ if (this.params.userInfo.authMethod !== 'local') return '';
361
+ return nip19.nsecEncode(this.params.userInfo.sk!);
362
+ }
363
+
364
+ private onAuth(type: 'login' | 'signup' | 'logout', info: Info | null = null) {
365
+ if (type !== 'logout' && !info) throw new Error('No user info in onAuth');
366
+
367
+ // make sure we emulate logout first
368
+ if (info && this.params.userInfo && (info.pubkey !== this.params.userInfo.pubkey || info.authMethod !== this.params.userInfo.authMethod)) {
369
+ const event = new CustomEvent('nlAuth', { detail: { type: 'logout' } });
370
+ console.log('nostr-login auth', event.detail);
371
+ document.dispatchEvent(event)
372
+ }
373
+
374
+ this.setUserInfo(info);
375
+
376
+ if (info) {
377
+ // async profile fetch
378
+ fetchProfile(info, this.profileNdk).then(p => {
379
+ if (this.params.userInfo !== info) return;
380
+
381
+ const userInfo = {
382
+ ...this.params.userInfo,
383
+ picture: p?.image || p?.picture,
384
+ name: p?.name || p?.displayName || p?.nip05 || nip19.npubEncode(info.pubkey),
385
+ // NOTE: do not overwrite info.nip05 with the one from profile!
386
+ // info.nip05 refers to nip46 provider,
387
+ // profile.nip05 is just a fancy name that user has chosen
388
+ // nip05: p?.nip05
389
+ };
390
+
391
+ this.setUserInfo(userInfo);
392
+ });
393
+ }
394
+
395
+ try {
396
+ const npub = info ? nip19.npubEncode(info.pubkey) : '';
397
+
398
+ const options: NostrLoginAuthOptions = {
399
+ type,
400
+ };
401
+
402
+ if (type === 'logout') {
403
+ // reset
404
+ if (this.iframe) this.iframe.remove();
405
+ this.iframe = undefined;
406
+ } else {
407
+ options.pubkey = info!.pubkey;
408
+ options.name = info!.name;
409
+
410
+ if (info!.sk) {
411
+ options.localNsec = nip19.nsecEncode(info!.sk);
412
+ }
413
+
414
+ if (info!.relays) {
415
+ options.relays = info!.relays;
416
+ }
417
+
418
+ if (info!.otpData) {
419
+ options.otpData = info!.otpData;
420
+ }
421
+
422
+ options.method = info!.authMethod || 'connect';
423
+ }
424
+
425
+ const event = new CustomEvent('nlAuth', { detail: options });
426
+ console.log('nostr-login auth', options);
427
+ document.dispatchEvent(event);
428
+
429
+ if (this.params.optionsModal.onAuth) {
430
+ this.params.optionsModal.onAuth(npub, options);
431
+ }
432
+ } catch (e) {
433
+ console.log('onAuth error', e);
434
+ }
435
+ }
436
+
437
+ private async createIframe(iframeUrl?: string) {
438
+ if (!iframeUrl) return undefined;
439
+
440
+ // ensure iframe
441
+ const url = new URL(iframeUrl);
442
+ const domain = url.hostname;
443
+ let iframe: HTMLIFrameElement | undefined;
444
+
445
+ // one iframe per domain
446
+ const did = domain.replaceAll('.', '-');
447
+ const id = '__nostr-login-worker-iframe-' + did;
448
+ iframe = document.querySelector(`#${id}`) as HTMLIFrameElement;
449
+ console.log('iframe', id, iframe);
450
+ if (!iframe) {
451
+ iframe = document.createElement('iframe');
452
+ iframe.setAttribute('width', '0');
453
+ iframe.setAttribute('height', '0');
454
+ iframe.setAttribute('border', '0');
455
+ iframe.style.display = 'none';
456
+ // iframe.setAttribute('sandbox', 'allow-forms allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts');
457
+ iframe.id = id;
458
+ document.body.append(iframe);
459
+ }
460
+
461
+ // wait until loaded
462
+ iframe.setAttribute('src', iframeUrl);
463
+
464
+ // we start listening right now to avoid races
465
+ // with 'load' event below
466
+ const ready = new ReadyListener(['workerReady', 'workerError'], url.origin);
467
+
468
+ await new Promise(ok => {
469
+ iframe!.addEventListener('load', ok);
470
+ });
471
+
472
+ // now make sure the iframe is ready,
473
+ // timeout timer starts here
474
+ const r = await ready.wait();
475
+
476
+ // FIXME wait until the iframe is ready to accept requests,
477
+ // maybe it should send us some message?
478
+
479
+ console.log('nostr-login iframe ready', iframeUrl, r);
480
+
481
+ return { iframe, port: r[1] as MessagePort };
482
+ }
483
+
484
+ // private async getIframeUrl(domain?: string) {
485
+ // if (!domain) return '';
486
+ // try {
487
+ // const r = await fetch(`https://${domain}/.well-known/nostr.json`);
488
+ // const data = await r.json();
489
+ // return data.nip46?.iframe_url || '';
490
+ // } catch (e) {
491
+ // console.log('failed to fetch iframe url', e, domain);
492
+ // return '';
493
+ // }
494
+ // }
495
+
496
+ public async sendNeedAuth() {
497
+ const [nostrconnect] = await this.getNostrConnectServices();
498
+ const event = new CustomEvent('nlNeedAuth', { detail: { nostrconnect } });
499
+ console.log('nostr-login need auth', nostrconnect);
500
+ document.dispatchEvent(event);
501
+ }
502
+
503
+ public isAuthing() {
504
+ return !!this.readyCallback;
505
+ }
506
+
507
+ public async startAuth() {
508
+ console.log("startAuth");
509
+ if (this.readyCallback) throw new Error('Already started');
510
+
511
+ // start the new promise
512
+ this.readyPromise = new Promise<void>(ok => (this.readyCallback = ok));
513
+ }
514
+
515
+ public async endAuth() {
516
+ console.log('endAuth', this.params.userInfo);
517
+ if (this.params.userInfo && this.params.userInfo.iframeUrl) {
518
+ // create iframe
519
+ const { iframe, port } = (await this.createIframe(this.params.userInfo.iframeUrl)) || {};
520
+ this.iframe = iframe;
521
+ if (!this.iframe || !port) return;
522
+
523
+ // assign iframe to RPC object
524
+ (this.signer!.rpc as IframeNostrRpc).setWorkerIframePort(port);
525
+ }
526
+
527
+ this.readyCallback!();
528
+ this.readyCallback = undefined;
529
+ }
530
+
531
+ public resetAuth() {
532
+ if (this.readyCallback) this.readyCallback();
533
+ this.readyCallback = undefined;
534
+ }
535
+
536
+ private async listen(info: Info) {
537
+ if (!info.iframeUrl) return this.signer!.listen(this.nostrConnectSecret);
538
+ const r = await this.starterReady!.wait();
539
+ if (r[0] === 'starterError') throw new Error(r[1]);
540
+ return this.signer!.setListenReply(r[1], this.nostrConnectSecret);
541
+ }
542
+
543
+ public async connect(info: Info, perms?: string) {
544
+ return this.signer!.connect(info.token, perms);
545
+ }
546
+
547
+ public async initSigner(info: Info, { listen = false, connect = false, eventToAddAccount = false } = {}) {
548
+ // mutex
549
+ if (this.signerPromise) {
550
+ try {
551
+ await this.signerPromise;
552
+ } catch {}
553
+ }
554
+
555
+ // we remove support for iframe from nip05 and bunker-url methods,
556
+ // only nostrconnect flow will use it.
557
+ // info.iframeUrl = info.iframeUrl || (await this.getIframeUrl(info.domain));
558
+ console.log('initSigner info', info);
559
+
560
+ // start listening for the ready signal
561
+ const iframeOrigin = info.iframeUrl ? new URL(info.iframeUrl!).origin : undefined;
562
+ if (iframeOrigin) this.starterReady = new ReadyListener(['starterDone', 'starterError'], iframeOrigin);
563
+
564
+ // notify modals so they could show the starter iframe,
565
+ // FIXME shouldn't this come from nostrconnect service list?
566
+ this.emit('onIframeUrl', info.iframeUrl);
567
+
568
+ this.signerPromise = new Promise<void>(async (ok, err) => {
569
+ this.signerErrCallback = err;
570
+ try {
571
+ // pre-connect if we're creating the connection (listen|connect) or
572
+ // not iframe mode
573
+ if (info.relays && !info.iframeUrl) {
574
+ for (const r of info.relays) {
575
+ this.ndk.addExplicitRelay(r, undefined);
576
+ }
577
+ }
578
+
579
+ // wait until we connect, otherwise
580
+ // signer won't start properly
581
+ await new Promise<void>((resolve, reject) => {
582
+ let finished = false
583
+ // 10 sec timeout for connection
584
+ setTimeout(() => {
585
+ if (!finished) {
586
+ finished = true
587
+ reject('Connection timed out')
588
+ }
589
+ }, 10000)
590
+
591
+ this.ndk.connect().then(() => {
592
+ if (!finished) {
593
+ finished = true
594
+ resolve()
595
+ }
596
+ }).catch(e => {
597
+ if (!finished) {
598
+ finished = true
599
+ reject(e)
600
+ }
601
+ })
602
+ });
603
+
604
+ // create and prepare the signer
605
+ const localSigner = new PrivateKeySigner(info.sk!);
606
+ this.signer = new Nip46Signer(this.ndk, localSigner, info.signerPubkey!, iframeOrigin);
607
+
608
+ // we should notify the banner the same way as
609
+ // the onAuthUrl does
610
+ this.signer.on(`iframeRestart`, async () => {
611
+ const iframeUrl = info.iframeUrl + (info.iframeUrl!.includes('?') ? '&' : '?') + 'pubkey=' + info.pubkey + '&rebind=' + localSigner.pubkey;
612
+ this.emit('iframeRestart', { pubkey: info.pubkey, iframeUrl });
613
+ });
614
+
615
+ // OAuth flow
616
+ // if (!listen) {
617
+ this.signer.on('authUrl', (url: string) => {
618
+ console.log('nostr login auth url', url);
619
+
620
+ // notify our UI
621
+ this.emit('onAuthUrl', { url, iframeUrl: info.iframeUrl, eventToAddAccount });
622
+ });
623
+ // }
624
+
625
+ if (listen) {
626
+ // nostrconnect: flow
627
+ // wait for the incoming message from signer
628
+ await this.listen(info);
629
+ } else if (connect) {
630
+ // bunker: flow
631
+ // send 'connect' message to signer
632
+ await this.connect(info, this.params.optionsModal.perms);
633
+ } else {
634
+ // provide saved pubkey as a hint
635
+ await this.signer!.initUserPubkey(info.pubkey);
636
+ }
637
+
638
+ // ensure, we're using it in callbacks above
639
+ // and expect info to be valid after this call
640
+ info.pubkey = this.signer!.userPubkey;
641
+ // learned after nostrconnect flow
642
+ info.signerPubkey = this.signer!.remotePubkey;
643
+
644
+ ok();
645
+ } catch (e) {
646
+ console.log('initSigner failure', e);
647
+ // make sure signer isn't set
648
+ this.signer = null;
649
+ err(e);
650
+ }
651
+ });
652
+
653
+ return this.signerPromise;
654
+ }
655
+
656
+ public async authNip46(
657
+ type: 'login' | 'signup',
658
+ { name, bunkerUrl, sk = '', domain = '', iframeUrl = '' }: { name: string; bunkerUrl: string; sk?: string; domain?: string; iframeUrl?: string },
659
+ ) {
660
+ try {
661
+ const info = bunkerUrlToInfo(bunkerUrl, sk);
662
+ if (isBunkerUrl(name)) info.bunkerUrl = name;
663
+ else {
664
+ info.nip05 = name;
665
+ info.domain = name.split('@')[1];
666
+ }
667
+ if (domain) info.domain = domain;
668
+ if (iframeUrl) info.iframeUrl = iframeUrl;
669
+
670
+ // console.log('nostr login auth info', info);
671
+ if (!info.signerPubkey || !info.sk || !info.relays?.[0]) {
672
+ throw new Error(`Bad bunker url ${bunkerUrl}`);
673
+ }
674
+
675
+ const eventToAddAccount = Boolean(this.params.userInfo);
676
+ console.log('authNip46', type, info);
677
+
678
+ // updates the info
679
+ await this.initSigner(info, { connect: true, eventToAddAccount });
680
+
681
+ // callback
682
+ this.onAuth(type, info);
683
+ } catch (e) {
684
+ console.log('nostr login auth failed', e);
685
+ // make ure it's closed
686
+ // this.popupManager.closePopup();
687
+ throw e;
688
+ }
689
+ }
690
+
691
+ public async signEvent(event: any) {
692
+ if (this.localSigner) {
693
+ event.pubkey = getPublicKey(this.localSigner.privateKey!);
694
+ event.id = getEventHash(event);
695
+ event.sig = await this.localSigner.sign(event);
696
+ } else {
697
+ event.pubkey = this.signer?.remotePubkey;
698
+ event.id = getEventHash(event);
699
+ event.sig = await this.signer?.sign(event);
700
+ }
701
+ console.log('signed', { event });
702
+ return event;
703
+ }
704
+
705
+ private async codec_call(method: string, pubkey: string, param: string) {
706
+ return new Promise<string>((resolve, reject) => {
707
+ this.signer!.rpc.sendRequest(this.signer!.remotePubkey!, method, [pubkey, param], 24133, (response: NDKRpcResponse) => {
708
+ if (!response.error) {
709
+ resolve(response.result);
710
+ } else {
711
+ reject(response.error);
712
+ }
713
+ });
714
+ });
715
+ }
716
+
717
+ public async encrypt04(pubkey: string, plaintext: string) {
718
+ if (this.localSigner) {
719
+ return this.localSigner.encrypt(new NDKUser({ pubkey }), plaintext);
720
+ } else {
721
+ return this.signer!.encrypt(new NDKUser({ pubkey }), plaintext);
722
+ }
723
+ }
724
+
725
+ public async decrypt04(pubkey: string, ciphertext: string) {
726
+ if (this.localSigner) {
727
+ return this.localSigner.decrypt(new NDKUser({ pubkey }), ciphertext);
728
+ } else {
729
+ // decrypt is broken in ndk v2.3.1, and latest
730
+ // ndk v2.8.1 doesn't allow to override connect easily,
731
+ // so we reimplement and fix decrypt here as a temporary fix
732
+
733
+ return this.codec_call('nip04_decrypt', pubkey, ciphertext);
734
+ }
735
+ }
736
+
737
+ public async encrypt44(pubkey: string, plaintext: string) {
738
+ if (this.localSigner) {
739
+ return this.nip44Codec.encrypt(this.localSigner.privateKey!, pubkey, plaintext);
740
+ } else {
741
+ // no support of nip44 in ndk yet
742
+ return this.codec_call('nip44_encrypt', pubkey, plaintext);
743
+ }
744
+ }
745
+
746
+ public async decrypt44(pubkey: string, ciphertext: string) {
747
+ if (this.localSigner) {
748
+ return this.nip44Codec.decrypt(this.localSigner.privateKey!, pubkey, ciphertext);
749
+ } else {
750
+ // no support of nip44 in ndk yet
751
+ return this.codec_call('nip44_decrypt', pubkey, ciphertext);
752
+ }
753
+ }
754
+ }
755
+
756
+ export default AuthNostrService;