@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.
- package/.prettierrc.json +13 -0
- package/README.md +167 -0
- package/dist/const/index.d.ts +1 -0
- package/dist/iife-module.d.ts +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.esm.js +18 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/modules/AuthNostrService.d.ts +84 -0
- package/dist/modules/BannerManager.d.ts +20 -0
- package/dist/modules/ModalManager.d.ts +25 -0
- package/dist/modules/Nip46.d.ts +56 -0
- package/dist/modules/Nostr.d.ts +34 -0
- package/dist/modules/NostrExtensionService.d.ts +17 -0
- package/dist/modules/NostrParams.d.ts +8 -0
- package/dist/modules/Popup.d.ts +7 -0
- package/dist/modules/ProcessManager.d.ts +10 -0
- package/dist/modules/Signer.d.ts +9 -0
- package/dist/modules/index.d.ts +8 -0
- package/dist/types.d.ts +72 -0
- package/dist/unpkg.js +17 -0
- package/dist/utils/index.d.ts +27 -0
- package/dist/utils/nip44.d.ts +9 -0
- package/index.html +30 -0
- package/package.json +28 -0
- package/rollup.config.js +55 -0
- package/src/const/index.ts +1 -0
- package/src/iife-module.ts +81 -0
- package/src/index.ts +347 -0
- package/src/modules/AuthNostrService.ts +756 -0
- package/src/modules/BannerManager.ts +146 -0
- package/src/modules/ModalManager.ts +635 -0
- package/src/modules/Nip46.ts +441 -0
- package/src/modules/Nostr.ts +107 -0
- package/src/modules/NostrExtensionService.ts +99 -0
- package/src/modules/NostrParams.ts +18 -0
- package/src/modules/Popup.ts +27 -0
- package/src/modules/ProcessManager.ts +67 -0
- package/src/modules/Signer.ts +25 -0
- package/src/modules/index.ts +8 -0
- package/src/types.ts +124 -0
- package/src/utils/index.ts +326 -0
- package/src/utils/nip44.ts +185 -0
- 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;
|