@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,441 @@
1
+ import NDK, { NDKEvent, NDKFilter, NDKNip46Signer, NDKNostrRpc, NDKRpcRequest, NDKRpcResponse, NDKSubscription, NDKSubscriptionCacheUsage, NostrEvent } from '@nostr-dev-kit/ndk';
2
+ import { validateEvent, verifySignature } from 'nostr-tools';
3
+ import { PrivateKeySigner } from './Signer';
4
+
5
+ class NostrRpc extends NDKNostrRpc {
6
+ protected _ndk: NDK;
7
+ protected _signer: PrivateKeySigner;
8
+ protected requests: Set<string> = new Set();
9
+ private sub?: NDKSubscription;
10
+ protected _useNip44: boolean = false;
11
+
12
+ public constructor(ndk: NDK, signer: PrivateKeySigner) {
13
+ super(ndk, signer, ndk.debug.extend('nip46:signer:rpc'));
14
+ this._ndk = ndk;
15
+ this._signer = signer;
16
+ }
17
+
18
+ public async subscribe(filter: NDKFilter): Promise<NDKSubscription> {
19
+ // NOTE: fixing ndk
20
+ filter.kinds = filter.kinds?.filter(k => k === 24133);
21
+ this.sub = await super.subscribe(filter);
22
+ return this.sub;
23
+ }
24
+
25
+ public stop() {
26
+ if (this.sub) {
27
+ this.sub.stop();
28
+ this.sub = undefined;
29
+ }
30
+ }
31
+
32
+ public setUseNip44(useNip44: boolean) {
33
+ this._useNip44 = useNip44;
34
+ }
35
+
36
+ private isNip04(ciphertext: string) {
37
+ const l = ciphertext.length;
38
+ if (l < 28) return false;
39
+ return ciphertext[l - 28] === '?' && ciphertext[l - 27] === 'i' && ciphertext[l - 26] === 'v' && ciphertext[l - 25] === '=';
40
+ }
41
+
42
+ // override to auto-decrypt nip04/nip44
43
+ public async parseEvent(event: NDKEvent): Promise<NDKRpcRequest | NDKRpcResponse> {
44
+ const remoteUser = this._ndk.getUser({ pubkey: event.pubkey });
45
+ remoteUser.ndk = this._ndk;
46
+ const decrypt = this.isNip04(event.content) ? this._signer.decrypt : this._signer.decryptNip44;
47
+ const decryptedContent = await decrypt.call(this._signer, remoteUser, event.content);
48
+ const parsedContent = JSON.parse(decryptedContent);
49
+ const { id, method, params, result, error } = parsedContent;
50
+
51
+ if (method) {
52
+ return { id, pubkey: event.pubkey, method, params, event };
53
+ } else {
54
+ return { id, result, error, event };
55
+ }
56
+ }
57
+
58
+ public async parseNostrConnectReply(reply: any, secret: string) {
59
+ const event = new NDKEvent(this._ndk, reply);
60
+ const parsedEvent = await this.parseEvent(event);
61
+ console.log('nostr connect parsedEvent', parsedEvent);
62
+ if (!(parsedEvent as NDKRpcRequest).method) {
63
+ const response = parsedEvent as NDKRpcResponse;
64
+ if (response.result !== secret) throw new Error(response.error);
65
+ return event.pubkey;
66
+ } else {
67
+ throw new Error('Bad nostr connect reply');
68
+ }
69
+ }
70
+
71
+ // ndk doesn't support nostrconnect:
72
+ // we just listed to an unsolicited reply to
73
+ // our pubkey and if it's ack/secret - we're fine
74
+ public async listen(nostrConnectSecret: string): Promise<string> {
75
+ const pubkey = this._signer.pubkey;
76
+ console.log('nostr-login listening for conn to', pubkey);
77
+ const sub = await this.subscribe({
78
+ 'kinds': [24133],
79
+ '#p': [pubkey],
80
+ });
81
+ return new Promise<string>((ok, err) => {
82
+ sub.on('event', async (event: NDKEvent) => {
83
+ try {
84
+ const parsedEvent = await this.parseEvent(event);
85
+ // console.log('ack parsedEvent', parsedEvent);
86
+ if (!(parsedEvent as NDKRpcRequest).method) {
87
+ const response = parsedEvent as NDKRpcResponse;
88
+
89
+ // ignore
90
+ if (response.result === 'auth_url') return;
91
+
92
+ // FIXME for now accept 'ack' replies, later on only
93
+ // accept secrets
94
+ if (response.result === 'ack' || response.result === nostrConnectSecret) {
95
+ ok(event.pubkey);
96
+ } else {
97
+ err(response.error);
98
+ }
99
+ }
100
+ } catch (e) {
101
+ console.log('error parsing event', e, event.rawEvent());
102
+ }
103
+ // done
104
+ this.stop();
105
+ });
106
+ });
107
+ }
108
+
109
+ // since ndk doesn't yet support perms param
110
+ // we reimplement the 'connect' call here
111
+ // instead of await signer.blockUntilReady();
112
+ public async connect(pubkey: string, token?: string, perms?: string) {
113
+ return new Promise<void>((ok, err) => {
114
+ const connectParams = [pubkey!, token || '', perms || ''];
115
+ this.sendRequest(pubkey!, 'connect', connectParams, 24133, (response: NDKRpcResponse) => {
116
+ if (response.result === 'ack') {
117
+ ok();
118
+ } else {
119
+ err(response.error);
120
+ }
121
+ });
122
+ });
123
+ }
124
+
125
+ protected getId(): string {
126
+ return Math.random().toString(36).substring(7);
127
+ }
128
+
129
+ public async sendRequest(remotePubkey: string, method: string, params: string[] = [], kind = 24133, cb?: (res: NDKRpcResponse) => void): Promise<NDKRpcResponse> {
130
+ const id = this.getId();
131
+
132
+ // response handler will deduplicate auth urls and responses
133
+ this.setResponseHandler(id, cb);
134
+
135
+ // create and sign request
136
+ const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
137
+ console.log("sendRequest", { event, method, remotePubkey, params });
138
+
139
+ // send to relays
140
+ const relays = await event.publish();
141
+ if (relays.size === 0) throw new Error('Failed to publish to relays');
142
+
143
+ // NOTE: ndk returns a promise that never resolves and
144
+ // in fact REQUIRES cb to be provided (otherwise no way
145
+ // to consume the result), we've already stepped on the bug
146
+ // of waiting for this unresolvable result, so now we return
147
+ // undefined to make sure waiters fail, not hang.
148
+ // @ts-ignore
149
+ return undefined as NDKRpcResponse;
150
+ }
151
+
152
+ protected setResponseHandler(id: string, cb?: (res: NDKRpcResponse) => void) {
153
+ let authUrlSent = false;
154
+ const now = Date.now();
155
+ return new Promise<NDKRpcResponse>((resolve, reject) => {
156
+ const responseHandler = (response: NDKRpcResponse) => {
157
+ if (response.result === 'auth_url') {
158
+ this.once(`response-${id}`, responseHandler);
159
+ if (!authUrlSent) {
160
+ authUrlSent = true;
161
+ this.emit('authUrl', response.error);
162
+ }
163
+ } else if (cb) {
164
+ if (this.requests.has(id)) {
165
+ this.requests.delete(id);
166
+ console.log('nostr-login processed nip46 request in', Date.now() - now, 'ms');
167
+ cb(response);
168
+ resolve(response);
169
+ }
170
+ }
171
+ };
172
+
173
+ this.once(`response-${id}`, responseHandler);
174
+
175
+ // timeout
176
+ setTimeout(() => {
177
+ if (this.requests.has(id)) {
178
+ this.requests.delete(id);
179
+ this.removeListener(`response-${id}`, responseHandler);
180
+ reject('Request timed out');
181
+ }
182
+ }, 30000);
183
+ });
184
+ }
185
+
186
+
187
+ protected async createRequestEvent(id: string, remotePubkey: string, method: string, params: string[] = [], kind = 24133) {
188
+ this.requests.add(id);
189
+ const localUser = await this._signer.user();
190
+ const remoteUser = this._ndk.getUser({ pubkey: remotePubkey });
191
+ const request = { id, method, params };
192
+
193
+ const event = new NDKEvent(this._ndk, {
194
+ kind,
195
+ content: JSON.stringify(request),
196
+ tags: [['p', remotePubkey]],
197
+ pubkey: localUser.pubkey,
198
+ } as NostrEvent);
199
+
200
+ const useNip44 = this._useNip44 && method !== 'create_account';
201
+ const encrypt = useNip44 ? this._signer.encryptNip44 : this._signer.encrypt;
202
+ event.content = await encrypt.call(this._signer, remoteUser, event.content);
203
+ await event.sign(this._signer);
204
+
205
+ return event;
206
+ }
207
+ }
208
+
209
+ export class IframeNostrRpc extends NostrRpc {
210
+ private peerOrigin?: string;
211
+ private iframePort?: MessagePort;
212
+ private iframeRequests = new Map<string, { id: string; pubkey: string }>();
213
+
214
+ public constructor(ndk: NDK, localSigner: PrivateKeySigner, iframePeerOrigin?: string) {
215
+ super(ndk, localSigner);
216
+ this._ndk = ndk;
217
+ this.peerOrigin = iframePeerOrigin;
218
+ }
219
+
220
+ public async subscribe(filter: NDKFilter): Promise<NDKSubscription> {
221
+ if (!this.peerOrigin) return super.subscribe(filter);
222
+ return new NDKSubscription(
223
+ this._ndk,
224
+ {},
225
+ {
226
+ // don't send to relay
227
+ closeOnEose: true,
228
+ cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE,
229
+ },
230
+ );
231
+ }
232
+
233
+ public setWorkerIframePort(port: MessagePort) {
234
+ if (!this.peerOrigin) throw new Error('Unexpected iframe port');
235
+
236
+ this.iframePort = port;
237
+
238
+ // to make sure Chrome doesn't terminate the channel
239
+ setInterval(() => {
240
+ console.log('iframe-nip46 ping');
241
+ this.iframePort!.postMessage('ping');
242
+ }, 5000);
243
+
244
+ port.onmessage = async ev => {
245
+ console.log('iframe-nip46 got response', ev.data);
246
+ if (typeof ev.data === 'string' && ev.data.startsWith('errorNoKey')) {
247
+ const event_id = ev.data.split(':')[1];
248
+ const { id = '', pubkey = '' } = this.iframeRequests.get(event_id) || {};
249
+ if (id && pubkey && this.requests.has(id)) this.emit(`iframeRestart-${pubkey}`);
250
+ return;
251
+ }
252
+
253
+ // a copy-paste from rpc.subscribe
254
+ try {
255
+ const event = ev.data;
256
+
257
+ if (!validateEvent(event)) throw new Error('Invalid event from iframe');
258
+ if (!verifySignature(event)) throw new Error('Invalid event signature from iframe');
259
+ const nevent = new NDKEvent(this._ndk, event);
260
+ const parsedEvent = await this.parseEvent(nevent);
261
+ // we're only implementing client-side rpc
262
+ if (!(parsedEvent as NDKRpcRequest).method) {
263
+ console.log('parsed response', parsedEvent);
264
+ this.emit(`response-${parsedEvent.id}`, parsedEvent);
265
+ }
266
+ } catch (e) {
267
+ console.log('error parsing event', e, ev.data);
268
+ }
269
+ };
270
+ }
271
+
272
+ public async sendRequest(remotePubkey: string, method: string, params: string[] = [], kind = 24133, cb?: (res: NDKRpcResponse) => void): Promise<NDKRpcResponse> {
273
+ const id = this.getId();
274
+
275
+ // create and sign request event
276
+ const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
277
+
278
+ // set response handler, it will dedup auth urls,
279
+ // and also dedup response handlers - we're sending
280
+ // to relays and to iframe
281
+ this.setResponseHandler(id, cb);
282
+
283
+ if (this.iframePort) {
284
+ // map request event id to request id, if iframe
285
+ // has no key it will reply with error:event_id (it can't
286
+ // decrypt the request id without keys)
287
+ this.iframeRequests.set(event.id, { id, pubkey: remotePubkey });
288
+
289
+ // send to iframe
290
+ console.log('iframe-nip46 sending request to', this.peerOrigin, event.rawEvent());
291
+ this.iframePort.postMessage(event.rawEvent());
292
+ } else {
293
+ // send to relays
294
+ await event.publish();
295
+ }
296
+
297
+ // see notes in 'super'
298
+ // @ts-ignore
299
+ return undefined as NDKRpcResponse;
300
+ }
301
+ }
302
+
303
+ export class ReadyListener {
304
+ origin: string;
305
+ messages: string[];
306
+ promise: Promise<any>;
307
+
308
+ constructor(messages: string[], origin: string) {
309
+ this.origin = origin;
310
+ this.messages = messages;
311
+ this.promise = new Promise<any>(ok => {
312
+ console.log(new Date(), 'started listener for', this.messages);
313
+
314
+ // ready message handler
315
+ const onReady = async (e: MessageEvent) => {
316
+ const originHostname = new URL(origin!).hostname;
317
+ const messageHostname = new URL(e.origin).hostname;
318
+ // same host or subdomain
319
+ const validHost = messageHostname === originHostname || messageHostname.endsWith('.' + originHostname);
320
+ if (!validHost || !Array.isArray(e.data) || !e.data.length || !this.messages.includes(e.data[0])) {
321
+ // console.log(new Date(), 'got invalid ready message', e.origin, e.data);
322
+ return;
323
+ }
324
+
325
+ console.log(new Date(), 'got ready message from', e.origin, e.data);
326
+ window.removeEventListener('message', onReady);
327
+ ok(e.data);
328
+ };
329
+ window.addEventListener('message', onReady);
330
+ });
331
+ }
332
+
333
+ async wait(): Promise<any> {
334
+ console.log(new Date(), 'waiting for', this.messages);
335
+ const r = await this.promise;
336
+ // NOTE: timer here doesn't help bcs it must be activated when
337
+ // user "confirms", but that's happening on a different
338
+ // origin and we can't really know.
339
+ // await new Promise<any>((ok, err) => {
340
+ // // 10 sec should be more than enough
341
+ // setTimeout(() => err(new Date() + ' timeout for ' + this.message), 10000);
342
+
343
+ // // if promise already resolved or will resolve in the future
344
+ // this.promise.then(ok);
345
+ // });
346
+
347
+ console.log(new Date(), 'finished waiting for', this.messages, r);
348
+ return r;
349
+ }
350
+ }
351
+
352
+ export class Nip46Signer extends NDKNip46Signer {
353
+ private _userPubkey: string = '';
354
+ private _rpc: IframeNostrRpc;
355
+
356
+ constructor(ndk: NDK, localSigner: PrivateKeySigner, signerPubkey: string, iframeOrigin?: string) {
357
+ super(ndk, signerPubkey, localSigner);
358
+
359
+ // override with our own rpc implementation
360
+ this._rpc = new IframeNostrRpc(ndk, localSigner, iframeOrigin);
361
+ this._rpc.setUseNip44(true); // !!this.params.optionsModal.dev);
362
+ this._rpc.on('authUrl', (url: string) => {
363
+ this.emit('authUrl', url);
364
+ });
365
+
366
+ this.rpc = this._rpc;
367
+ }
368
+
369
+ get userPubkey() {
370
+ return this._userPubkey;
371
+ }
372
+
373
+ private async setSignerPubkey(signerPubkey: string, sameAsUser: boolean = false) {
374
+ console.log("setSignerPubkey", signerPubkey);
375
+
376
+ // ensure it's set
377
+ this.remotePubkey = signerPubkey;
378
+
379
+ // when we're sure it's known
380
+ this._rpc.on(`iframeRestart-${signerPubkey}`, () => {
381
+ this.emit('iframeRestart');
382
+ });
383
+
384
+ // now call getPublicKey and swap remotePubkey w/ that
385
+ await this.initUserPubkey(sameAsUser ? signerPubkey : '');
386
+ }
387
+
388
+ public async initUserPubkey(hintPubkey?: string) {
389
+ if (this._userPubkey) throw new Error('Already called initUserPubkey');
390
+
391
+ if (hintPubkey) {
392
+ this._userPubkey = hintPubkey;
393
+ return;
394
+ }
395
+
396
+ this._userPubkey = await new Promise<string>((ok, err) => {
397
+ if (!this.remotePubkey) throw new Error('Signer pubkey not set');
398
+
399
+ console.log("get_public_key", this.remotePubkey);
400
+ this._rpc.sendRequest(this.remotePubkey, 'get_public_key', [], 24133, (response: NDKRpcResponse) => {
401
+ ok(response.result);
402
+ });
403
+ });
404
+ }
405
+
406
+ public async listen(nostrConnectSecret: string) {
407
+ const signerPubkey = await (this.rpc as IframeNostrRpc).listen(nostrConnectSecret);
408
+ await this.setSignerPubkey(signerPubkey);
409
+ }
410
+
411
+ public async connect(token?: string, perms?: string) {
412
+ if (!this.remotePubkey) throw new Error('No signer pubkey');
413
+ await this._rpc.connect(this.remotePubkey, token, perms);
414
+ await this.setSignerPubkey(this.remotePubkey);
415
+ }
416
+
417
+ public async setListenReply(reply: any, nostrConnectSecret: string) {
418
+ const signerPubkey = await this._rpc.parseNostrConnectReply(reply, nostrConnectSecret);
419
+ await this.setSignerPubkey(signerPubkey, true);
420
+ }
421
+
422
+ public async createAccount2({ bunkerPubkey, name, domain, perms = '' }: { bunkerPubkey: string; name: string; domain: string; perms?: string }) {
423
+ const params = [
424
+ name,
425
+ domain,
426
+ '', // email
427
+ perms,
428
+ ];
429
+
430
+ const r = await new Promise<NDKRpcResponse>(ok => {
431
+ this.rpc.sendRequest(bunkerPubkey, 'create_account', params, undefined, ok);
432
+ });
433
+
434
+ console.log('create_account pubkey', r);
435
+ if (r.result === 'error') {
436
+ throw new Error(r.error);
437
+ }
438
+
439
+ return r.result;
440
+ }
441
+ }
@@ -0,0 +1,107 @@
1
+ import { Info } from 'nostr-login-components';
2
+
3
+ export interface Signer {
4
+ signEvent: (event: any) => Promise<any>;
5
+ nip04: {
6
+ encrypt: (pubkey: string, plaintext: string) => Promise<string>;
7
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
8
+ };
9
+ nip44: {
10
+ encrypt: (pubkey: string, plaintext: string) => Promise<string>;
11
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
12
+ };
13
+ }
14
+
15
+ export interface NostrObjectParams {
16
+ waitReady(): Promise<void>;
17
+ getUserInfo(): Info | null;
18
+ launch(): Promise<void>;
19
+ getSigner(): Signer;
20
+ wait<T>(cb: () => Promise<T>): Promise<T>;
21
+ }
22
+
23
+ class Nostr {
24
+ #params: NostrObjectParams;
25
+ private nip04: {
26
+ encrypt: (pubkey: string, plaintext: string) => Promise<any>;
27
+ decrypt: (pubkey: string, ciphertext: string) => Promise<any>;
28
+ };
29
+ private nip44: {
30
+ encrypt: (pubkey: string, plaintext: string) => Promise<any>;
31
+ decrypt: (pubkey: string, ciphertext: string) => Promise<any>;
32
+ };
33
+
34
+ constructor(params: NostrObjectParams) {
35
+ this.#params = params;
36
+
37
+ this.getPublicKey = this.getPublicKey.bind(this);
38
+ this.signEvent = this.signEvent.bind(this);
39
+ this.getRelays = this.getRelays.bind(this);
40
+ this.nip04 = {
41
+ encrypt: this.encrypt04.bind(this),
42
+ decrypt: this.decrypt04.bind(this),
43
+ };
44
+ this.nip44 = {
45
+ encrypt: this.encrypt44.bind(this),
46
+ decrypt: this.decrypt44.bind(this),
47
+ };
48
+ }
49
+
50
+ private async ensureAuth() {
51
+ await this.#params.waitReady();
52
+
53
+ // authed?
54
+ if (this.#params.getUserInfo()) return;
55
+
56
+ // launch auth flow
57
+ await this.#params.launch();
58
+
59
+ // give up
60
+ if (!this.#params.getUserInfo()) {
61
+ throw new Error('Rejected by user');
62
+ }
63
+ }
64
+
65
+ async getPublicKey() {
66
+ await this.ensureAuth();
67
+ const userInfo = this.#params.getUserInfo();
68
+ if (userInfo) {
69
+ return userInfo.pubkey;
70
+ } else {
71
+ throw new Error('No user');
72
+ }
73
+ }
74
+
75
+ // @ts-ignore
76
+ async signEvent(event) {
77
+ await this.ensureAuth();
78
+ return this.#params.wait(async () => await this.#params.getSigner().signEvent(event));
79
+ }
80
+
81
+ async getRelays() {
82
+ // FIXME implement!
83
+ return {};
84
+ }
85
+
86
+ async encrypt04(pubkey: string, plaintext: string) {
87
+ await this.ensureAuth();
88
+ return this.#params.wait(async () => await this.#params.getSigner().nip04.encrypt(pubkey, plaintext));
89
+ }
90
+
91
+ async decrypt04(pubkey: string, ciphertext: string) {
92
+ await this.ensureAuth();
93
+ return this.#params.wait(async () => await this.#params.getSigner().nip04.decrypt(pubkey, ciphertext));
94
+ }
95
+
96
+ async encrypt44(pubkey: string, plaintext: string) {
97
+ await this.ensureAuth();
98
+ return this.#params.wait(async () => await this.#params.getSigner().nip44.encrypt(pubkey, plaintext));
99
+ }
100
+
101
+ async decrypt44(pubkey: string, ciphertext: string) {
102
+ await this.ensureAuth();
103
+ return this.#params.wait(async () => await this.#params.getSigner().nip44.decrypt(pubkey, ciphertext));
104
+ }
105
+ }
106
+
107
+ export default Nostr;
@@ -0,0 +1,99 @@
1
+ import { Nostr, NostrParams } from './';
2
+ import { EventEmitter } from 'tseep';
3
+
4
+ class NostrExtensionService extends EventEmitter {
5
+ private params: NostrParams;
6
+ private nostrExtension: any | undefined;
7
+
8
+ constructor(params: NostrParams) {
9
+ super();
10
+ this.params = params;
11
+ }
12
+
13
+ public startCheckingExtension(nostr: Nostr) {
14
+ if (this.checkExtension(nostr)) return;
15
+
16
+ // watch out for extension trying to overwrite us
17
+ const to = setInterval(() => {
18
+ if (this.checkExtension(nostr)) clearTimeout(to);
19
+ }, 100);
20
+ }
21
+
22
+ private checkExtension(nostr: Nostr) {
23
+ // @ts-ignore
24
+ if (!this.nostrExtension && window.nostr && window.nostr !== nostr) {
25
+ this.initExtension(nostr);
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+
31
+ private async initExtension(nostr: Nostr, lastTry?: boolean) {
32
+ // @ts-ignore
33
+ this.nostrExtension = window.nostr;
34
+ // @ts-ignore
35
+ window.nostr = nostr;
36
+ // we're signed in with extesions? well execute that
37
+ if (this.params.userInfo?.authMethod === 'extension') {
38
+ await this.trySetExtensionForPubkey(this.params.userInfo.pubkey);
39
+ }
40
+
41
+ // schedule another check
42
+ if (!lastTry) {
43
+ setTimeout(() => {
44
+ // NOTE: we can't know if user has >1 extension and thus
45
+ // if the current one we detected is the actual 'last one'
46
+ // that will set the window.nostr. So the simplest
47
+ // solution is to wait a bit more, hoping that if one
48
+ // extension started then the rest are likely to start soon,
49
+ // and then just capture the most recent one
50
+
51
+ // @ts-ignore
52
+ if (window.nostr !== nostr && this.nostrExtension !== window.nostr) {
53
+ this.initExtension(nostr, true);
54
+ }
55
+ }, 300);
56
+ }
57
+
58
+ // in the worst case of app saving the nostrExtension reference
59
+ // it will be calling it directly, not a big deal
60
+ }
61
+
62
+ private async setExtensionReadPubkey(expectedPubkey?: string) {
63
+ window.nostr = this.nostrExtension;
64
+ // @ts-ignore
65
+ const pubkey = await window.nostr.getPublicKey();
66
+ if (expectedPubkey && expectedPubkey !== pubkey) {
67
+ this.emit('extensionLogout');
68
+ } else {
69
+ this.emit('extensionLogin', pubkey);
70
+ }
71
+ }
72
+
73
+ public async trySetExtensionForPubkey(expectedPubkey: string) {
74
+ if (this.nostrExtension) {
75
+ return this.setExtensionReadPubkey(expectedPubkey);
76
+ }
77
+ }
78
+
79
+ public async setExtension() {
80
+ return this.setExtensionReadPubkey();
81
+ }
82
+
83
+ public unsetExtension(nostr: Nostr) {
84
+ if (window.nostr === this.nostrExtension) {
85
+ // @ts-ignore
86
+ window.nostr = nostr;
87
+ }
88
+ }
89
+
90
+ public getExtension() {
91
+ return this.nostrExtension;
92
+ }
93
+
94
+ public hasExtension() {
95
+ return !!this.nostrExtension;
96
+ }
97
+ }
98
+
99
+ export default NostrExtensionService;
@@ -0,0 +1,18 @@
1
+ import { Info } from 'nostr-login-components';
2
+ import { NostrLoginOptions } from '../types';
3
+
4
+ class NostrParams {
5
+ public userInfo: Info | null;
6
+ public optionsModal: NostrLoginOptions;
7
+ constructor() {
8
+ this.userInfo = null;
9
+
10
+ this.optionsModal = {
11
+ theme: 'default',
12
+ startScreen: 'welcome',
13
+ devOverrideBunkerOrigin: '',
14
+ };
15
+ }
16
+ }
17
+
18
+ export default NostrParams;
@@ -0,0 +1,27 @@
1
+ class Popup {
2
+ private popup: Window | null = null;
3
+
4
+ constructor() {}
5
+
6
+ public openPopup(url: string) {
7
+ // user might have closed it already
8
+ if (!this.popup || this.popup.closed) {
9
+ // NOTE: do not set noreferrer, bunker might use referrer to
10
+ // simplify the naming of the connected app.
11
+ // NOTE: do not pass noopener, otherwise null is returned
12
+ this.popup = window.open(url, '_blank', 'width=400,height=700');
13
+ console.log('popup', this.popup);
14
+ if (!this.popup) throw new Error('Popup blocked. Try again, please!');
15
+ }
16
+ }
17
+
18
+ public closePopup() {
19
+ // make sure we release the popup
20
+ try {
21
+ this.popup?.close();
22
+ this.popup = null;
23
+ } catch {}
24
+ }
25
+ }
26
+
27
+ export default Popup;