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