@konemono/nostr-login 1.7.69 → 1.8.0
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/index.esm.js +10 -12
- package/dist/index.esm.js.map +1 -1
- package/dist/modules/AmberDirectSigner.d.ts +1 -0
- package/dist/modules/AuthNostrService.d.ts +5 -5
- package/dist/modules/Nip46.d.ts +47 -31
- package/dist/modules/NostrExtensionService.d.ts +5 -0
- package/dist/modules/Signer.d.ts +11 -4
- package/dist/modules/nip46/Nip46Adapter.d.ts +26 -0
- package/dist/modules/nip46/Nip46Client.d.ts +52 -0
- package/dist/modules/nip46/types.d.ts +24 -0
- package/dist/unpkg.js +10 -12
- package/dist/utils/index.d.ts +2 -3
- package/package.json +7 -4
- package/src/index.ts +1 -1
- package/src/modules/AmberDirectSigner.ts +204 -147
- package/src/modules/AuthNostrService.ts +151 -122
- package/src/modules/Nip46.iframe.test.ts +124 -0
- package/src/modules/Nip46.test.ts +31 -0
- package/src/modules/Nip46.ts +212 -261
- package/src/modules/NostrExtensionService.ts +6 -0
- package/src/modules/Signer.ts +39 -8
- package/src/modules/nip46/Nip46Adapter.ts +123 -0
- package/src/modules/nip46/Nip46Client.ts +248 -0
- package/src/modules/nip46/types.ts +26 -0
- package/src/utils/index.ts +67 -21
- package/tsconfig.json +2 -2
- package/vitest.config.ts +9 -0
|
@@ -2,17 +2,17 @@ import { localStorageAddAccount, bunkerUrlToInfo, isBunkerUrl, fetchProfile, get
|
|
|
2
2
|
import { ConnectionString, Info } from 'nostr-login-components/dist/types/types';
|
|
3
3
|
import { generatePrivateKey, getEventHash, getPublicKey, nip19 } from 'nostr-tools';
|
|
4
4
|
import { NostrLoginAuthOptions, Response } from '../types';
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import { NostrParams } from './';
|
|
7
7
|
import { EventEmitter } from 'tseep';
|
|
8
8
|
import { Signer } from './Nostr';
|
|
9
9
|
import { Nip44 } from '../utils/nip44';
|
|
10
|
-
import { IframeNostrRpc, Nip46Signer, ReadyListener } from './Nip46';
|
|
10
|
+
import { IframeNostrRpc, Nip46Signer, ReadyListener, RpcResponse } from './Nip46';
|
|
11
|
+
import { Nip46Client } from './nip46/Nip46Client';
|
|
12
|
+
import { Nip46Adapter } from './nip46/Nip46Adapter';
|
|
11
13
|
import { PrivateKeySigner } from './Signer';
|
|
12
14
|
import { AmberDirectSigner } from './AmberDirectSigner';
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
16
|
const OUTBOX_RELAYS = ['wss://user.kindpag.es', 'wss://purplepag.es', 'wss://relay.nos.social'];
|
|
17
17
|
const DEFAULT_NOSTRCONNECT_RELAYS = ['wss://relay.nsec.app/', 'wss://ephemeral.snowflare.cc/'];
|
|
18
18
|
const CONNECT_TIMEOUT = 5000;
|
|
@@ -40,9 +40,7 @@ const NOSTRCONNECT_APPS: ConnectionString[] = [
|
|
|
40
40
|
];
|
|
41
41
|
|
|
42
42
|
class AuthNostrService extends EventEmitter implements Signer {
|
|
43
|
-
private
|
|
44
|
-
private profileNdk: NDK;
|
|
45
|
-
private signer: Nip46Signer | null = null;
|
|
43
|
+
private signer: any = null;
|
|
46
44
|
private amberSigner: AmberDirectSigner | null = null;
|
|
47
45
|
private localSigner: PrivateKeySigner | null = null;
|
|
48
46
|
private params: NostrParams;
|
|
@@ -68,15 +66,6 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
68
66
|
constructor(params: NostrParams) {
|
|
69
67
|
super();
|
|
70
68
|
this.params = params;
|
|
71
|
-
this.ndk = new NDK({
|
|
72
|
-
enableOutboxModel: false,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
this.profileNdk = new NDK({
|
|
76
|
-
enableOutboxModel: true,
|
|
77
|
-
explicitRelayUrls: OUTBOX_RELAYS,
|
|
78
|
-
});
|
|
79
|
-
this.profileNdk.connect(CONNECT_TIMEOUT);
|
|
80
69
|
|
|
81
70
|
this.nip04 = {
|
|
82
71
|
encrypt: this.encrypt04.bind(this),
|
|
@@ -102,57 +91,79 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
102
91
|
|
|
103
92
|
// Periodic check as a safety net
|
|
104
93
|
setInterval(check, 1000);
|
|
94
|
+
|
|
95
|
+
window.addEventListener('message', event => {
|
|
96
|
+
if (event.data && event.data.method === 'amberResponse') {
|
|
97
|
+
const { id, type, result } = event.data;
|
|
98
|
+
console.log('Amber response received via message', { id, type, result });
|
|
99
|
+
this.handleAmberResponse({ id, type, result });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
private checkAmberResponse() {
|
|
108
105
|
const response = AmberDirectSigner.parseResponse();
|
|
109
106
|
if (response) {
|
|
110
|
-
|
|
107
|
+
// If we have an opener and it's not the same window, we are in a popup
|
|
108
|
+
if (window.opener && window.opener !== window) {
|
|
109
|
+
console.log('Amber response in popup, sending back to opener');
|
|
110
|
+
window.opener.postMessage({ method: 'amberResponse', ...response }, window.location.origin);
|
|
111
|
+
window.close();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.handleAmberResponse(response);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
this.emit('onAuthUrl', { url: '' });
|
|
119
|
+
private handledAmberIds: Set<string> = new Set();
|
|
114
120
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
console.log('Resolved pending Amber promise via resolvePending');
|
|
119
|
-
}
|
|
121
|
+
private handleAmberResponse(response: { id: string; type: string; result: string }) {
|
|
122
|
+
if (this.handledAmberIds.has(response.id)) return;
|
|
123
|
+
this.handledAmberIds.add(response.id);
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
const info: Info = {
|
|
123
|
-
pubkey: response.result,
|
|
124
|
-
name: nip19.npubEncode(response.result),
|
|
125
|
-
authMethod: 'amber' as any,
|
|
126
|
-
relays: [],
|
|
127
|
-
signerPubkey: '',
|
|
128
|
-
};
|
|
129
|
-
console.log('Amber login success', info);
|
|
130
|
-
this.onAuth('login', info);
|
|
131
|
-
} else {
|
|
132
|
-
// ★ 不要: この部分は削除または簡略化
|
|
133
|
-
// NIP-55では直接resultを使用するため、追加のキャッシュは不要
|
|
134
|
-
}
|
|
125
|
+
console.log('Handling Amber response', response);
|
|
135
126
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
let changed = false;
|
|
139
|
-
if (url.searchParams.has('event')) {
|
|
140
|
-
url.searchParams.delete('event');
|
|
141
|
-
changed = true;
|
|
142
|
-
}
|
|
127
|
+
// Stop the "Connecting..." spinner
|
|
128
|
+
this.emit('onAuthUrl', { url: '' });
|
|
143
129
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
changed = true;
|
|
150
|
-
}
|
|
130
|
+
// Resolve pending promises if any (for non-reload cases)
|
|
131
|
+
const resolved = AmberDirectSigner.resolvePending(response.id, response.type, response.result);
|
|
132
|
+
if (resolved) {
|
|
133
|
+
console.log('Resolved pending Amber promise via resolvePending');
|
|
134
|
+
}
|
|
151
135
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
136
|
+
if (response.type === 'get_public_key' || response.type.includes('pub')) {
|
|
137
|
+
const info: Info = {
|
|
138
|
+
pubkey: response.result,
|
|
139
|
+
name: nip19.npubEncode(response.result),
|
|
140
|
+
authMethod: 'amber' as any,
|
|
141
|
+
relays: [],
|
|
142
|
+
signerPubkey: '',
|
|
143
|
+
};
|
|
144
|
+
console.log('Amber login success', info);
|
|
145
|
+
this.onAuth('login', info);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// URLクリーンアップをより徹底的に
|
|
149
|
+
const url = new URL(window.location.href);
|
|
150
|
+
let changed = false;
|
|
151
|
+
if (url.searchParams.has('event')) {
|
|
152
|
+
url.searchParams.delete('event');
|
|
153
|
+
changed = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// パス末尾が結果と一致する場合はパスもクリア
|
|
157
|
+
const pathParts = url.pathname.split('/');
|
|
158
|
+
if (pathParts.length > 0 && pathParts[pathParts.length - 1] === response.result) {
|
|
159
|
+
pathParts.pop();
|
|
160
|
+
url.pathname = pathParts.join('/') || '/';
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (changed) {
|
|
165
|
+
console.log('Cleaning up Amber response URL', url.toString());
|
|
166
|
+
window.history.replaceState({}, '', url.toString());
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
169
|
|
|
@@ -164,13 +175,13 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
164
175
|
if (this.signerPromise) {
|
|
165
176
|
try {
|
|
166
177
|
await this.signerPromise;
|
|
167
|
-
} catch {
|
|
178
|
+
} catch {}
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
if (this.readyPromise) {
|
|
171
182
|
try {
|
|
172
183
|
await this.readyPromise;
|
|
173
|
-
} catch {
|
|
184
|
+
} catch {}
|
|
174
185
|
}
|
|
175
186
|
}
|
|
176
187
|
|
|
@@ -192,10 +203,9 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
192
203
|
importConnect?: boolean;
|
|
193
204
|
iframeUrl?: string;
|
|
194
205
|
} = {},
|
|
195
|
-
) {
|
|
206
|
+
): Promise<Info> {
|
|
196
207
|
relays = relays && relays.length > 0 ? relays : DEFAULT_NOSTRCONNECT_RELAYS;
|
|
197
208
|
|
|
198
|
-
|
|
199
209
|
const info: Info = {
|
|
200
210
|
authMethod: 'connect',
|
|
201
211
|
pubkey: '', // unknown yet!
|
|
@@ -216,20 +226,34 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
216
226
|
|
|
217
227
|
const id = Math.random().toString(36).substring(7);
|
|
218
228
|
const url = signer.generateUrl('', 'get_public_key', id);
|
|
219
|
-
this.emit('onAuthUrl', { url });
|
|
220
|
-
|
|
221
|
-
const pubkey = await signer.getPublicKey(id);
|
|
222
229
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
name: nip19.npubEncode(pubkey),
|
|
226
|
-
authMethod: 'amber' as any,
|
|
227
|
-
relays: [],
|
|
228
|
-
signerPubkey: '',
|
|
229
|
-
};
|
|
230
|
+
// Emit for the "Connecting..." spinner
|
|
231
|
+
this.emit('onAuthUrl', { url });
|
|
230
232
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
try {
|
|
234
|
+
const pubkey = await signer.getPublicKey(id);
|
|
235
|
+
const info: Info = {
|
|
236
|
+
pubkey,
|
|
237
|
+
name: nip19.npubEncode(pubkey),
|
|
238
|
+
authMethod: 'amber' as any,
|
|
239
|
+
relays: [],
|
|
240
|
+
signerPubkey: '',
|
|
241
|
+
};
|
|
242
|
+
this.onAuth('login', info);
|
|
243
|
+
return info;
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.log('Amber getPublicKey failed or was blocked', e);
|
|
246
|
+
// Fallback: wait for onAuth to be called (e.g. via user clicking Continue)
|
|
247
|
+
return new Promise(resolve => {
|
|
248
|
+
const handler = (info: Info | null) => {
|
|
249
|
+
if (info && info.authMethod === ('amber' as any)) {
|
|
250
|
+
this.off('onUserInfo', handler); // Use onUserInfo as a proxy for onAuth
|
|
251
|
+
resolve(info);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
this.on('onUserInfo', handler);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
233
257
|
}
|
|
234
258
|
window.open(link, '_blank', 'width=400,height=700');
|
|
235
259
|
}
|
|
@@ -267,7 +291,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
267
291
|
perms: encodeURIComponent(this.params.optionsModal.perms || ''),
|
|
268
292
|
};
|
|
269
293
|
|
|
270
|
-
return `nostrconnect://${pubkey}?image=${meta.icon}&url=${meta.url}&name=${meta.name}&perms=${meta.perms}&secret=${this.nostrConnectSecret}${(relays || []).length > 0 ? (relays || []).map((r, i) => `&relay=${r}`) :
|
|
294
|
+
return `nostrconnect://${pubkey}?image=${meta.icon}&url=${meta.url}&name=${meta.name}&perms=${meta.perms}&secret=${this.nostrConnectSecret}${(relays || []).length > 0 ? (relays || []).map((r, i) => `&relay=${r}`) : ''}`;
|
|
271
295
|
}
|
|
272
296
|
|
|
273
297
|
public async getNostrConnectServices(): Promise<[string, ConnectionString[]]> {
|
|
@@ -341,7 +365,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
341
365
|
this.releaseSigner();
|
|
342
366
|
this.localSigner = new PrivateKeySigner(info.sk!);
|
|
343
367
|
|
|
344
|
-
if (signup) await createProfile(info, this.
|
|
368
|
+
if (signup) await createProfile(info, this.localSigner, this.params.optionsModal.signupRelays, this.params.optionsModal.outboxRelays);
|
|
345
369
|
|
|
346
370
|
this.onAuth(signup ? 'signup' : 'login', info);
|
|
347
371
|
}
|
|
@@ -419,9 +443,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
419
443
|
const userPubkey = await this.signer!.createAccount2({ bunkerPubkey: info.signerPubkey!, name, domain, perms: this.params.optionsModal.perms });
|
|
420
444
|
|
|
421
445
|
return {
|
|
422
|
-
bunkerUrl:
|
|
423
|
-
`bunker://${userPubkey}?` +
|
|
424
|
-
(info.relays ?? []).map((r: string) => `relay=${encodeURIComponent(r)}`).join('&'),
|
|
446
|
+
bunkerUrl: `bunker://${userPubkey}?` + (info.relays ?? []).map((r: string) => `relay=${encodeURIComponent(r)}`).join('&'),
|
|
425
447
|
sk: info.sk,
|
|
426
448
|
};
|
|
427
449
|
}
|
|
@@ -431,11 +453,6 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
431
453
|
this.signerErrCallback?.('cancelled');
|
|
432
454
|
this.localSigner = null;
|
|
433
455
|
this.amberSigner = null;
|
|
434
|
-
|
|
435
|
-
// disconnect from signer relays
|
|
436
|
-
for (const r of this.ndk.pool.relays.keys()) {
|
|
437
|
-
this.ndk.pool.removeRelay(r);
|
|
438
|
-
}
|
|
439
456
|
}
|
|
440
457
|
|
|
441
458
|
public async logout(keepSigner = false) {
|
|
@@ -473,14 +490,14 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
473
490
|
if (info && this.params.userInfo && (info.pubkey !== this.params.userInfo.pubkey || info.authMethod !== this.params.userInfo.authMethod)) {
|
|
474
491
|
const event = new CustomEvent('nlAuth', { detail: { type: 'logout' } });
|
|
475
492
|
console.log('nostr-login auth', event.detail);
|
|
476
|
-
document.dispatchEvent(event)
|
|
493
|
+
document.dispatchEvent(event);
|
|
477
494
|
}
|
|
478
495
|
|
|
479
496
|
this.setUserInfo(info);
|
|
480
497
|
|
|
481
498
|
if (info) {
|
|
482
499
|
// async profile fetch
|
|
483
|
-
fetchProfile(info,
|
|
500
|
+
fetchProfile(info, info.relays).then(p => {
|
|
484
501
|
if (this.params.userInfo !== info) return;
|
|
485
502
|
|
|
486
503
|
const userInfo = {
|
|
@@ -610,7 +627,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
610
627
|
}
|
|
611
628
|
|
|
612
629
|
public startAuth() {
|
|
613
|
-
console.log(
|
|
630
|
+
console.log('startAuth');
|
|
614
631
|
if (this.readyCallback) throw new Error('Already started');
|
|
615
632
|
|
|
616
633
|
// start the new promise
|
|
@@ -654,7 +671,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
654
671
|
if (this.signerPromise) {
|
|
655
672
|
try {
|
|
656
673
|
await this.signerPromise;
|
|
657
|
-
} catch {
|
|
674
|
+
} catch {}
|
|
658
675
|
}
|
|
659
676
|
|
|
660
677
|
// we remove support for iframe from nip05 and bunker-url methods,
|
|
@@ -673,39 +690,44 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
673
690
|
this.signerPromise = new Promise<void>(async (ok, err) => {
|
|
674
691
|
this.signerErrCallback = err;
|
|
675
692
|
try {
|
|
676
|
-
// pre-connect if we're creating the connection (listen|connect) or
|
|
677
|
-
// not iframe mode
|
|
678
|
-
if (info.relays && !info.iframeUrl) {
|
|
679
|
-
for (const r of info.relays) {
|
|
680
|
-
this.ndk.addExplicitRelay(r, undefined);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// wait until we connect, otherwise
|
|
685
|
-
// signer won't start properly
|
|
686
|
-
await this.ndk.connect(CONNECT_TIMEOUT);
|
|
687
|
-
|
|
688
693
|
// create and prepare the signer
|
|
689
694
|
const localSigner = new PrivateKeySigner(info.sk!);
|
|
690
|
-
this.signer = new Nip46Signer(this.ndk, localSigner, info.signerPubkey!, iframeOrigin);
|
|
691
695
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const iframeUrl = info.iframeUrl + (info.iframeUrl!.includes('?') ? '&' : '?') + 'pubkey=' + info.pubkey + '&rebind=' + localSigner.pubkey;
|
|
696
|
-
this.emit('iframeRestart', { pubkey: info.pubkey, iframeUrl });
|
|
697
|
-
});
|
|
696
|
+
if (info.iframeUrl) {
|
|
697
|
+
// use NDK-free iframe signer implementation (MessagePort + SimplePool)
|
|
698
|
+
this.signer = new Nip46Signer(localSigner, info.signerPubkey!, iframeOrigin, info.relays || []);
|
|
698
699
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
700
|
+
// we should notify the banner the same way as the onAuthUrl does
|
|
701
|
+
this.signer.on(`iframeRestart`, async () => {
|
|
702
|
+
const iframeUrl = info.iframeUrl + (info.iframeUrl!.includes('?') ? '&' : '?') + 'pubkey=' + info.pubkey + '&rebind=' + localSigner.pubkey;
|
|
703
|
+
this.emit('iframeRestart', { pubkey: info.pubkey, iframeUrl });
|
|
704
|
+
});
|
|
703
705
|
|
|
704
|
-
//
|
|
705
|
-
this.
|
|
706
|
-
|
|
707
|
-
// }
|
|
706
|
+
// OAuth flow
|
|
707
|
+
this.signer.on('authUrl', (url: string) => {
|
|
708
|
+
console.log('nostr login auth url', url);
|
|
708
709
|
|
|
710
|
+
// notify our UI
|
|
711
|
+
this.emit('onAuthUrl', { url, iframeUrl: info.iframeUrl, eventToAddAccount });
|
|
712
|
+
});
|
|
713
|
+
} else {
|
|
714
|
+
// New SimplePool-based NIP-46 flow
|
|
715
|
+
const client = new Nip46Client({
|
|
716
|
+
localPrivateKey: info.sk!,
|
|
717
|
+
remotePubkey: info.signerPubkey!,
|
|
718
|
+
relays: info.relays || [],
|
|
719
|
+
timeoutMs: 30000,
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const adapter = new Nip46Adapter(client, localSigner);
|
|
723
|
+
this.signer = adapter;
|
|
724
|
+
|
|
725
|
+
// OAuth flow: forward authUrl events
|
|
726
|
+
this.signer.on('authUrl', (url: string) => {
|
|
727
|
+
console.log('nostr login auth url', url);
|
|
728
|
+
this.emit('onAuthUrl', { url, iframeUrl: info.iframeUrl, eventToAddAccount });
|
|
729
|
+
});
|
|
730
|
+
}
|
|
709
731
|
if (listen) {
|
|
710
732
|
// nostrconnect: flow
|
|
711
733
|
// wait for the incoming message from signer
|
|
@@ -803,7 +825,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
803
825
|
|
|
804
826
|
private async codec_call(method: string, pubkey: string, param: string) {
|
|
805
827
|
return new Promise<string>((resolve, reject) => {
|
|
806
|
-
this.signer!.rpc.sendRequest(this.signer!.remotePubkey!, method, [pubkey, param], 24133, (response:
|
|
828
|
+
this.signer!.rpc.sendRequest(this.signer!.remotePubkey!, method, [pubkey, param], 24133, (response: RpcResponse) => {
|
|
807
829
|
if (!response.error) {
|
|
808
830
|
resolve(response.result);
|
|
809
831
|
} else {
|
|
@@ -815,28 +837,35 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
815
837
|
|
|
816
838
|
public async encrypt04(pubkey: string, plaintext: string) {
|
|
817
839
|
if (this.localSigner) {
|
|
818
|
-
return this.localSigner.encrypt(
|
|
840
|
+
return this.localSigner.encrypt(pubkey, plaintext);
|
|
819
841
|
} else if (this.params.userInfo?.authMethod === ('amber' as any)) {
|
|
820
842
|
const userInfo = this.params.userInfo!;
|
|
821
843
|
if (!this.amberSigner) this.amberSigner = new AmberDirectSigner(userInfo.pubkey);
|
|
822
844
|
return this.amberSigner.encrypt04(pubkey, plaintext);
|
|
823
845
|
} else {
|
|
824
|
-
|
|
846
|
+
// adapter supports encrypt(pubkey, plaintext)
|
|
847
|
+
if (this.signer && typeof this.signer.encrypt === 'function') {
|
|
848
|
+
return this.signer.encrypt(pubkey, plaintext);
|
|
849
|
+
}
|
|
850
|
+
// fallback to remote codec via signer RPC
|
|
851
|
+
return this.codec_call('nip04_encrypt', pubkey, plaintext);
|
|
825
852
|
}
|
|
826
853
|
}
|
|
827
854
|
|
|
828
855
|
public async decrypt04(pubkey: string, ciphertext: string) {
|
|
829
856
|
if (this.localSigner) {
|
|
830
|
-
return this.localSigner.decrypt(
|
|
857
|
+
return this.localSigner.decrypt(pubkey, ciphertext);
|
|
831
858
|
} else if (this.params.userInfo?.authMethod === ('amber' as any)) {
|
|
832
859
|
const userInfo = this.params.userInfo!;
|
|
833
860
|
if (!this.amberSigner) this.amberSigner = new AmberDirectSigner(userInfo.pubkey);
|
|
834
861
|
return this.amberSigner.decrypt04(pubkey, ciphertext);
|
|
835
862
|
} else {
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
863
|
+
// If signer supports direct decrypt(pubkey, ciphertext), use it
|
|
864
|
+
if (this.signer && typeof this.signer.decrypt === 'function') {
|
|
865
|
+
return this.signer.decrypt(pubkey, ciphertext);
|
|
866
|
+
}
|
|
839
867
|
|
|
868
|
+
// fallback to remote codec via signer RPC
|
|
840
869
|
return this.codec_call('nip04_decrypt', pubkey, ciphertext);
|
|
841
870
|
}
|
|
842
871
|
}
|
|
@@ -849,7 +878,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
849
878
|
if (!this.amberSigner) this.amberSigner = new AmberDirectSigner(userInfo.pubkey);
|
|
850
879
|
return this.amberSigner.encrypt44(pubkey, plaintext);
|
|
851
880
|
} else {
|
|
852
|
-
// no support of nip44 in
|
|
881
|
+
// no support of nip44 in legacy signer implementation
|
|
853
882
|
return this.codec_call('nip44_encrypt', pubkey, plaintext);
|
|
854
883
|
}
|
|
855
884
|
}
|
|
@@ -862,7 +891,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
862
891
|
if (!this.amberSigner) this.amberSigner = new AmberDirectSigner(userInfo.pubkey);
|
|
863
892
|
return this.amberSigner.decrypt44(pubkey, ciphertext);
|
|
864
893
|
} else {
|
|
865
|
-
// no support of nip44 in
|
|
894
|
+
// no support of nip44 in legacy signer implementation
|
|
866
895
|
return this.codec_call('nip44_decrypt', pubkey, ciphertext);
|
|
867
896
|
}
|
|
868
897
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { IframeNostrRpc } from './Nip46';
|
|
3
|
+
import { PrivateKeySigner } from './Signer';
|
|
4
|
+
import { generatePrivateKey, validateEvent, verifySignature } from 'nostr-tools';
|
|
5
|
+
|
|
6
|
+
describe('IframeNostrRpc integration', () => {
|
|
7
|
+
it('roundtrips request/response via MessagePort', async () => {
|
|
8
|
+
const localSk = generatePrivateKey();
|
|
9
|
+
const remoteSk = generatePrivateKey();
|
|
10
|
+
const localSigner = new PrivateKeySigner(localSk);
|
|
11
|
+
const remoteSigner = new PrivateKeySigner(remoteSk);
|
|
12
|
+
|
|
13
|
+
const rpc = new IframeNostrRpc(localSigner, 'https://example.com', ['wss://relay.example']);
|
|
14
|
+
|
|
15
|
+
// mock MessagePort
|
|
16
|
+
const port: any = {
|
|
17
|
+
onmessage: undefined as any,
|
|
18
|
+
postMessage: (msg: any) => {
|
|
19
|
+
// ignore ping messages from keepalive
|
|
20
|
+
if (typeof msg === 'string') return;
|
|
21
|
+
// log for debugging
|
|
22
|
+
console.log('[test] postMessage received', typeof msg, msg && typeof msg === 'object' && msg.pubkey);
|
|
23
|
+
console.log('[test] msg.id', msg && msg.id, 'kind', msg && msg.kind, 'contentType', typeof (msg && msg.content), 'contentLen', msg && msg.content && msg.content.length);
|
|
24
|
+
// simulate remote iframe processing (defer to avoid race with request setup)
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
(async () => {
|
|
27
|
+
try {
|
|
28
|
+
// remote decrypts request (sender pubkey is msg.pubkey)
|
|
29
|
+
const decrypted = await remoteSigner.decrypt(msg.pubkey, msg.content);
|
|
30
|
+
console.log('[test] decrypted raw', typeof decrypted, decrypted && decrypted.slice ? decrypted.slice(0, 120) : decrypted);
|
|
31
|
+
const req = JSON.parse(decrypted);
|
|
32
|
+
console.log('[test] parsed request id', req.id, 'method', req.method);
|
|
33
|
+
|
|
34
|
+
const response = { id: req.id, result: 'ack' };
|
|
35
|
+
const content = await remoteSigner.encrypt(msg.pubkey, JSON.stringify(response));
|
|
36
|
+
const event: any = {
|
|
37
|
+
kind: msg.kind,
|
|
38
|
+
content,
|
|
39
|
+
tags: [['p', msg.pubkey]],
|
|
40
|
+
pubkey: remoteSigner.pubkey,
|
|
41
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
await remoteSigner.sign(event);
|
|
45
|
+
|
|
46
|
+
// debug: validate/verify locally to see why rpc might ignore it
|
|
47
|
+
console.log('[test] validateEvent', validateEvent(event), 'verifySignature', verifySignature(event));
|
|
48
|
+
|
|
49
|
+
// instead of posting back via port, directly parse with rpc to avoid EventEmitter timing issues
|
|
50
|
+
try {
|
|
51
|
+
const parsed = await (rpc as any).parseEvent(event);
|
|
52
|
+
console.log('[test] direct parsed result', parsed);
|
|
53
|
+
if (parsed && (parsed as any).result === 'ack') {
|
|
54
|
+
// resolve the outer test promise via port._resolve
|
|
55
|
+
try {
|
|
56
|
+
(port as any)._resolve?.();
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error('[test] resolve error', e);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error('[test] parseEvent direct error', e);
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error('[test] postMessage handler error', e);
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
}, 0);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
rpc.setWorkerIframePort(port as MessagePort);
|
|
76
|
+
|
|
77
|
+
// override getId so we can listen for the specific response event
|
|
78
|
+
const id = 'test-iframe-id';
|
|
79
|
+
(rpc as any).getId = () => id;
|
|
80
|
+
|
|
81
|
+
// instrument rpc.emit to see emitted events
|
|
82
|
+
const origEmit = (rpc as any).emit;
|
|
83
|
+
(rpc as any).emit = function (ev: any, payload: any) {
|
|
84
|
+
try {
|
|
85
|
+
console.log('[test] rpc.emit', ev, payload && payload.id, payload && JSON.stringify(payload));
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.log('[test] rpc.emit', ev, payload && payload.id);
|
|
88
|
+
}
|
|
89
|
+
return origEmit.apply(this, arguments as any);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// instrument parseEvent to see if the rpc handler runs
|
|
93
|
+
const origParse = (rpc as any).parseEvent;
|
|
94
|
+
(rpc as any).parseEvent = async function (event: any) {
|
|
95
|
+
console.log('[test] rpc.parseEvent called');
|
|
96
|
+
try {
|
|
97
|
+
const parsed = await origParse.apply(this, arguments as any);
|
|
98
|
+
console.log('[test] rpc.parseEvent result', parsed && parsed.id, parsed && (parsed as any).method);
|
|
99
|
+
return parsed;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error('[test] rpc.parseEvent error', e);
|
|
102
|
+
throw e;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await new Promise<void>((ok, err) => {
|
|
107
|
+
// attach resolve onto the port so deferred handler can call it
|
|
108
|
+
(port as any)._resolve = ok;
|
|
109
|
+
|
|
110
|
+
// use internal setResponseHandler directly to ensure we catch responses (optional)
|
|
111
|
+
(rpc as any).setResponseHandler(id, (res: any) => {
|
|
112
|
+
console.log('[test] setResponseHandler callback', res);
|
|
113
|
+
try {
|
|
114
|
+
expect(res.result).toBe('ack');
|
|
115
|
+
ok();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
err(e);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
rpc.sendRequest(remoteSigner.pubkey, 'connect', [localSigner.pubkey], 24133);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { PrivateKeySigner } from './Signer';
|
|
3
|
+
import { NostrRpc, IframeNostrRpc, Nip46Signer } from './Nip46';
|
|
4
|
+
import { generatePrivateKey, getPublicKey } from 'nostr-tools';
|
|
5
|
+
|
|
6
|
+
// basic smoke tests
|
|
7
|
+
|
|
8
|
+
describe('NostrRpc basic', () => {
|
|
9
|
+
it('creates and signs event', async () => {
|
|
10
|
+
const sk = generatePrivateKey();
|
|
11
|
+
const signer = new PrivateKeySigner(sk);
|
|
12
|
+
const rpc = new NostrRpc(signer, ['wss://relay.nostr.example']);
|
|
13
|
+
|
|
14
|
+
const event = await rpc.createRequestEvent('id1', getPublicKey(sk), 'echo', ['hello']);
|
|
15
|
+
expect(event.kind).toBe(24133);
|
|
16
|
+
expect(event.pubkey).toBe(signer.pubkey);
|
|
17
|
+
expect(event.content).toBeTruthy();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('Nip46Signer basic', () => {
|
|
22
|
+
it('wraps signer and can sign', async () => {
|
|
23
|
+
const sk = generatePrivateKey();
|
|
24
|
+
const signer = new PrivateKeySigner(sk);
|
|
25
|
+
const nip = new Nip46Signer(signer, signer.pubkey);
|
|
26
|
+
|
|
27
|
+
const ev: any = { kind: 1, content: 'x', pubkey: signer.pubkey, tags: [] };
|
|
28
|
+
const sig = await nip.sign(ev);
|
|
29
|
+
expect(sig).toBeTruthy();
|
|
30
|
+
});
|
|
31
|
+
});
|