@konemono/nostr-login 1.11.0 → 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 +25 -19
- package/dist/index.esm.js.map +1 -1
- package/dist/modules/AuthNostrService.d.ts +4 -14
- package/dist/modules/Nip46.d.ts +23 -52
- package/dist/modules/Signer.d.ts +6 -12
- package/dist/unpkg.js +25 -19
- package/dist/utils/index.d.ts +3 -6
- package/dist/utils/nip44.d.ts +3 -3
- package/package.json +8 -8
- package/src/modules/AuthNostrService.ts +103 -223
- package/src/modules/ModalManager.ts +2 -1
- package/src/modules/Nip46.ts +171 -270
- package/src/modules/NostrExtensionService.ts +0 -2
- package/src/modules/Signer.ts +13 -35
- package/src/utils/index.ts +22 -71
- package/src/utils/nip44.ts +8 -12
- package/test-relay-management.html +0 -407
|
@@ -60,7 +60,6 @@ class NostrExtensionService extends EventEmitter {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
private async setExtensionReadPubkey(expectedPubkey?: string) {
|
|
63
|
-
// @ts-ignore
|
|
64
63
|
window.nostr = this.nostrExtension;
|
|
65
64
|
// @ts-ignore
|
|
66
65
|
const pubkey = await window.nostr.getPublicKey();
|
|
@@ -82,7 +81,6 @@ class NostrExtensionService extends EventEmitter {
|
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
public unsetExtension(nostr: Nostr) {
|
|
85
|
-
// @ts-ignore
|
|
86
84
|
if (window.nostr === this.nostrExtension) {
|
|
87
85
|
// @ts-ignore
|
|
88
86
|
window.nostr = nostr;
|
package/src/modules/Signer.ts
CHANGED
|
@@ -1,48 +1,26 @@
|
|
|
1
|
+
import { NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
|
|
1
2
|
import { Nip44 } from '../utils/nip44';
|
|
2
|
-
import { getPublicKey
|
|
3
|
+
import { getPublicKey } from 'nostr-tools';
|
|
4
|
+
import { hexToBytes } from 'nostr-tools/lib/types/utils';
|
|
3
5
|
|
|
4
|
-
export class PrivateKeySigner {
|
|
5
|
-
public privateKey: Uint8Array;
|
|
6
|
+
export class PrivateKeySigner extends NDKPrivateKeySigner {
|
|
6
7
|
private nip44: Nip44 = new Nip44();
|
|
7
|
-
private
|
|
8
|
+
private _pubkey: string;
|
|
8
9
|
|
|
9
|
-
constructor(privateKey:
|
|
10
|
-
|
|
11
|
-
this.
|
|
10
|
+
constructor(privateKey: string) {
|
|
11
|
+
super(privateKey);
|
|
12
|
+
this._pubkey = getPublicKey(hexToBytes(privateKey));
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
get pubkey() {
|
|
15
|
-
return this.
|
|
16
|
+
return this._pubkey;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
return
|
|
19
|
+
encryptNip44(recipient: NDKUser, value: string): Promise<string> {
|
|
20
|
+
return Promise.resolve(this.nip44.encrypt(this.privateKey!, recipient.pubkey, value));
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
...event,
|
|
25
|
-
created_at: event.created_at || Math.floor(Date.now() / 1000),
|
|
26
|
-
tags: event.tags || [],
|
|
27
|
-
content: event.content || '',
|
|
28
|
-
kind: event.kind
|
|
29
|
-
}, this.privateKey);
|
|
30
|
-
return signed.sig;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async encrypt(recipientPubkey: string, value: string): Promise<string> {
|
|
34
|
-
return nip04.encrypt(this.privateKey, recipientPubkey, value);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async decrypt(senderPubkey: string, value: string): Promise<string> {
|
|
38
|
-
return nip04.decrypt(this.privateKey, senderPubkey, value);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async encryptNip44(recipientPubkey: string, value: string): Promise<string> {
|
|
42
|
-
return this.nip44.encrypt(this.privateKey, recipientPubkey, value);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async decryptNip44(senderPubkey: string, value: string): Promise<string> {
|
|
46
|
-
return this.nip44.decrypt(this.privateKey, senderPubkey, value);
|
|
23
|
+
decryptNip44(sender: NDKUser, value: string): Promise<string> {
|
|
24
|
+
return Promise.resolve(this.nip44.decrypt(this.privateKey!, sender.pubkey, value));
|
|
47
25
|
}
|
|
48
26
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Info, RecentType } from 'nostr-login-components/dist/types/types';
|
|
2
|
-
import {
|
|
3
|
-
import { generateSecretKey
|
|
2
|
+
import NDK, { NDKEvent, NDKRelaySet, NDKSigner, NDKUser } from '@nostr-dev-kit/ndk';
|
|
3
|
+
import { generateSecretKey } from 'nostr-tools';
|
|
4
4
|
import { NostrLoginOptions } from '../types';
|
|
5
|
-
import {
|
|
6
|
-
import { firstValueFrom } from 'rxjs'; // You might need to install rxjs if not present, but rx-nostr depends on it
|
|
5
|
+
import { bytesToHex } from 'nostr-tools/lib/types/utils';
|
|
7
6
|
|
|
8
7
|
const LOCAL_STORE_KEY = '__nostrlogin_nip46';
|
|
9
8
|
const LOGGED_IN_ACCOUNTS = '__nostrlogin_accounts';
|
|
@@ -11,19 +10,6 @@ const RECENT_ACCOUNTS = '__nostrlogin_recent';
|
|
|
11
10
|
const OUTBOX_RELAYS = ['wss://purplepag.es', 'wss://relay.nos.social', 'wss://user.kindpag.es', 'wss://relay.damus.io', 'wss://nos.lol'];
|
|
12
11
|
const DEFAULT_SIGNUP_RELAYS = ['wss://relay.damus.io/', 'wss://nos.lol/', 'wss://relay.primal.net/'];
|
|
13
12
|
|
|
14
|
-
export function bytesToHex(bytes: Uint8Array): string {
|
|
15
|
-
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function hexToBytes(hex: string): Uint8Array {
|
|
19
|
-
if (hex.length % 2 !== 0) throw new Error('Invalid hex string');
|
|
20
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
21
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
22
|
-
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
23
|
-
}
|
|
24
|
-
return bytes;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
13
|
export const localStorageSetItem = (key: string, value: string) => {
|
|
28
14
|
localStorage.setItem(key, value);
|
|
29
15
|
};
|
|
@@ -44,30 +30,12 @@ export const localStorageRemoveItem = (key: string) => {
|
|
|
44
30
|
localStorage.removeItem(key);
|
|
45
31
|
};
|
|
46
32
|
|
|
47
|
-
export const fetchProfile = async (info: Info,
|
|
48
|
-
|
|
49
|
-
const req = createRxForwardReq();
|
|
50
|
-
const sub = rxNostr.use(req);
|
|
51
|
-
|
|
52
|
-
// We want the first event (replace specific logic handled by rx-nostr/relay usually, but here just take first)
|
|
53
|
-
// Actually we should wait for EOSE or timeout?
|
|
54
|
-
// For simplicity, let's try to get the latest kind 0.
|
|
33
|
+
export const fetchProfile = async (info: Info, profileNdk: NDK) => {
|
|
34
|
+
const user = new NDKUser({ pubkey: info.pubkey });
|
|
55
35
|
|
|
56
|
-
|
|
36
|
+
user.ndk = profileNdk;
|
|
57
37
|
|
|
58
|
-
|
|
59
|
-
const packet = await firstValueFrom(sub);
|
|
60
|
-
if (packet && packet.event) {
|
|
61
|
-
try {
|
|
62
|
-
return JSON.parse(packet.event.content);
|
|
63
|
-
} catch (e) {
|
|
64
|
-
console.error("Failed to parse profile content", e);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
} catch (e) {
|
|
68
|
-
console.log("Profile fetch timeout or error", e);
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
38
|
+
return await user.fetchProfile();
|
|
71
39
|
};
|
|
72
40
|
|
|
73
41
|
export const prepareSignupRelays = (signupRelays?: string) => {
|
|
@@ -79,70 +47,53 @@ export const prepareSignupRelays = (signupRelays?: string) => {
|
|
|
79
47
|
return relays;
|
|
80
48
|
};
|
|
81
49
|
|
|
82
|
-
export const createProfile = async (info: Info,
|
|
50
|
+
export const createProfile = async (info: Info, profileNdk: NDK, signer: NDKSigner, signupRelays?: string, outboxRelays?: string[]) => {
|
|
83
51
|
const meta = {
|
|
84
52
|
name: info.name,
|
|
85
53
|
};
|
|
86
54
|
|
|
87
|
-
const profileEvent
|
|
55
|
+
const profileEvent = new NDKEvent(profileNdk, {
|
|
88
56
|
kind: 0,
|
|
89
57
|
created_at: Math.floor(Date.now() / 1000),
|
|
90
58
|
pubkey: info.pubkey,
|
|
91
59
|
content: JSON.stringify(meta),
|
|
92
60
|
tags: [],
|
|
93
|
-
};
|
|
61
|
+
});
|
|
94
62
|
if (window.location.hostname) profileEvent.tags.push(['client', window.location.hostname]);
|
|
95
63
|
|
|
96
|
-
const relaysEvent
|
|
64
|
+
const relaysEvent = new NDKEvent(profileNdk, {
|
|
97
65
|
kind: 10002,
|
|
98
66
|
created_at: Math.floor(Date.now() / 1000),
|
|
99
67
|
pubkey: info.pubkey,
|
|
100
68
|
content: '',
|
|
101
69
|
tags: [],
|
|
102
|
-
};
|
|
70
|
+
});
|
|
103
71
|
|
|
104
72
|
const relays = prepareSignupRelays(signupRelays)
|
|
105
73
|
for (const r of relays) {
|
|
106
74
|
relaysEvent.tags.push(['r', r]);
|
|
107
75
|
}
|
|
108
76
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Let's assume it returns sig or modifies event.
|
|
114
|
-
// Ideally use nostr-tools finalizeEvent if we had the key directly, but we use the signer class.
|
|
115
|
-
|
|
116
|
-
// NOTE: PrivateKeySigner.sign(event) usually returns the signature string?
|
|
117
|
-
// And we need to calculate ID.
|
|
118
|
-
// Let's update the event object with ID and Sig as we go if the signer doesn't do it fully.
|
|
77
|
+
await profileEvent.sign(signer);
|
|
78
|
+
console.log('signed profile', profileEvent);
|
|
79
|
+
await relaysEvent.sign(signer);
|
|
80
|
+
console.log('signed relays', relaysEvent);
|
|
119
81
|
|
|
120
|
-
|
|
121
|
-
// event.id = getEventHash(event);
|
|
122
|
-
// event.sig = await signer.sign(event);
|
|
82
|
+
const outboxRelaysFinal = outboxRelays && outboxRelays.length ? outboxRelays : OUTBOX_RELAYS;
|
|
123
83
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Actually, let's just use the signer if it encapsulates it.
|
|
129
|
-
// The 'signer' passed here is `PrivateKeySigner`.
|
|
130
|
-
// `AuthNostrService` uses `this.localSigner.sign(event)`.
|
|
131
|
-
|
|
132
|
-
// I need to import getEventHash if I am to do it manually here.
|
|
133
|
-
// Or I can update PrivateKeySigner to do it.
|
|
134
|
-
// For now I will import getEventHash at top.
|
|
84
|
+
await profileEvent.publish(NDKRelaySet.fromRelayUrls(outboxRelaysFinal, profileNdk));
|
|
85
|
+
console.log('published profile', profileEvent);
|
|
86
|
+
await relaysEvent.publish(NDKRelaySet.fromRelayUrls(outboxRelaysFinal, profileNdk));
|
|
87
|
+
console.log('published relays', relaysEvent);
|
|
135
88
|
};
|
|
136
89
|
|
|
137
90
|
export const bunkerUrlToInfo = (bunkerUrl: string, sk = ''): Info => {
|
|
138
91
|
const url = new URL(bunkerUrl);
|
|
139
92
|
|
|
140
|
-
const skHex = sk || bytesToHex(generateSecretKey());
|
|
141
|
-
|
|
142
93
|
return {
|
|
143
94
|
pubkey: '',
|
|
144
95
|
signerPubkey: url.hostname || url.pathname.split('//')[1],
|
|
145
|
-
sk:
|
|
96
|
+
sk: sk || bytesToHex(generateSecretKey()),
|
|
146
97
|
relays: url.searchParams.getAll('relay'),
|
|
147
98
|
token: url.searchParams.get('secret') || '',
|
|
148
99
|
authMethod: 'connect',
|
package/src/utils/nip44.ts
CHANGED
|
@@ -10,7 +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
|
|
13
|
+
import { hexToBytes } from "nostr-tools/lib/types/utils";
|
|
14
14
|
|
|
15
15
|
// from https://github.com/nbd-wtf/nostr-tools
|
|
16
16
|
|
|
@@ -160,30 +160,26 @@ export function decryptNip44(payload: string, conversationKey: Uint8Array): stri
|
|
|
160
160
|
export class Nip44 {
|
|
161
161
|
private cache = new Map<string, Uint8Array>()
|
|
162
162
|
|
|
163
|
-
public createKey(privkey: string
|
|
164
|
-
|
|
165
|
-
// but explicit cast might be needed if type defs are strict
|
|
166
|
-
return u.getConversationKey(privkey as any, pubkey)
|
|
163
|
+
public createKey(privkey: string, pubkey: string) {
|
|
164
|
+
return u.getConversationKey(privkey, pubkey)
|
|
167
165
|
}
|
|
168
166
|
|
|
169
|
-
private getKey(privkey: string
|
|
170
|
-
const
|
|
171
|
-
const privHex = typeof privkey === 'string' ? privkey : bytesToHex(privkey);
|
|
172
|
-
const id = getPublicKey(privBytes) + pubkey
|
|
167
|
+
private getKey(privkey: string, pubkey: string, extractable?: boolean) {
|
|
168
|
+
const id = getPublicKey(hexToBytes(privkey)) + pubkey
|
|
173
169
|
let cryptoKey = this.cache.get(id)
|
|
174
170
|
if (cryptoKey) return cryptoKey
|
|
175
171
|
|
|
176
|
-
const key = this.createKey(
|
|
172
|
+
const key = this.createKey(privkey, pubkey)
|
|
177
173
|
this.cache.set(id, key)
|
|
178
174
|
return key
|
|
179
175
|
}
|
|
180
176
|
|
|
181
|
-
public encrypt(privkey: string
|
|
177
|
+
public encrypt(privkey: string, pubkey: string, text: string): string {
|
|
182
178
|
const key = this.getKey(privkey, pubkey)
|
|
183
179
|
return encryptNip44(text, key)
|
|
184
180
|
}
|
|
185
181
|
|
|
186
|
-
public decrypt(privkey: string
|
|
182
|
+
public decrypt(privkey: string, pubkey: string, data: string): string {
|
|
187
183
|
const key = this.getKey(privkey, pubkey)
|
|
188
184
|
return decryptNip44(data, key)
|
|
189
185
|
}
|
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="ja">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Relay Management Test</title>
|
|
7
|
-
<style>
|
|
8
|
-
body {
|
|
9
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
10
|
-
max-width: 1200px;
|
|
11
|
-
margin: 0 auto;
|
|
12
|
-
padding: 20px;
|
|
13
|
-
background: #f5f5f5;
|
|
14
|
-
}
|
|
15
|
-
.container {
|
|
16
|
-
background: white;
|
|
17
|
-
border-radius: 8px;
|
|
18
|
-
padding: 20px;
|
|
19
|
-
margin-bottom: 20px;
|
|
20
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
21
|
-
}
|
|
22
|
-
h1 {
|
|
23
|
-
color: #333;
|
|
24
|
-
border-bottom: 2px solid #4CAF50;
|
|
25
|
-
padding-bottom: 10px;
|
|
26
|
-
}
|
|
27
|
-
h2 {
|
|
28
|
-
color: #555;
|
|
29
|
-
margin-top: 0;
|
|
30
|
-
}
|
|
31
|
-
button {
|
|
32
|
-
background: #4CAF50;
|
|
33
|
-
color: white;
|
|
34
|
-
border: none;
|
|
35
|
-
padding: 10px 20px;
|
|
36
|
-
border-radius: 4px;
|
|
37
|
-
cursor: pointer;
|
|
38
|
-
margin: 5px;
|
|
39
|
-
font-size: 14px;
|
|
40
|
-
}
|
|
41
|
-
button:hover {
|
|
42
|
-
background: #45a049;
|
|
43
|
-
}
|
|
44
|
-
button:disabled {
|
|
45
|
-
background: #ccc;
|
|
46
|
-
cursor: not-allowed;
|
|
47
|
-
}
|
|
48
|
-
.status-box {
|
|
49
|
-
background: #f9f9f9;
|
|
50
|
-
border: 1px solid #ddd;
|
|
51
|
-
border-radius: 4px;
|
|
52
|
-
padding: 15px;
|
|
53
|
-
margin: 10px 0;
|
|
54
|
-
font-family: 'Courier New', monospace;
|
|
55
|
-
font-size: 13px;
|
|
56
|
-
white-space: pre-wrap;
|
|
57
|
-
max-height: 300px;
|
|
58
|
-
overflow-y: auto;
|
|
59
|
-
}
|
|
60
|
-
.log {
|
|
61
|
-
background: #263238;
|
|
62
|
-
color: #aed581;
|
|
63
|
-
border-radius: 4px;
|
|
64
|
-
padding: 15px;
|
|
65
|
-
margin: 10px 0;
|
|
66
|
-
font-family: 'Courier New', monospace;
|
|
67
|
-
font-size: 12px;
|
|
68
|
-
max-height: 400px;
|
|
69
|
-
overflow-y: auto;
|
|
70
|
-
}
|
|
71
|
-
.log-entry {
|
|
72
|
-
margin: 2px 0;
|
|
73
|
-
}
|
|
74
|
-
.log-error {
|
|
75
|
-
color: #ff5252;
|
|
76
|
-
}
|
|
77
|
-
.log-success {
|
|
78
|
-
color: #69f0ae;
|
|
79
|
-
}
|
|
80
|
-
.log-info {
|
|
81
|
-
color: #64b5f6;
|
|
82
|
-
}
|
|
83
|
-
.stats-grid {
|
|
84
|
-
display: grid;
|
|
85
|
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
86
|
-
gap: 10px;
|
|
87
|
-
margin: 10px 0;
|
|
88
|
-
}
|
|
89
|
-
.stat-card {
|
|
90
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
91
|
-
color: white;
|
|
92
|
-
padding: 15px;
|
|
93
|
-
border-radius: 8px;
|
|
94
|
-
text-align: center;
|
|
95
|
-
}
|
|
96
|
-
.stat-value {
|
|
97
|
-
font-size: 32px;
|
|
98
|
-
font-weight: bold;
|
|
99
|
-
margin: 5px 0;
|
|
100
|
-
}
|
|
101
|
-
.stat-label {
|
|
102
|
-
font-size: 12px;
|
|
103
|
-
opacity: 0.9;
|
|
104
|
-
}
|
|
105
|
-
.relay-list {
|
|
106
|
-
list-style: none;
|
|
107
|
-
padding: 0;
|
|
108
|
-
}
|
|
109
|
-
.relay-item {
|
|
110
|
-
background: #f5f5f5;
|
|
111
|
-
padding: 10px;
|
|
112
|
-
margin: 5px 0;
|
|
113
|
-
border-radius: 4px;
|
|
114
|
-
display: flex;
|
|
115
|
-
justify-content: space-between;
|
|
116
|
-
align-items: center;
|
|
117
|
-
}
|
|
118
|
-
.relay-status {
|
|
119
|
-
padding: 4px 12px;
|
|
120
|
-
border-radius: 12px;
|
|
121
|
-
font-size: 12px;
|
|
122
|
-
font-weight: bold;
|
|
123
|
-
}
|
|
124
|
-
.status-connected {
|
|
125
|
-
background: #4CAF50;
|
|
126
|
-
color: white;
|
|
127
|
-
}
|
|
128
|
-
.status-connecting {
|
|
129
|
-
background: #FF9800;
|
|
130
|
-
color: white;
|
|
131
|
-
}
|
|
132
|
-
.status-disconnected {
|
|
133
|
-
background: #9E9E9E;
|
|
134
|
-
color: white;
|
|
135
|
-
}
|
|
136
|
-
.status-error {
|
|
137
|
-
background: #F44336;
|
|
138
|
-
color: white;
|
|
139
|
-
}
|
|
140
|
-
input[type="text"] {
|
|
141
|
-
padding: 8px;
|
|
142
|
-
border: 1px solid #ddd;
|
|
143
|
-
border-radius: 4px;
|
|
144
|
-
width: 300px;
|
|
145
|
-
font-size: 14px;
|
|
146
|
-
}
|
|
147
|
-
</style>
|
|
148
|
-
</head>
|
|
149
|
-
<body>
|
|
150
|
-
<h1>🔌 Relay Management Test Tool</h1>
|
|
151
|
-
|
|
152
|
-
<div class="container">
|
|
153
|
-
<h2>📊 Current Status</h2>
|
|
154
|
-
<div class="stats-grid" id="statsGrid">
|
|
155
|
-
<div class="stat-card">
|
|
156
|
-
<div class="stat-label">Total Relays</div>
|
|
157
|
-
<div class="stat-value" id="totalRelays">-</div>
|
|
158
|
-
</div>
|
|
159
|
-
<div class="stat-card">
|
|
160
|
-
<div class="stat-label">Connected</div>
|
|
161
|
-
<div class="stat-value" id="connectedRelays">-</div>
|
|
162
|
-
</div>
|
|
163
|
-
<div class="stat-card">
|
|
164
|
-
<div class="stat-label">Connecting</div>
|
|
165
|
-
<div class="stat-value" id="connectingRelays">-</div>
|
|
166
|
-
</div>
|
|
167
|
-
<div class="stat-card">
|
|
168
|
-
<div class="stat-label">Disconnected</div>
|
|
169
|
-
<div class="stat-value" id="disconnectedRelays">-</div>
|
|
170
|
-
</div>
|
|
171
|
-
<div class="stat-card">
|
|
172
|
-
<div class="stat-label">Error</div>
|
|
173
|
-
<div class="stat-value" id="errorRelays">-</div>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
<h3>Relay List</h3>
|
|
178
|
-
<ul class="relay-list" id="relayList"></ul>
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
<div class="container">
|
|
182
|
-
<h2>🧪 Test Actions</h2>
|
|
183
|
-
<button onclick="refreshStatus()">🔄 Refresh Status</button>
|
|
184
|
-
<button onclick="checkHealth()">❤️ Check Health</button>
|
|
185
|
-
<button onclick="reconnectAll()">🔌 Reconnect All</button>
|
|
186
|
-
<button onclick="simulateOffline()">📡 Simulate Offline</button>
|
|
187
|
-
<button onclick="runAutoTest()">🤖 Run Auto Test</button>
|
|
188
|
-
<button onclick="clearLogs()">🗑️ Clear Logs</button>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
<div class="container">
|
|
192
|
-
<h2>➕ Add/Remove Relay</h2>
|
|
193
|
-
<input type="text" id="relayUrl" placeholder="wss://relay.example.com" />
|
|
194
|
-
<button onclick="connectRelay()">Connect</button>
|
|
195
|
-
<button onclick="disconnectRelay()">Disconnect</button>
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
<div class="container">
|
|
199
|
-
<h2>📝 Logs</h2>
|
|
200
|
-
<div class="log" id="logContainer"></div>
|
|
201
|
-
</div>
|
|
202
|
-
|
|
203
|
-
<script type="module">
|
|
204
|
-
// このスクリプトは実際のnostr-loginインスタンスにアクセスする必要があります
|
|
205
|
-
// 実際の使用時は、アプリケーションのAuthNostrServiceインスタンスを参照してください
|
|
206
|
-
|
|
207
|
-
let authService = null;
|
|
208
|
-
|
|
209
|
-
// グローバルに公開されたインスタンスを探す
|
|
210
|
-
window.addEventListener('load', () => {
|
|
211
|
-
// 実際のアプリケーションでAuthNostrServiceインスタンスを取得する方法に応じて調整
|
|
212
|
-
// 例: authService = window.nostrLogin?.authService;
|
|
213
|
-
log('⚠️ AuthNostrService instance not found. Please integrate with your app.', 'error');
|
|
214
|
-
log('ℹ️ This is a test tool template. Modify the script to access your AuthNostrService instance.', 'info');
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
function log(message, type = 'info') {
|
|
218
|
-
const logContainer = document.getElementById('logContainer');
|
|
219
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
220
|
-
const entry = document.createElement('div');
|
|
221
|
-
entry.className = `log-entry log-${type}`;
|
|
222
|
-
entry.textContent = `[${timestamp}] ${message}`;
|
|
223
|
-
logContainer.appendChild(entry);
|
|
224
|
-
logContainer.scrollTop = logContainer.scrollHeight;
|
|
225
|
-
console.log(message);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
window.refreshStatus = async function() {
|
|
229
|
-
if (!authService) {
|
|
230
|
-
log('❌ AuthNostrService not available', 'error');
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
log('🔄 Refreshing status...', 'info');
|
|
236
|
-
|
|
237
|
-
const status = authService.getRelayStatus();
|
|
238
|
-
const stats = authService.getRelayStats();
|
|
239
|
-
|
|
240
|
-
// Update stats
|
|
241
|
-
document.getElementById('totalRelays').textContent = stats.total;
|
|
242
|
-
document.getElementById('connectedRelays').textContent = stats.connected;
|
|
243
|
-
document.getElementById('connectingRelays').textContent = stats.connecting;
|
|
244
|
-
document.getElementById('disconnectedRelays').textContent = stats.disconnected;
|
|
245
|
-
document.getElementById('errorRelays').textContent = stats.error;
|
|
246
|
-
|
|
247
|
-
// Update relay list
|
|
248
|
-
const relayList = document.getElementById('relayList');
|
|
249
|
-
relayList.innerHTML = '';
|
|
250
|
-
|
|
251
|
-
for (const [url, state] of Object.entries(status)) {
|
|
252
|
-
const li = document.createElement('li');
|
|
253
|
-
li.className = 'relay-item';
|
|
254
|
-
|
|
255
|
-
const urlSpan = document.createElement('span');
|
|
256
|
-
urlSpan.textContent = url;
|
|
257
|
-
|
|
258
|
-
const statusSpan = document.createElement('span');
|
|
259
|
-
statusSpan.className = `relay-status status-${state.toLowerCase()}`;
|
|
260
|
-
statusSpan.textContent = state;
|
|
261
|
-
|
|
262
|
-
li.appendChild(urlSpan);
|
|
263
|
-
li.appendChild(statusSpan);
|
|
264
|
-
relayList.appendChild(li);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
log(`✅ Status updated: ${stats.connected}/${stats.total} connected`, 'success');
|
|
268
|
-
} catch (error) {
|
|
269
|
-
log(`❌ Error: ${error.message}`, 'error');
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
window.checkHealth = async function() {
|
|
274
|
-
if (!authService) {
|
|
275
|
-
log('❌ AuthNostrService not available', 'error');
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
try {
|
|
280
|
-
const health = await authService.checkRelayHealth();
|
|
281
|
-
log(`❤️ Health check: ${health ? 'HEALTHY' : 'UNHEALTHY'}`, health ? 'success' : 'error');
|
|
282
|
-
} catch (error) {
|
|
283
|
-
log(`❌ Error: ${error.message}`, 'error');
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
window.reconnectAll = async function() {
|
|
288
|
-
if (!authService) {
|
|
289
|
-
log('❌ AuthNostrService not available', 'error');
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
try {
|
|
294
|
-
log('🔌 Reconnecting all relays...', 'info');
|
|
295
|
-
await authService.reconnectAllRelays();
|
|
296
|
-
log('✅ Reconnection initiated', 'success');
|
|
297
|
-
setTimeout(refreshStatus, 3000);
|
|
298
|
-
} catch (error) {
|
|
299
|
-
log(`❌ Error: ${error.message}`, 'error');
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
window.connectRelay = async function() {
|
|
304
|
-
if (!authService) {
|
|
305
|
-
log('❌ AuthNostrService not available', 'error');
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const url = document.getElementById('relayUrl').value;
|
|
310
|
-
if (!url) {
|
|
311
|
-
log('❌ Please enter a relay URL', 'error');
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
log(`🔌 Connecting to ${url}...`, 'info');
|
|
317
|
-
await authService.connectToRelay(url);
|
|
318
|
-
log(`✅ Connection initiated to ${url}`, 'success');
|
|
319
|
-
setTimeout(refreshStatus, 2000);
|
|
320
|
-
} catch (error) {
|
|
321
|
-
log(`❌ Error: ${error.message}`, 'error');
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
window.disconnectRelay = async function() {
|
|
326
|
-
if (!authService) {
|
|
327
|
-
log('❌ AuthNostrService not available', 'error');
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const url = document.getElementById('relayUrl').value;
|
|
332
|
-
if (!url) {
|
|
333
|
-
log('❌ Please enter a relay URL', 'error');
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
log(`🔌 Disconnecting from ${url}...`, 'info');
|
|
339
|
-
await authService.disconnectFromRelay(url);
|
|
340
|
-
log(`✅ Disconnected from ${url}`, 'success');
|
|
341
|
-
setTimeout(refreshStatus, 1000);
|
|
342
|
-
} catch (error) {
|
|
343
|
-
log(`❌ Error: ${error.message}`, 'error');
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
window.simulateOffline = function() {
|
|
348
|
-
log('📡 To simulate offline:', 'info');
|
|
349
|
-
log('1. Open DevTools (F12)', 'info');
|
|
350
|
-
log('2. Go to Network tab', 'info');
|
|
351
|
-
log('3. Select "Offline" from throttling dropdown', 'info');
|
|
352
|
-
log('4. Wait a few seconds', 'info');
|
|
353
|
-
log('5. Select "Online" to restore connection', 'info');
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
window.runAutoTest = async function() {
|
|
357
|
-
if (!authService) {
|
|
358
|
-
log('❌ AuthNostrService not available', 'error');
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
log('🤖 Starting automated tests...', 'info');
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
// Test 1: Get status
|
|
366
|
-
log('Test 1: Getting relay status...', 'info');
|
|
367
|
-
const status = authService.getRelayStatus();
|
|
368
|
-
log(`✅ Got status for ${Object.keys(status).length} relays`, 'success');
|
|
369
|
-
|
|
370
|
-
// Test 2: Get stats
|
|
371
|
-
log('Test 2: Getting relay stats...', 'info');
|
|
372
|
-
const stats = authService.getRelayStats();
|
|
373
|
-
log(`✅ Stats: ${stats.connected}/${stats.total} connected`, 'success');
|
|
374
|
-
|
|
375
|
-
// Test 3: Health check
|
|
376
|
-
log('Test 3: Checking health...', 'info');
|
|
377
|
-
const health = await authService.checkRelayHealth();
|
|
378
|
-
log(`✅ Health: ${health ? 'HEALTHY' : 'UNHEALTHY'}`, 'success');
|
|
379
|
-
|
|
380
|
-
// Test 4: Reconnect
|
|
381
|
-
log('Test 4: Testing reconnect...', 'info');
|
|
382
|
-
await authService.reconnectAllRelays();
|
|
383
|
-
log('✅ Reconnect initiated', 'success');
|
|
384
|
-
|
|
385
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
386
|
-
await refreshStatus();
|
|
387
|
-
|
|
388
|
-
log('🎉 All tests completed!', 'success');
|
|
389
|
-
} catch (error) {
|
|
390
|
-
log(`❌ Test failed: ${error.message}`, 'error');
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
window.clearLogs = function() {
|
|
395
|
-
document.getElementById('logContainer').innerHTML = '';
|
|
396
|
-
log('🗑️ Logs cleared', 'info');
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
// Auto-refresh every 5 seconds
|
|
400
|
-
setInterval(() => {
|
|
401
|
-
if (authService) {
|
|
402
|
-
refreshStatus();
|
|
403
|
-
}
|
|
404
|
-
}, 5000);
|
|
405
|
-
</script>
|
|
406
|
-
</body>
|
|
407
|
-
</html>
|