@konemono/nostr-login 1.9.12 → 1.9.14
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 +5 -5
- package/dist/index.esm.js.map +1 -1
- package/dist/modules/AuthNostrService.d.ts +4 -0
- package/dist/modules/nip46/Nip46Adapter.d.ts +2 -10
- package/dist/modules/nip46/Nip46Client.d.ts +1 -16
- package/dist/modules/nip46/types.d.ts +0 -8
- package/dist/unpkg.js +5 -5
- package/package.json +1 -1
- package/src/modules/AmberDirectSigner.ts +4 -18
- package/src/modules/AuthNostrService.ts +151 -6
- package/src/modules/nip46/Nip46Adapter.ts +26 -83
- package/src/modules/nip46/Nip46Client.ts +4 -112
- package/src/modules/nip46/types.ts +15 -23
- package/tsconfig.json +1 -1
package/package.json
CHANGED
|
@@ -30,14 +30,12 @@ export class AmberDirectSigner implements Signer {
|
|
|
30
30
|
public async getPublicKey(id?: string): Promise<string> {
|
|
31
31
|
id = id || Math.random().toString(36).substring(7);
|
|
32
32
|
const url = this.generateUrl('', 'get_public_key', id);
|
|
33
|
-
console.log('
|
|
33
|
+
console.log('Amber redirecting to:', url);
|
|
34
34
|
window.open(url, '_blank', 'width=400,height=600');
|
|
35
35
|
return new Promise((resolve, reject) => {
|
|
36
36
|
const timer = setTimeout(() => {
|
|
37
37
|
AmberDirectSigner.pendingResolves.delete(id!);
|
|
38
|
-
|
|
39
|
-
console.error(error.message);
|
|
40
|
-
reject(error);
|
|
38
|
+
reject(new Error('AmberDirectSigner timeout'));
|
|
41
39
|
}, 20000);
|
|
42
40
|
AmberDirectSigner.pendingResolves.set(id!, { resolve, timer });
|
|
43
41
|
});
|
|
@@ -123,12 +121,9 @@ export class AmberDirectSigner implements Signer {
|
|
|
123
121
|
}
|
|
124
122
|
|
|
125
123
|
public generateUrl(content: string, type: string, id: string, pubkey?: string): string {
|
|
126
|
-
const
|
|
124
|
+
const callbackUrl = window.location.href.split('#')[0].split('?')[0];
|
|
127
125
|
const encodedContent = encodeURIComponent(content || '');
|
|
128
|
-
|
|
129
|
-
// callbackUrl にはベースURLだけを渡す
|
|
130
|
-
// Amberが自動的に ?event=<result> を追加する (NIP-55)
|
|
131
|
-
const callbackUrl = baseUrl;
|
|
126
|
+
const encodedCallback = encodeURIComponent(callbackUrl);
|
|
132
127
|
|
|
133
128
|
localStorage.setItem('amber_last_type', type);
|
|
134
129
|
localStorage.setItem('amber_last_id', id);
|
|
@@ -170,13 +165,6 @@ export class AmberDirectSigner implements Signer {
|
|
|
170
165
|
// パターン1: NIP-55標準 (?event=...)
|
|
171
166
|
let result = url.searchParams.get('event');
|
|
172
167
|
|
|
173
|
-
// Amberが壊れた{result}プレースホルダーを返した場合のクリーンアップ
|
|
174
|
-
// 例: ?event=result}5650178... → 5650178...
|
|
175
|
-
if (result && result.includes('result}')) {
|
|
176
|
-
result = result.replace(/^result\}/, '');
|
|
177
|
-
console.log('[AmberDirectSigner] Cleaned up malformed result:', result);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
168
|
// パターン2: パス末尾にhexId (/<hexId>)
|
|
181
169
|
if (!result) {
|
|
182
170
|
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
@@ -220,11 +208,9 @@ export class AmberDirectSigner implements Signer {
|
|
|
220
208
|
if (entry) {
|
|
221
209
|
this.pendingResolves.delete(id);
|
|
222
210
|
if (entry.timer) clearTimeout(entry.timer);
|
|
223
|
-
console.log('[AmberDirectSigner] Resolving pending:', { id, type, resultLen: result.length });
|
|
224
211
|
entry.resolve(result);
|
|
225
212
|
return true;
|
|
226
213
|
}
|
|
227
|
-
console.warn('[AmberDirectSigner] No pending resolve found for:', id);
|
|
228
214
|
return false;
|
|
229
215
|
}
|
|
230
216
|
|
|
@@ -11,6 +11,7 @@ import { IframeNostrRpc, Nip46Signer, ReadyListener, RpcResponse } from './Nip46
|
|
|
11
11
|
import { Nip46Client } from './nip46/Nip46Client';
|
|
12
12
|
import { Nip46Adapter } from './nip46/Nip46Adapter';
|
|
13
13
|
import { PrivateKeySigner } from './Signer';
|
|
14
|
+
import { AmberDirectSigner } from './AmberDirectSigner';
|
|
14
15
|
|
|
15
16
|
const OUTBOX_RELAYS = ['wss://user.kindpag.es', 'wss://purplepag.es', 'wss://relay.nos.social'];
|
|
16
17
|
const DEFAULT_NOSTRCONNECT_RELAYS = ['wss://relay.nsec.app/', 'wss://ephemeral.snowflare.cc/'];
|
|
@@ -27,7 +28,7 @@ const NOSTRCONNECT_APPS: ConnectionString[] = [
|
|
|
27
28
|
{
|
|
28
29
|
name: 'Amber',
|
|
29
30
|
img: 'https://raw.githubusercontent.com/greenart7c3/Amber/refs/heads/master/assets/android-icon.svg',
|
|
30
|
-
link: '
|
|
31
|
+
link: 'amber',
|
|
31
32
|
relays: DEFAULT_NOSTRCONNECT_RELAYS,
|
|
32
33
|
},
|
|
33
34
|
{
|
|
@@ -40,6 +41,7 @@ const NOSTRCONNECT_APPS: ConnectionString[] = [
|
|
|
40
41
|
|
|
41
42
|
class AuthNostrService extends EventEmitter implements Signer {
|
|
42
43
|
private signer: any = null;
|
|
44
|
+
private amberSigner: AmberDirectSigner | null = null;
|
|
43
45
|
private localSigner: PrivateKeySigner | null = null;
|
|
44
46
|
private params: NostrParams;
|
|
45
47
|
private signerPromise?: Promise<void>;
|
|
@@ -73,6 +75,96 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
73
75
|
encrypt: this.encrypt44.bind(this),
|
|
74
76
|
decrypt: this.decrypt44.bind(this),
|
|
75
77
|
};
|
|
78
|
+
|
|
79
|
+
setTimeout(() => this.checkAmberResponse(), 100);
|
|
80
|
+
|
|
81
|
+
const check = () => {
|
|
82
|
+
this.checkAmberResponse();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
window.addEventListener('focus', check);
|
|
86
|
+
window.addEventListener('visibilitychange', () => {
|
|
87
|
+
if (document.visibilityState === 'visible') check();
|
|
88
|
+
});
|
|
89
|
+
window.addEventListener('popstate', check);
|
|
90
|
+
window.addEventListener('hashchange', check);
|
|
91
|
+
|
|
92
|
+
// Periodic check as a safety net
|
|
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
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private checkAmberResponse() {
|
|
105
|
+
const response = AmberDirectSigner.parseResponse();
|
|
106
|
+
if (response) {
|
|
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
|
+
}
|
|
118
|
+
|
|
119
|
+
private handledAmberIds: Set<string> = new Set();
|
|
120
|
+
|
|
121
|
+
private handleAmberResponse(response: { id: string; type: string; result: string }) {
|
|
122
|
+
if (this.handledAmberIds.has(response.id)) return;
|
|
123
|
+
this.handledAmberIds.add(response.id);
|
|
124
|
+
|
|
125
|
+
console.log('Handling Amber response', response);
|
|
126
|
+
|
|
127
|
+
// Stop the "Connecting..." spinner
|
|
128
|
+
this.emit('onAuthUrl', { url: '' });
|
|
129
|
+
|
|
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
|
+
}
|
|
135
|
+
|
|
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());
|
|
167
|
+
}
|
|
76
168
|
}
|
|
77
169
|
|
|
78
170
|
public isIframe() {
|
|
@@ -128,6 +220,41 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
128
220
|
|
|
129
221
|
// non-iframe flow
|
|
130
222
|
if (link && !iframeUrl) {
|
|
223
|
+
if (link === 'amber') {
|
|
224
|
+
const signer = new AmberDirectSigner();
|
|
225
|
+
this.amberSigner = signer;
|
|
226
|
+
|
|
227
|
+
const id = Math.random().toString(36).substring(7);
|
|
228
|
+
const url = signer.generateUrl('', 'get_public_key', id);
|
|
229
|
+
|
|
230
|
+
// Emit for the "Connecting..." spinner
|
|
231
|
+
this.emit('onAuthUrl', { url });
|
|
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
|
+
}
|
|
257
|
+
}
|
|
131
258
|
window.open(link, '_blank', 'width=400,height=700');
|
|
132
259
|
}
|
|
133
260
|
|
|
@@ -164,7 +291,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
164
291
|
perms: encodeURIComponent(this.params.optionsModal.perms || ''),
|
|
165
292
|
};
|
|
166
293
|
|
|
167
|
-
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}`) : ''}`;
|
|
168
295
|
}
|
|
169
296
|
|
|
170
297
|
public async getNostrConnectServices(): Promise<[string, ConnectionString[]]> {
|
|
@@ -292,12 +419,9 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
292
419
|
}
|
|
293
420
|
|
|
294
421
|
public async setAmber(info: Info) {
|
|
295
|
-
// Amber now uses NIP-46 (nostrconnect) like other signers
|
|
296
422
|
this.releaseSigner();
|
|
297
|
-
|
|
298
|
-
await this.initSigner(info);
|
|
423
|
+
this.amberSigner = new AmberDirectSigner(info.pubkey);
|
|
299
424
|
this.onAuth('login', info);
|
|
300
|
-
await this.endAuth();
|
|
301
425
|
}
|
|
302
426
|
|
|
303
427
|
public async createAccount(nip05: string) {
|
|
@@ -328,6 +452,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
328
452
|
this.signer = null;
|
|
329
453
|
this.signerErrCallback?.('cancelled');
|
|
330
454
|
this.localSigner = null;
|
|
455
|
+
this.amberSigner = null;
|
|
331
456
|
}
|
|
332
457
|
|
|
333
458
|
public async logout(keepSigner = false) {
|
|
@@ -677,6 +802,10 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
677
802
|
event.pubkey = getPublicKey(this.localSigner.privateKey!);
|
|
678
803
|
event.id = getEventHash(event);
|
|
679
804
|
event.sig = await this.localSigner.sign(event);
|
|
805
|
+
} else if (this.params.userInfo?.authMethod === ('amber' as any)) {
|
|
806
|
+
const userInfo = this.params.userInfo!;
|
|
807
|
+
if (!this.amberSigner) this.amberSigner = new AmberDirectSigner(userInfo.pubkey);
|
|
808
|
+
return this.amberSigner.signEvent(event);
|
|
680
809
|
} else {
|
|
681
810
|
event.pubkey = this.signer?.remotePubkey;
|
|
682
811
|
event.id = getEventHash(event);
|
|
@@ -709,6 +838,10 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
709
838
|
public async encrypt04(pubkey: string, plaintext: string) {
|
|
710
839
|
if (this.localSigner) {
|
|
711
840
|
return this.localSigner.encrypt(pubkey, plaintext);
|
|
841
|
+
} else if (this.params.userInfo?.authMethod === ('amber' as any)) {
|
|
842
|
+
const userInfo = this.params.userInfo!;
|
|
843
|
+
if (!this.amberSigner) this.amberSigner = new AmberDirectSigner(userInfo.pubkey);
|
|
844
|
+
return this.amberSigner.encrypt04(pubkey, plaintext);
|
|
712
845
|
} else {
|
|
713
846
|
// adapter supports encrypt(pubkey, plaintext)
|
|
714
847
|
if (this.signer && typeof this.signer.encrypt === 'function') {
|
|
@@ -722,6 +855,10 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
722
855
|
public async decrypt04(pubkey: string, ciphertext: string) {
|
|
723
856
|
if (this.localSigner) {
|
|
724
857
|
return this.localSigner.decrypt(pubkey, ciphertext);
|
|
858
|
+
} else if (this.params.userInfo?.authMethod === ('amber' as any)) {
|
|
859
|
+
const userInfo = this.params.userInfo!;
|
|
860
|
+
if (!this.amberSigner) this.amberSigner = new AmberDirectSigner(userInfo.pubkey);
|
|
861
|
+
return this.amberSigner.decrypt04(pubkey, ciphertext);
|
|
725
862
|
} else {
|
|
726
863
|
// If signer supports direct decrypt(pubkey, ciphertext), use it
|
|
727
864
|
if (this.signer && typeof this.signer.decrypt === 'function') {
|
|
@@ -736,6 +873,10 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
736
873
|
public async encrypt44(pubkey: string, plaintext: string) {
|
|
737
874
|
if (this.localSigner) {
|
|
738
875
|
return this.nip44Codec.encrypt(this.localSigner.privateKey!, pubkey, plaintext);
|
|
876
|
+
} else if (this.params.userInfo?.authMethod === ('amber' as any)) {
|
|
877
|
+
const userInfo = this.params.userInfo!;
|
|
878
|
+
if (!this.amberSigner) this.amberSigner = new AmberDirectSigner(userInfo.pubkey);
|
|
879
|
+
return this.amberSigner.encrypt44(pubkey, plaintext);
|
|
739
880
|
} else {
|
|
740
881
|
// no support of nip44 in legacy signer implementation
|
|
741
882
|
return this.codec_call('nip44_encrypt', pubkey, plaintext);
|
|
@@ -745,6 +886,10 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
745
886
|
public async decrypt44(pubkey: string, ciphertext: string) {
|
|
746
887
|
if (this.localSigner) {
|
|
747
888
|
return this.nip44Codec.decrypt(this.localSigner.privateKey!, pubkey, ciphertext);
|
|
889
|
+
} else if (this.params.userInfo?.authMethod === ('amber' as any)) {
|
|
890
|
+
const userInfo = this.params.userInfo!;
|
|
891
|
+
if (!this.amberSigner) this.amberSigner = new AmberDirectSigner(userInfo.pubkey);
|
|
892
|
+
return this.amberSigner.decrypt44(pubkey, ciphertext);
|
|
748
893
|
} else {
|
|
749
894
|
// no support of nip44 in legacy signer implementation
|
|
750
895
|
return this.codec_call('nip44_decrypt', pubkey, ciphertext);
|
|
@@ -27,83 +27,38 @@ export class Nip46Adapter extends EventEmitter {
|
|
|
27
27
|
if (this.userPubkey) throw new Error('Already called initUserPubkey');
|
|
28
28
|
if (hintPubkey) {
|
|
29
29
|
this.userPubkey = hintPubkey;
|
|
30
|
-
console.log('[Nip46Adapter] User pubkey set from hint:', hintPubkey);
|
|
31
30
|
return;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!res) throw new Error('No public key returned');
|
|
38
|
-
this.userPubkey = res;
|
|
39
|
-
console.log('[Nip46Adapter] User pubkey fetched:', res);
|
|
40
|
-
} catch (error: any) {
|
|
41
|
-
console.error('[Nip46Adapter] Failed to get user pubkey:', error.message);
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
33
|
+
const res = await this.client.sendRequest('get_public_key', []);
|
|
34
|
+
if (!res) throw new Error('No public key returned');
|
|
35
|
+
this.userPubkey = res;
|
|
44
36
|
}
|
|
45
37
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* サイナーからの接続を待つ
|
|
49
|
-
*/
|
|
50
|
-
async listen(nostrConnectSecret: string, timeoutMs: number = 60000): Promise<string> {
|
|
51
|
-
console.log('[Nip46Adapter] Starting listen mode, timeout:', timeoutMs);
|
|
52
|
-
|
|
53
|
-
return new Promise<string>((resolve, reject) => {
|
|
54
|
-
const timer = setTimeout(() => {
|
|
55
|
-
cleanup();
|
|
56
|
-
const error = new Error(`Listen timeout after ${timeoutMs}ms`);
|
|
57
|
-
console.error('[Nip46Adapter]', error.message);
|
|
58
|
-
reject(error);
|
|
59
|
-
}, timeoutMs);
|
|
60
|
-
|
|
61
|
-
const cleanup = () => {
|
|
62
|
-
clearTimeout(timer);
|
|
63
|
-
this.client.off('response', onResponse);
|
|
64
|
-
};
|
|
65
|
-
|
|
38
|
+
async listen(nostrConnectSecret: string): Promise<string> {
|
|
39
|
+
return new Promise<string>((ok, err) => {
|
|
66
40
|
const onResponse = ({ response, pubkey }: any) => {
|
|
67
41
|
if (!response) return;
|
|
68
|
-
|
|
69
|
-
// auth_urlは無視(別のハンドラが処理)
|
|
70
42
|
if (response.result === 'auth_url') return;
|
|
71
|
-
|
|
72
|
-
// 成功: ack または secret が返される
|
|
73
43
|
if (response.result === 'ack' || response.result === nostrConnectSecret) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
resolve(pubkey);
|
|
77
|
-
} else if (response.error) {
|
|
78
|
-
cleanup();
|
|
79
|
-
console.error('[Nip46Adapter] Listen failed:', response.error);
|
|
80
|
-
reject(new Error(response.error));
|
|
44
|
+
this.client.off('response', onResponse);
|
|
45
|
+
ok(pubkey);
|
|
81
46
|
}
|
|
82
47
|
};
|
|
83
48
|
|
|
84
49
|
this.client.on('response', onResponse);
|
|
50
|
+
|
|
51
|
+
// also add a timeout
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
this.client.off('response', onResponse);
|
|
54
|
+
err(new Error('Listen timeout'));
|
|
55
|
+
}, 30000);
|
|
85
56
|
});
|
|
86
57
|
}
|
|
87
58
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*/
|
|
92
|
-
async connect(token?: string, perms?: string, timeoutMs: number = 30000): Promise<void> {
|
|
93
|
-
console.log('[Nip46Adapter] Connecting with token, timeout:', timeoutMs);
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const result = await this.client.sendRequest('connect', [this.localSigner.pubkey, token || '', perms || ''], timeoutMs);
|
|
97
|
-
|
|
98
|
-
if (result !== 'ack') {
|
|
99
|
-
throw new Error(`Connect failed: ${result || 'unknown error'}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
console.log('[Nip46Adapter] Connected successfully');
|
|
103
|
-
} catch (error: any) {
|
|
104
|
-
console.error('[Nip46Adapter] Connect failed:', error.message);
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
59
|
+
async connect(token?: string, perms?: string) {
|
|
60
|
+
const result = await this.client.sendRequest('connect', [this.localSigner.pubkey, token || '', perms || '']);
|
|
61
|
+
if (result !== 'ack') throw new Error(result || 'connect failed');
|
|
107
62
|
}
|
|
108
63
|
|
|
109
64
|
async setListenReply(reply: any, nostrConnectSecret: string) {
|
|
@@ -125,17 +80,10 @@ export class Nip46Adapter extends EventEmitter {
|
|
|
125
80
|
async createAccount2({ bunkerPubkey, name, domain, perms = '' }: { bunkerPubkey: string; name: string; domain: string; perms?: string }) {
|
|
126
81
|
const params = [name, domain, '', perms];
|
|
127
82
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (r === 'error') throw new Error('create_account failed');
|
|
133
|
-
console.log('[Nip46Adapter] Account created successfully');
|
|
134
|
-
return r;
|
|
135
|
-
} catch (error: any) {
|
|
136
|
-
console.error('[Nip46Adapter] Failed to create account:', error.message);
|
|
137
|
-
throw error;
|
|
138
|
-
}
|
|
83
|
+
const r = await this.client.sendRequest('create_account', params);
|
|
84
|
+
if (!r) throw new Error('create_account failed');
|
|
85
|
+
if (r === 'error') throw new Error('create_account error');
|
|
86
|
+
return r;
|
|
139
87
|
}
|
|
140
88
|
|
|
141
89
|
async encrypt(recipientPubkey: string, plaintext: string) {
|
|
@@ -149,19 +97,14 @@ export class Nip46Adapter extends EventEmitter {
|
|
|
149
97
|
}
|
|
150
98
|
|
|
151
99
|
async sign(event: any) {
|
|
100
|
+
const r = await this.client.sendRequest('sign_event', [JSON.stringify(event)]);
|
|
152
101
|
try {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
} catch (e) {
|
|
158
|
-
// not JSON
|
|
159
|
-
}
|
|
160
|
-
return r;
|
|
161
|
-
} catch (error: any) {
|
|
162
|
-
console.error('[Nip46Adapter] Failed to sign event:', error.message);
|
|
163
|
-
throw error;
|
|
102
|
+
const parsed = typeof r === 'string' ? JSON.parse(r) : r;
|
|
103
|
+
if (parsed && parsed.sig) return parsed.sig;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// not JSON
|
|
164
106
|
}
|
|
107
|
+
return r;
|
|
165
108
|
}
|
|
166
109
|
|
|
167
110
|
// provide rpc compatibility
|
|
@@ -15,9 +15,6 @@ export class Nip46Client extends EventEmitter {
|
|
|
15
15
|
private subscription: any = null;
|
|
16
16
|
private isSubscribed: boolean = false;
|
|
17
17
|
private nip44Codec: Nip44 = new Nip44();
|
|
18
|
-
private iframeConfig?: { origin: string; port?: MessagePort };
|
|
19
|
-
private retryConfig: { maxRetries: number; retryDelayMs: number };
|
|
20
|
-
private iframeKeepaliveInterval?: NodeJS.Timeout;
|
|
21
18
|
|
|
22
19
|
constructor(options: Nip46ClientOptions) {
|
|
23
20
|
super();
|
|
@@ -27,13 +24,6 @@ export class Nip46Client extends EventEmitter {
|
|
|
27
24
|
this.relays = options.relays;
|
|
28
25
|
this.defaultTimeoutMs = options.timeoutMs || 30000;
|
|
29
26
|
this.useNip44 = options.useNip44 || false;
|
|
30
|
-
this.iframeConfig = options.iframeConfig;
|
|
31
|
-
this.retryConfig = options.retryConfig || { maxRetries: 3, retryDelayMs: 1000 };
|
|
32
|
-
|
|
33
|
-
// iframe用のメッセージハンドラを設定
|
|
34
|
-
if (this.iframeConfig?.port) {
|
|
35
|
-
this.setupIframePort(this.iframeConfig.port);
|
|
36
|
-
}
|
|
37
27
|
}
|
|
38
28
|
|
|
39
29
|
get localPubkey(): string {
|
|
@@ -41,40 +31,9 @@ export class Nip46Client extends EventEmitter {
|
|
|
41
31
|
}
|
|
42
32
|
|
|
43
33
|
/**
|
|
44
|
-
* NIP-46
|
|
34
|
+
* NIP-46リクエストを送信
|
|
45
35
|
*/
|
|
46
36
|
async sendRequest(method: string, params: string[] = [], timeoutMs?: number): Promise<string> {
|
|
47
|
-
let lastError: Error | null = null;
|
|
48
|
-
|
|
49
|
-
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
50
|
-
try {
|
|
51
|
-
if (attempt > 0) {
|
|
52
|
-
const delay = this.retryConfig.retryDelayMs * attempt;
|
|
53
|
-
console.log(`[Nip46Client] Retry attempt ${attempt}/${this.retryConfig.maxRetries} for ${method} after ${delay}ms`);
|
|
54
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return await this.sendRequestInternal(method, params, timeoutMs);
|
|
58
|
-
} catch (error: any) {
|
|
59
|
-
lastError = error;
|
|
60
|
-
|
|
61
|
-
// タイムアウトまたはネットワークエラーの場合のみリトライ
|
|
62
|
-
const shouldRetry = error.message.includes('timed out') || error.message.includes('network') || error.message.includes('publish');
|
|
63
|
-
|
|
64
|
-
if (!shouldRetry || attempt === this.retryConfig.maxRetries) {
|
|
65
|
-
console.error(`[Nip46Client] Request failed after ${attempt + 1} attempts:`, error.message);
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
throw lastError || new Error('Request failed after retries');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* NIP-46リクエストを送信(内部実装)
|
|
76
|
-
*/
|
|
77
|
-
private async sendRequestInternal(method: string, params: string[] = [], timeoutMs?: number): Promise<string> {
|
|
78
37
|
const timeout = timeoutMs || this.defaultTimeoutMs;
|
|
79
38
|
const id = this.generateId();
|
|
80
39
|
const request: Nip46Request = { id, method, params };
|
|
@@ -135,25 +94,9 @@ export class Nip46Client extends EventEmitter {
|
|
|
135
94
|
// 署名
|
|
136
95
|
event.sig = getSignature(event, this.localPrivateKey);
|
|
137
96
|
|
|
138
|
-
// iframeがある場合は優先的に使用
|
|
139
|
-
if (this.iframeConfig?.port) {
|
|
140
|
-
try {
|
|
141
|
-
this.iframeConfig.port.postMessage(event);
|
|
142
|
-
console.log('[Nip46Client] Request sent via iframe:', request.id);
|
|
143
|
-
return;
|
|
144
|
-
} catch (e) {
|
|
145
|
-
console.warn('[Nip46Client] Iframe send failed, falling back to relays:', e);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
97
|
// リレーに送信
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
console.log('[Nip46Client] Request published to relays:', request.id);
|
|
153
|
-
} catch (e) {
|
|
154
|
-
console.error('[Nip46Client] Failed to publish to relays:', e);
|
|
155
|
-
throw new Error('Failed to publish request to relays');
|
|
156
|
-
}
|
|
98
|
+
await Promise.any(this.pool.publish(this.relays, event));
|
|
99
|
+
console.log('[Nip46Client] Request published:', request.id);
|
|
157
100
|
}
|
|
158
101
|
|
|
159
102
|
/**
|
|
@@ -203,12 +146,10 @@ export class Nip46Client extends EventEmitter {
|
|
|
203
146
|
// Emit response event for consumers (include sender pubkey)
|
|
204
147
|
this.emit('response', { response, pubkey: event.pubkey });
|
|
205
148
|
|
|
206
|
-
// auth_url
|
|
149
|
+
// auth_urlの特別処理
|
|
207
150
|
if (response.result === 'auth_url') {
|
|
208
151
|
console.log('[Nip46Client] Auth URL received:', response.error);
|
|
209
152
|
this.emit('authUrl', response.error);
|
|
210
|
-
// 注意: pendingRequestsは削除しない
|
|
211
|
-
// OAuth完了後に同じIDで実際のレスポンスが返ってくる
|
|
212
153
|
return;
|
|
213
154
|
}
|
|
214
155
|
|
|
@@ -291,12 +232,6 @@ export class Nip46Client extends EventEmitter {
|
|
|
291
232
|
this.isSubscribed = false;
|
|
292
233
|
}
|
|
293
234
|
|
|
294
|
-
// iframeのkeepaliveをクリア
|
|
295
|
-
if (this.iframeKeepaliveInterval) {
|
|
296
|
-
clearInterval(this.iframeKeepaliveInterval);
|
|
297
|
-
this.iframeKeepaliveInterval = undefined;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
235
|
// リレー接続を閉じる
|
|
301
236
|
this.pool.close(this.relays);
|
|
302
237
|
|
|
@@ -310,47 +245,4 @@ export class Nip46Client extends EventEmitter {
|
|
|
310
245
|
isConnected(): boolean {
|
|
311
246
|
return this.isSubscribed;
|
|
312
247
|
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* iframeポートを設定
|
|
316
|
-
*/
|
|
317
|
-
setIframePort(port: MessagePort): void {
|
|
318
|
-
if (!this.iframeConfig) {
|
|
319
|
-
throw new Error('Iframe config not set');
|
|
320
|
-
}
|
|
321
|
-
this.iframeConfig.port = port;
|
|
322
|
-
this.setupIframePort(port);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* iframe用のメッセージハンドラを設定
|
|
327
|
-
*/
|
|
328
|
-
private setupIframePort(port: MessagePort): void {
|
|
329
|
-
port.onmessage = async (ev: MessageEvent) => {
|
|
330
|
-
if (ev.data === 'ping') return;
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
const event = ev.data;
|
|
334
|
-
// validate and handle event
|
|
335
|
-
if (!event || typeof event !== 'object') {
|
|
336
|
-
console.warn('[Nip46Client] Invalid message from iframe');
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
await this.handleResponseEvent(event);
|
|
340
|
-
} catch (e) {
|
|
341
|
-
console.error('[Nip46Client] Iframe message error:', e);
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
// Keep alive
|
|
346
|
-
this.iframeKeepaliveInterval = setInterval(() => {
|
|
347
|
-
try {
|
|
348
|
-
port.postMessage('ping');
|
|
349
|
-
} catch (e) {
|
|
350
|
-
console.warn('[Nip46Client] Failed to send keepalive ping:', e);
|
|
351
|
-
}
|
|
352
|
-
}, 5000);
|
|
353
|
-
|
|
354
|
-
console.log('[Nip46Client] Iframe port setup complete');
|
|
355
|
-
}
|
|
356
248
|
}
|
|
@@ -1,34 +1,26 @@
|
|
|
1
1
|
export interface Nip46Request {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
id: string;
|
|
3
|
+
method: string;
|
|
4
|
+
params: string[];
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export interface Nip46Response {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
id: string;
|
|
9
|
+
result?: string;
|
|
10
|
+
error?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface PendingRequest {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
resolve: (result: string) => void;
|
|
15
|
+
reject: (error: Error) => void;
|
|
16
|
+
timer: NodeJS.Timeout;
|
|
17
|
+
method: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface Nip46ClientOptions {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
iframeConfig?: {
|
|
27
|
-
origin: string;
|
|
28
|
-
port?: MessagePort;
|
|
29
|
-
};
|
|
30
|
-
retryConfig?: {
|
|
31
|
-
maxRetries: number;
|
|
32
|
-
retryDelayMs: number;
|
|
33
|
-
};
|
|
21
|
+
localPrivateKey: string;
|
|
22
|
+
remotePubkey: string;
|
|
23
|
+
relays: string[];
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
useNip44?: boolean;
|
|
34
26
|
}
|
package/tsconfig.json
CHANGED