@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,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;
|