@konemono/nostr-login 1.10.16 → 1.11.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/index.esm.js +23 -12
- package/dist/index.esm.js.map +1 -1
- package/dist/modules/Nip46.d.ts +12 -0
- package/dist/unpkg.js +23 -12
- package/package.json +1 -1
- package/src/modules/AuthNostrService.ts +24 -43
- package/src/modules/ModalManager.ts +3 -2
- package/src/modules/Nip46.ts +85 -2
- package/src/modules/Signer.ts +2 -1
- package/src/utils/index.ts +5 -4
- package/src/utils/nip44.ts +2 -1
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { localStorageAddAccount, bunkerUrlToInfo, isBunkerUrl, fetchProfile, getBunkerUrl, localStorageRemoveCurrentAccount, createProfile, getIcon } from '../utils';
|
|
2
2
|
import { ConnectionString, Info } from 'nostr-login-components/dist/types/types';
|
|
3
|
-
import {
|
|
3
|
+
import { generateSecretKey, getEventHash, getPublicKey, nip19 } from 'nostr-tools';
|
|
4
4
|
import { NostrLoginAuthOptions, Response } from '../types';
|
|
5
5
|
import NDK, { NDKEvent, NDKNip46Signer, NDKRpcResponse, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
|
|
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, ensureNIP46Connection } from './Nip46';
|
|
11
11
|
import { PrivateKeySigner } from './Signer';
|
|
12
12
|
import { DEFAULT_NIP46_RELAYS } from '../const';
|
|
13
|
+
import { bytesToHex, hexToBytes } from 'nostr-tools/lib/types/utils';
|
|
13
14
|
|
|
14
15
|
const OUTBOX_RELAYS = ['wss://user.kindpag.es', 'wss://purplepag.es', 'wss://relay.nos.social'];
|
|
15
16
|
const NOSTRCONNECT_APPS: ConnectionString[] = [
|
|
@@ -63,6 +64,8 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
63
64
|
this.params = params;
|
|
64
65
|
this.ndk = new NDK({
|
|
65
66
|
enableOutboxModel: false,
|
|
67
|
+
autoConnectUserRelays: false,
|
|
68
|
+
autoFetchUserMutelist: false,
|
|
66
69
|
});
|
|
67
70
|
|
|
68
71
|
this.profileNdk = new NDK({
|
|
@@ -89,13 +92,13 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
89
92
|
if (this.signerPromise) {
|
|
90
93
|
try {
|
|
91
94
|
await this.signerPromise;
|
|
92
|
-
} catch {}
|
|
95
|
+
} catch { }
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
if (this.readyPromise) {
|
|
96
99
|
try {
|
|
97
100
|
await this.readyPromise;
|
|
98
|
-
} catch {}
|
|
101
|
+
} catch { }
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
|
|
@@ -169,10 +172,10 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
public async createNostrConnect() {
|
|
172
|
-
this.nostrConnectKey =
|
|
175
|
+
this.nostrConnectKey = bytesToHex(generateSecretKey());
|
|
173
176
|
this.nostrConnectSecret = Math.random().toString(36).substring(7);
|
|
174
177
|
|
|
175
|
-
const pubkey = getPublicKey(this.nostrConnectKey);
|
|
178
|
+
const pubkey = getPublicKey(hexToBytes(this.nostrConnectKey));
|
|
176
179
|
const meta = {
|
|
177
180
|
name: encodeURIComponent(document.location.host),
|
|
178
181
|
url: encodeURIComponent(document.location.origin),
|
|
@@ -231,8 +234,8 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
231
234
|
|
|
232
235
|
public async localSignup(name: string, sk?: string) {
|
|
233
236
|
const signup = !sk;
|
|
234
|
-
sk = sk ||
|
|
235
|
-
const pubkey = getPublicKey(sk);
|
|
237
|
+
sk = sk || bytesToHex(generateSecretKey());
|
|
238
|
+
const pubkey = getPublicKey(hexToBytes(sk));
|
|
236
239
|
const info: Info = {
|
|
237
240
|
pubkey,
|
|
238
241
|
sk,
|
|
@@ -258,7 +261,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
258
261
|
|
|
259
262
|
// for local we export our existing key
|
|
260
263
|
if (!this.localSigner || this.params.userInfo?.authMethod !== 'local') throw new Error('Most be local keys');
|
|
261
|
-
return url + '#import=' + nip19.nsecEncode(this.localSigner.privateKey!);
|
|
264
|
+
return url + '#import=' + nip19.nsecEncode(hexToBytes(this.localSigner.privateKey!));
|
|
262
265
|
}
|
|
263
266
|
|
|
264
267
|
public async importAndConnect(cs: ConnectionString) {
|
|
@@ -361,7 +364,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
361
364
|
public exportKeys() {
|
|
362
365
|
if (!this.params.userInfo) return '';
|
|
363
366
|
if (this.params.userInfo.authMethod !== 'local') return '';
|
|
364
|
-
return nip19.nsecEncode(this.params.userInfo.sk!);
|
|
367
|
+
return nip19.nsecEncode(hexToBytes(this.params.userInfo.sk!));
|
|
365
368
|
}
|
|
366
369
|
|
|
367
370
|
private onAuth(type: 'login' | 'signup' | 'logout', info: Info | null = null) {
|
|
@@ -411,7 +414,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
411
414
|
options.name = info!.name;
|
|
412
415
|
|
|
413
416
|
if (info!.sk) {
|
|
414
|
-
options.localNsec = nip19.nsecEncode(info!.sk);
|
|
417
|
+
options.localNsec = nip19.nsecEncode(hexToBytes(info!.sk));
|
|
415
418
|
}
|
|
416
419
|
|
|
417
420
|
if (info!.relays) {
|
|
@@ -552,7 +555,7 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
552
555
|
if (this.signerPromise) {
|
|
553
556
|
try {
|
|
554
557
|
await this.signerPromise;
|
|
555
|
-
} catch {}
|
|
558
|
+
} catch { }
|
|
556
559
|
}
|
|
557
560
|
|
|
558
561
|
// we remove support for iframe from nip05 and bunker-url methods,
|
|
@@ -604,7 +607,8 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
604
607
|
|
|
605
608
|
// wait until we connect, otherwise
|
|
606
609
|
// signer won't start properly
|
|
607
|
-
|
|
610
|
+
// NOTE: Deferred connection. ensureNIP46Connection will be called on demand.
|
|
611
|
+
// await this.ndk.connect();
|
|
608
612
|
|
|
609
613
|
// create and prepare the signer
|
|
610
614
|
const localSigner = new PrivateKeySigner(info.sk!);
|
|
@@ -699,12 +703,15 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
699
703
|
|
|
700
704
|
public async signEvent(event: any) {
|
|
701
705
|
if (this.localSigner) {
|
|
702
|
-
event.pubkey = getPublicKey(this.localSigner.privateKey!);
|
|
706
|
+
event.pubkey = getPublicKey(hexToBytes(this.localSigner.privateKey!));
|
|
703
707
|
event.id = getEventHash(event);
|
|
704
708
|
event.sig = await this.localSigner.sign(event);
|
|
705
709
|
} else {
|
|
706
710
|
await this.ensureSigner();
|
|
707
711
|
|
|
712
|
+
// 署名前に接続を確認 (rpc.sendRequest内でも行われるが、安全のため)
|
|
713
|
+
await ensureNIP46Connection(this.ndk, 5000);
|
|
714
|
+
|
|
708
715
|
event.pubkey = this.signer!.remotePubkey;
|
|
709
716
|
event.id = getEventHash(event);
|
|
710
717
|
event.sig = await this.signer!.sign(event);
|
|
@@ -718,41 +725,15 @@ class AuthNostrService extends EventEmitter implements Signer {
|
|
|
718
725
|
if (!this.signer && this.params.userInfo) {
|
|
719
726
|
console.log('Signer was destroyed, reinitializing...');
|
|
720
727
|
await this.initSigner(this.params.userInfo);
|
|
721
|
-
return;
|
|
728
|
+
return;
|
|
722
729
|
}
|
|
723
730
|
|
|
724
731
|
if (!this.signer) {
|
|
725
732
|
throw new Error('No signer available');
|
|
726
733
|
}
|
|
727
734
|
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
console.log('NDK pool stats:', stats);
|
|
731
|
-
|
|
732
|
-
if (stats.connected === 0) {
|
|
733
|
-
console.log('NDK relays disconnected, reinitializing signer...');
|
|
734
|
-
|
|
735
|
-
// リレーが完全に切断されている場合、signerも再初期化する必要がある
|
|
736
|
-
// (RPCサブスクリプションも切断されているため)
|
|
737
|
-
if (this.params.userInfo) {
|
|
738
|
-
// 古いsignerを破棄
|
|
739
|
-
this.signer = null;
|
|
740
|
-
|
|
741
|
-
// 既存のリレーを一度切断
|
|
742
|
-
for (const relay of this.ndk.pool.relays.values()) {
|
|
743
|
-
try {
|
|
744
|
-
relay.disconnect();
|
|
745
|
-
} catch (e) {
|
|
746
|
-
console.log('Error disconnecting relay:', e);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// signerを再初期化(リレー接続も含む)
|
|
751
|
-
await this.initSigner(this.params.userInfo);
|
|
752
|
-
} else {
|
|
753
|
-
throw new Error('Cannot reconnect: no user info');
|
|
754
|
-
}
|
|
755
|
-
}
|
|
735
|
+
// 接続確立は署名時またはRPCリクエスト時に行われるため、
|
|
736
|
+
// ここでの冗長な接続チェックは削除
|
|
756
737
|
}
|
|
757
738
|
|
|
758
739
|
private async codec_call(method: string, pubkey: string, param: string) {
|
|
@@ -5,6 +5,7 @@ import { EventEmitter } from 'tseep';
|
|
|
5
5
|
import { ConnectionString, Info, RecentType } from 'nostr-login-components/dist/types/types';
|
|
6
6
|
import { nip19 } from 'nostr-tools';
|
|
7
7
|
import { setDarkMode } from '..';
|
|
8
|
+
import { bytesToHex } from 'nostr-tools/lib/types/utils';
|
|
8
9
|
|
|
9
10
|
class ModalManager extends EventEmitter {
|
|
10
11
|
private modal: TypeModal | null = null;
|
|
@@ -28,7 +29,7 @@ class ModalManager extends EventEmitter {
|
|
|
28
29
|
if (this.launcherPromise) {
|
|
29
30
|
try {
|
|
30
31
|
await this.launcherPromise;
|
|
31
|
-
} catch {}
|
|
32
|
+
} catch { }
|
|
32
33
|
this.launcherPromise = undefined;
|
|
33
34
|
}
|
|
34
35
|
}
|
|
@@ -281,7 +282,7 @@ class ModalManager extends EventEmitter {
|
|
|
281
282
|
throw new Error('Bad nsec value');
|
|
282
283
|
}
|
|
283
284
|
if (decoded.type !== 'nsec') throw new Error('Bad bech32 type');
|
|
284
|
-
await this.authNostrService.localSignup('', decoded.data);
|
|
285
|
+
await this.authNostrService.localSignup('', bytesToHex(decoded.data));
|
|
285
286
|
ok();
|
|
286
287
|
} else if (nsecOrBunker.startsWith('bunker:')) {
|
|
287
288
|
await this.authNostrService.authNip46('login', { name: '', bunkerUrl: nsecOrBunker });
|
package/src/modules/Nip46.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import NDK, { NDKEvent, NDKFilter, NDKNip46Signer, NDKNostrRpc, NDKRpcRequest, NDKRpcResponse, NDKSubscription, NDKSubscriptionCacheUsage, NostrEvent } from '@nostr-dev-kit/ndk';
|
|
2
|
-
import { validateEvent
|
|
2
|
+
import { validateEvent } from 'nostr-tools';
|
|
3
3
|
import { PrivateKeySigner } from './Signer';
|
|
4
4
|
import { NIP46_REQUEST_TIMEOUT, NIP46_CONNECT_TIMEOUT } from '../const';
|
|
5
5
|
|
|
@@ -8,6 +8,82 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage: st
|
|
|
8
8
|
return Promise.race([promise, new Promise<T>((_, reject) => setTimeout(() => reject(new Error(errorMessage)), timeoutMs))]);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* NDKのリレー接続を待機する
|
|
13
|
+
*/
|
|
14
|
+
export function waitForConnection(ndk: NDK, timeout: number): Promise<void> {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const start = Date.now();
|
|
17
|
+
|
|
18
|
+
const check = () => {
|
|
19
|
+
// 接続済みリレーがあるか確認
|
|
20
|
+
const connected = Array.from(ndk.pool.relays.values()).some(relay => relay.status === 1); // 1 = CONNECTED
|
|
21
|
+
|
|
22
|
+
if (connected) {
|
|
23
|
+
resolve();
|
|
24
|
+
} else if (Date.now() - start > timeout) {
|
|
25
|
+
reject(new Error(`接続タイムアウト: ${timeout}ms`));
|
|
26
|
+
} else {
|
|
27
|
+
setTimeout(check, 100);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
check();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* NDKの接続を強制的に確立する
|
|
37
|
+
*/
|
|
38
|
+
export async function ensureNDKConnection(ndk: NDK, timeout: number): Promise<void> {
|
|
39
|
+
// connect()は冪等なので安全
|
|
40
|
+
await ndk.connect();
|
|
41
|
+
await waitForConnection(ndk, timeout);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* NIP-46専用リレーも含めて接続を確立する
|
|
46
|
+
*/
|
|
47
|
+
export async function ensureNIP46Connection(ndk: NDK, timeout: number): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
await ensureNDKConnection(ndk, timeout);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.warn('Initial connection attempt failed, retrying with force disconnect...', e);
|
|
52
|
+
// 接続数0なら明示的に全切断して再試行
|
|
53
|
+
if (Array.from(ndk.pool.relays.values()).filter(r => r.status === 1).length === 0) {
|
|
54
|
+
ndk.pool.relays.forEach(relay => {
|
|
55
|
+
try {
|
|
56
|
+
relay.disconnect();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error('Error disconnecting relay:', err);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// タイムアウトを延長して再試行
|
|
62
|
+
await ensureNDKConnection(ndk, timeout * 2);
|
|
63
|
+
} else {
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// NIP-46用リレーが個別に定義されている場合の接続確認
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
const signerRelays = ndk.signer?.relayUrls;
|
|
71
|
+
if (signerRelays && Array.isArray(signerRelays)) {
|
|
72
|
+
for (const url of signerRelays) {
|
|
73
|
+
const relay = ndk.pool.getRelay(url);
|
|
74
|
+
if (relay && relay.status !== 1) {
|
|
75
|
+
await relay.connect();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 最終チェック
|
|
81
|
+
const connectedRelays = Array.from(ndk.pool.relays.values()).filter(r => r.status === 1);
|
|
82
|
+
if (connectedRelays.length === 0) {
|
|
83
|
+
throw new Error('リレー接続に失敗しました');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
11
87
|
class NostrRpc extends NDKNostrRpc {
|
|
12
88
|
protected _ndk: NDK;
|
|
13
89
|
protected _signer: PrivateKeySigner;
|
|
@@ -235,6 +311,9 @@ class NostrRpc extends NDKNostrRpc {
|
|
|
235
311
|
}
|
|
236
312
|
|
|
237
313
|
public async sendRequest(remotePubkey: string, method: string, params: string[] = [], kind = 24133, cb?: (res: NDKRpcResponse) => void): Promise<NDKRpcResponse> {
|
|
314
|
+
// リクエスト送信前に接続を確認
|
|
315
|
+
await ensureNIP46Connection(this._ndk, 5000);
|
|
316
|
+
|
|
238
317
|
const id = this.getId();
|
|
239
318
|
|
|
240
319
|
// response handler will deduplicate auth urls and responses
|
|
@@ -376,7 +455,6 @@ export class IframeNostrRpc extends NostrRpc {
|
|
|
376
455
|
const event = ev.data;
|
|
377
456
|
|
|
378
457
|
if (!validateEvent(event)) throw new Error('Invalid event from iframe');
|
|
379
|
-
if (!verifySignature(event)) throw new Error('Invalid event signature from iframe');
|
|
380
458
|
const nevent = new NDKEvent(this._ndk, event);
|
|
381
459
|
const parsedEvent = await this.parseEvent(nevent);
|
|
382
460
|
// レスポンス受信時にタイムスタンプを更新
|
|
@@ -393,6 +471,11 @@ export class IframeNostrRpc extends NostrRpc {
|
|
|
393
471
|
}
|
|
394
472
|
|
|
395
473
|
public async sendRequest(remotePubkey: string, method: string, params: string[] = [], kind = 24133, cb?: (res: NDKRpcResponse) => void): Promise<NDKRpcResponse> {
|
|
474
|
+
// リクエスト送信前に接続を確認 (relayを使用する場合に備えて)
|
|
475
|
+
if (!this.iframePort) {
|
|
476
|
+
await ensureNIP46Connection(this._ndk, 5000);
|
|
477
|
+
}
|
|
478
|
+
|
|
396
479
|
const id = this.getId();
|
|
397
480
|
|
|
398
481
|
// create and sign request event
|
package/src/modules/Signer.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
|
|
2
2
|
import { Nip44 } from '../utils/nip44';
|
|
3
3
|
import { getPublicKey } from 'nostr-tools';
|
|
4
|
+
import { hexToBytes } from 'nostr-tools/lib/types/utils';
|
|
4
5
|
|
|
5
6
|
export class PrivateKeySigner extends NDKPrivateKeySigner {
|
|
6
7
|
private nip44: Nip44 = new Nip44();
|
|
@@ -8,7 +9,7 @@ export class PrivateKeySigner extends NDKPrivateKeySigner {
|
|
|
8
9
|
|
|
9
10
|
constructor(privateKey: string) {
|
|
10
11
|
super(privateKey);
|
|
11
|
-
this._pubkey = getPublicKey(privateKey);
|
|
12
|
+
this._pubkey = getPublicKey(hexToBytes(privateKey));
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
get pubkey() {
|
package/src/utils/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Info, RecentType } from 'nostr-login-components/dist/types/types';
|
|
2
2
|
import NDK, { NDKEvent, NDKRelaySet, NDKSigner, NDKUser } from '@nostr-dev-kit/ndk';
|
|
3
|
-
import {
|
|
3
|
+
import { generateSecretKey } from 'nostr-tools';
|
|
4
4
|
import { NostrLoginOptions } from '../types';
|
|
5
|
+
import { bytesToHex } from 'nostr-tools/lib/types/utils';
|
|
5
6
|
|
|
6
7
|
const LOCAL_STORE_KEY = '__nostrlogin_nip46';
|
|
7
8
|
const LOGGED_IN_ACCOUNTS = '__nostrlogin_accounts';
|
|
@@ -19,7 +20,7 @@ export const localStorageGetItem = (key: string) => {
|
|
|
19
20
|
if (value) {
|
|
20
21
|
try {
|
|
21
22
|
return JSON.parse(value);
|
|
22
|
-
} catch {}
|
|
23
|
+
} catch { }
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
return null;
|
|
@@ -92,7 +93,7 @@ export const bunkerUrlToInfo = (bunkerUrl: string, sk = ''): Info => {
|
|
|
92
93
|
return {
|
|
93
94
|
pubkey: '',
|
|
94
95
|
signerPubkey: url.hostname || url.pathname.split('//')[1],
|
|
95
|
-
sk: sk ||
|
|
96
|
+
sk: sk || bytesToHex(generateSecretKey()),
|
|
96
97
|
relays: url.searchParams.getAll('relay'),
|
|
97
98
|
token: url.searchParams.get('secret') || '',
|
|
98
99
|
authMethod: 'connect',
|
|
@@ -165,7 +166,7 @@ export const checkNip05 = async (nip05: string) => {
|
|
|
165
166
|
pubkey = d.names[name];
|
|
166
167
|
return;
|
|
167
168
|
}
|
|
168
|
-
} catch {}
|
|
169
|
+
} catch { }
|
|
169
170
|
|
|
170
171
|
available = true;
|
|
171
172
|
})();
|
package/src/utils/nip44.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { sha256 } from "@noble/hashes/sha256"
|
|
|
10
10
|
import { hmac } from "@noble/hashes/hmac";
|
|
11
11
|
import { base64 } from "@scure/base";
|
|
12
12
|
import { getPublicKey } from 'nostr-tools'
|
|
13
|
+
import { hexToBytes } from "nostr-tools/lib/types/utils";
|
|
13
14
|
|
|
14
15
|
// from https://github.com/nbd-wtf/nostr-tools
|
|
15
16
|
|
|
@@ -164,7 +165,7 @@ export class Nip44 {
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
private getKey(privkey: string, pubkey: string, extractable?: boolean) {
|
|
167
|
-
const id = getPublicKey(privkey) + pubkey
|
|
168
|
+
const id = getPublicKey(hexToBytes(privkey)) + pubkey
|
|
168
169
|
let cryptoKey = this.cache.get(id)
|
|
169
170
|
if (cryptoKey) return cryptoKey
|
|
170
171
|
|