Wallet plugin: офлайн wallet-session и выбор homeserver\n\nСделано:\n- wallet plugin сохраняет PDA-профиль и остаётся офлайн до действия;\n- добавлен каркас выбора ключа подписи и homeserver-устройства;\n- добавлен ручной refresh trusted devices через ListSessions;\n- на регистрации показан первый сервер SHiNE и его адрес;\n- обновлены pending notes для ручной проверки.\n\nЕщё не проверено / не доделано:\n- end-to-end ручная проверка plugin после этих правок не завершена;\n- signaling запроса подписи и ответ подписи ещё не реализованы;\n- локальный browser plugin нужно отдельно reload в Chrome/Opera.
This commit is contained in:
parent
f8a76bcd7f
commit
2225c2d173
@ -0,0 +1,17 @@
|
|||||||
|
# Wallet plugin: PDA-ключи и выбор homeserver
|
||||||
|
|
||||||
|
- краткое описание:
|
||||||
|
`SHiNE-browser-plugin-wallet` после session-only подключения сохраняет `login`, публичные `root/device/blockchain` ключи из PDA и список опубликованных `homeserver`-сессий. Постоянное подключение не удерживается: plugin остаётся офлайн, а список trusted devices обновляет по запросу.
|
||||||
|
|
||||||
|
- что проверять:
|
||||||
|
1. После успешного pairing plugin показывает сохранённую wallet-session без автоматического постоянного подключения.
|
||||||
|
2. В карточке wallet-session виден сокращённый `deviceKey`.
|
||||||
|
3. Кнопка `Обновить устройства` подтягивает homeserver-сессии из PDA и показывает их список со статусом `online/offline/unknown`.
|
||||||
|
4. В селекте ключа подписи доступен `deviceKey (ed25519, ...)`.
|
||||||
|
5. Кнопка `Запросить подпись` не падает и честно сообщает, что signaling подписи ещё не доделан.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
кошелёк хранит PDA-метаданные локально, не висит всё время онлайн и показывает каркас выбора ключа и устройства перед будущим этапом signaling подписи.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
pending
|
||||||
@ -5,6 +5,7 @@ import {
|
|||||||
DEFAULT_SHINE_SERVER_LOGIN,
|
DEFAULT_SHINE_SERVER_LOGIN,
|
||||||
buildHttpBase,
|
buildHttpBase,
|
||||||
normalizeServerLogin,
|
normalizeServerLogin,
|
||||||
|
readWalletProfileByLogin,
|
||||||
resolveShineServerByServerLogin,
|
resolveShineServerByServerLogin,
|
||||||
} from './js/lib/shine-server-resolver.js';
|
} from './js/lib/shine-server-resolver.js';
|
||||||
|
|
||||||
@ -24,6 +25,12 @@ const state = {
|
|||||||
pollTimer: 0,
|
pollTimer: 0,
|
||||||
activeSession: null,
|
activeSession: null,
|
||||||
connectionOnline: false,
|
connectionOnline: false,
|
||||||
|
walletProfile: null,
|
||||||
|
signing: {
|
||||||
|
selectedKeyId: 'device',
|
||||||
|
selectedDeviceName: '',
|
||||||
|
devicesResolvedAtMs: 0,
|
||||||
|
},
|
||||||
statusText: '',
|
statusText: '',
|
||||||
statusKind: 'info',
|
statusKind: 'info',
|
||||||
};
|
};
|
||||||
@ -80,6 +87,12 @@ async function loadStateFromStorage() {
|
|||||||
login: String(settings?.login || '').trim(),
|
login: String(settings?.login || '').trim(),
|
||||||
};
|
};
|
||||||
state.activeSession = await loadSessionMaterial();
|
state.activeSession = await loadSessionMaterial();
|
||||||
|
state.walletProfile = state.activeSession?.walletProfile || null;
|
||||||
|
state.signing = {
|
||||||
|
selectedKeyId: String(state.activeSession?.selectedKeyId || 'device'),
|
||||||
|
selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''),
|
||||||
|
devicesResolvedAtMs: Number(state.activeSession?.devicesResolvedAtMs || 0),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persistSettings(nextSettings = {}) {
|
async function persistSettings(nextSettings = {}) {
|
||||||
@ -93,7 +106,89 @@ async function persistSettings(nextSettings = {}) {
|
|||||||
return state.settings;
|
return state.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resumeActiveSession() {
|
async function saveActiveSessionRecord() {
|
||||||
|
if (!state.activeSession) return;
|
||||||
|
const nextRecord = {
|
||||||
|
...state.activeSession,
|
||||||
|
walletProfile: state.walletProfile,
|
||||||
|
selectedKeyId: state.signing.selectedKeyId,
|
||||||
|
selectedDeviceName: state.signing.selectedDeviceName,
|
||||||
|
devicesResolvedAtMs: state.signing.devicesResolvedAtMs,
|
||||||
|
};
|
||||||
|
state.activeSession = nextRecord;
|
||||||
|
await saveSessionMaterial(nextRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortKey(value = '', size = 10) {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
return raw ? raw.slice(0, size) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSigningKeyOptions(walletProfile) {
|
||||||
|
const deviceKey = String(walletProfile?.publicKeys?.deviceKeyBase58 || '').trim();
|
||||||
|
if (!deviceKey) return [];
|
||||||
|
return [{
|
||||||
|
id: 'device',
|
||||||
|
label: `deviceKey (ed25519, ${shortKey(deviceKey)})`,
|
||||||
|
keyType: 'ed25519',
|
||||||
|
publicKeyBase58: deviceKey,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeHomeserverStatuses(publishedHomeservers = [], serverSessions = []) {
|
||||||
|
const published = Array.isArray(publishedHomeservers) ? publishedHomeservers : [];
|
||||||
|
const homeserverSessions = Array.isArray(serverSessions)
|
||||||
|
? serverSessions.filter((item) => Number(item?.sessionType || 0) === 100)
|
||||||
|
: [];
|
||||||
|
const onlineHomeservers = homeserverSessions.filter((item) => !!item?.onlineOnThisServer);
|
||||||
|
|
||||||
|
return published.map((item) => {
|
||||||
|
let onlineState = 'unknown';
|
||||||
|
if (published.length === 1) {
|
||||||
|
onlineState = onlineHomeservers.length > 0 ? 'online' : 'offline';
|
||||||
|
} else if (onlineHomeservers.length === 0) {
|
||||||
|
onlineState = 'offline';
|
||||||
|
} else if (onlineHomeservers.length === published.length) {
|
||||||
|
onlineState = 'online';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
onlineState,
|
||||||
|
onlineLabel: onlineState === 'online' ? 'online' : onlineState === 'offline' ? 'offline' : 'unknown',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hydrateWalletProfile(login) {
|
||||||
|
const cleanLogin = String(login || state.activeSession?.login || state.settings.login || '').trim();
|
||||||
|
if (!cleanLogin) throw new Error('Нет логина для чтения PDA кошелька.');
|
||||||
|
const profile = await readWalletProfileByLogin(cleanLogin);
|
||||||
|
const signingKeyOptions = buildSigningKeyOptions(profile);
|
||||||
|
const selectedKeyId = signingKeyOptions.some((item) => item.id === state.signing.selectedKeyId)
|
||||||
|
? state.signing.selectedKeyId
|
||||||
|
: (signingKeyOptions[0]?.id || '');
|
||||||
|
const selectedDeviceName = state.signing.selectedDeviceName
|
||||||
|
|| String(profile?.homeserverSessions?.[0]?.sessionName || '');
|
||||||
|
|
||||||
|
state.walletProfile = {
|
||||||
|
...profile,
|
||||||
|
signingKeyOptions,
|
||||||
|
homeserverSessions: Array.isArray(profile.homeserverSessions) ? profile.homeserverSessions.map((item) => ({
|
||||||
|
...item,
|
||||||
|
onlineState: 'unknown',
|
||||||
|
onlineLabel: 'unknown',
|
||||||
|
})) : [],
|
||||||
|
};
|
||||||
|
state.signing = {
|
||||||
|
...state.signing,
|
||||||
|
selectedKeyId,
|
||||||
|
selectedDeviceName,
|
||||||
|
};
|
||||||
|
await saveActiveSessionRecord();
|
||||||
|
return state.walletProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resumeActiveSession({ keepConnected = false } = {}) {
|
||||||
const sessionRecord = await loadSessionMaterial();
|
const sessionRecord = await loadSessionMaterial();
|
||||||
state.activeSession = sessionRecord;
|
state.activeSession = sessionRecord;
|
||||||
if (!sessionRecord) {
|
if (!sessionRecord) {
|
||||||
@ -110,8 +205,14 @@ async function resumeActiveSession() {
|
|||||||
login: String(sessionRecord?.login || state.settings.login || '').trim(),
|
login: String(sessionRecord?.login || state.settings.login || '').trim(),
|
||||||
});
|
});
|
||||||
const resumed = await ensureApi().resumeSession(sessionRecord);
|
const resumed = await ensureApi().resumeSession(sessionRecord);
|
||||||
state.connectionOnline = true;
|
state.connectionOnline = !!keepConnected;
|
||||||
setStatus(`Wallet-session активна для @${resumed.login}.`, 'info');
|
if (!keepConnected) {
|
||||||
|
ensureApi().close();
|
||||||
|
state.api = null;
|
||||||
|
setStatus(`Wallet-session сохранена для @${resumed.login}. Подключение будет открываться только по действию.`, 'info');
|
||||||
|
} else {
|
||||||
|
setStatus(`Wallet-session активна для @${resumed.login}.`, 'info');
|
||||||
|
}
|
||||||
return { ok: true, connected: true, login: resumed.login, sessionId: resumed.sessionId };
|
return { ok: true, connected: true, login: resumed.login, sessionId: resumed.sessionId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
state.connectionOnline = false;
|
state.connectionOnline = false;
|
||||||
@ -142,13 +243,14 @@ async function attachApprovedSession(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await clearSessionMaterial();
|
await clearSessionMaterial();
|
||||||
await saveSessionMaterial(sessionRecord);
|
|
||||||
state.activeSession = sessionRecord;
|
state.activeSession = sessionRecord;
|
||||||
|
await hydrateWalletProfile(login);
|
||||||
|
await saveActiveSessionRecord();
|
||||||
await persistSettings({
|
await persistSettings({
|
||||||
login: sessionRecord.login,
|
login: sessionRecord.login,
|
||||||
serverUrl: sessionRecord.serverUrl,
|
serverUrl: sessionRecord.serverUrl,
|
||||||
});
|
});
|
||||||
await resumeActiveSession();
|
state.connectionOnline = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollPairingStatus() {
|
async function pollPairingStatus() {
|
||||||
@ -166,7 +268,7 @@ async function pollPairingStatus() {
|
|||||||
const decoded = await decryptPairingPayloadFromEnvelope(payload?.encryptedPayload, state.requesterMaterial);
|
const decoded = await decryptPairingPayloadFromEnvelope(payload?.encryptedPayload, state.requesterMaterial);
|
||||||
await attachApprovedSession(decoded);
|
await attachApprovedSession(decoded);
|
||||||
clearPairingState();
|
clearPairingState();
|
||||||
setStatus('Wallet-session создана и сохранена.', 'info');
|
setStatus('Wallet-session создана и сохранена. Кошелёк остаётся офлайн до запроса подписи.', 'info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (stateValue === 'rejected') {
|
if (stateValue === 'rejected') {
|
||||||
@ -251,13 +353,87 @@ async function cancelPairing() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function disconnectSession() {
|
async function disconnectSession() {
|
||||||
|
ensureApi().close();
|
||||||
|
state.api = null;
|
||||||
await clearSessionMaterial();
|
await clearSessionMaterial();
|
||||||
state.activeSession = null;
|
state.activeSession = null;
|
||||||
state.connectionOnline = false;
|
state.connectionOnline = false;
|
||||||
|
state.walletProfile = null;
|
||||||
|
state.signing = {
|
||||||
|
selectedKeyId: 'device',
|
||||||
|
selectedDeviceName: '',
|
||||||
|
devicesResolvedAtMs: 0,
|
||||||
|
};
|
||||||
setStatus('Сохранённая wallet-session удалена из plugin.', 'info');
|
setStatus('Сохранённая wallet-session удалена из plugin.', 'info');
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshWalletDevices() {
|
||||||
|
if (!state.activeSession?.login) {
|
||||||
|
throw new Error('Сначала подключите wallet-session.');
|
||||||
|
}
|
||||||
|
await hydrateWalletProfile(state.activeSession.login);
|
||||||
|
const resumed = await resumeActiveSession({ keepConnected: true });
|
||||||
|
if (!resumed.ok) {
|
||||||
|
throw new Error(resumed.error || 'Не удалось открыть wallet-session.');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const sessions = await ensureApi().listSessions();
|
||||||
|
state.walletProfile = {
|
||||||
|
...state.walletProfile,
|
||||||
|
homeserverSessions: mergeHomeserverStatuses(state.walletProfile?.homeserverSessions, sessions),
|
||||||
|
};
|
||||||
|
state.signing.devicesResolvedAtMs = Date.now();
|
||||||
|
if (!state.signing.selectedDeviceName && state.walletProfile.homeserverSessions[0]?.sessionName) {
|
||||||
|
state.signing.selectedDeviceName = state.walletProfile.homeserverSessions[0].sessionName;
|
||||||
|
}
|
||||||
|
await saveActiveSessionRecord();
|
||||||
|
setStatus('Список доверенных homeserver-устройств обновлён.', 'info');
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
devices: state.walletProfile.homeserverSessions,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
ensureApi().close();
|
||||||
|
state.api = null;
|
||||||
|
state.connectionOnline = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSigningSelection({ selectedKeyId, selectedDeviceName } = {}) {
|
||||||
|
state.signing = {
|
||||||
|
...state.signing,
|
||||||
|
selectedKeyId: String(selectedKeyId || state.signing.selectedKeyId || ''),
|
||||||
|
selectedDeviceName: String(selectedDeviceName || state.signing.selectedDeviceName || ''),
|
||||||
|
};
|
||||||
|
await saveActiveSessionRecord();
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareSignSignal() {
|
||||||
|
if (!state.activeSession?.login) {
|
||||||
|
throw new Error('Сначала подключите wallet-session.');
|
||||||
|
}
|
||||||
|
if (!state.signing.selectedKeyId) {
|
||||||
|
throw new Error('Не выбран ключ подписи.');
|
||||||
|
}
|
||||||
|
if (!state.signing.selectedDeviceName) {
|
||||||
|
throw new Error('Не выбрано устройство homeserver.');
|
||||||
|
}
|
||||||
|
const selectedDevice = (state.walletProfile?.homeserverSessions || []).find((item) => item.sessionName === state.signing.selectedDeviceName);
|
||||||
|
if (!selectedDevice) {
|
||||||
|
throw new Error('Выбранное устройство не найдено в PDA аккаунта.');
|
||||||
|
}
|
||||||
|
setStatus(
|
||||||
|
`Каркас готов: запрос подписи должен идти через ${selectedDevice.sessionName}. Сам signaling подписи ещё не доделан.`,
|
||||||
|
'info',
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
pending: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function snapshot() {
|
function snapshot() {
|
||||||
return {
|
return {
|
||||||
settings: { ...state.settings },
|
settings: { ...state.settings },
|
||||||
@ -270,6 +446,8 @@ function snapshot() {
|
|||||||
},
|
},
|
||||||
session: state.activeSession ? { ...state.activeSession } : null,
|
session: state.activeSession ? { ...state.activeSession } : null,
|
||||||
connectionOnline: state.connectionOnline,
|
connectionOnline: state.connectionOnline,
|
||||||
|
walletProfile: state.walletProfile ? { ...state.walletProfile } : null,
|
||||||
|
signing: { ...state.signing },
|
||||||
status: {
|
status: {
|
||||||
text: state.statusText,
|
text: state.statusText,
|
||||||
kind: state.statusKind,
|
kind: state.statusKind,
|
||||||
@ -305,6 +483,21 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
sendResponse({ ok: true, result, state: snapshot() });
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (type === 'wallet:refreshWalletDevices') {
|
||||||
|
const result = await refreshWalletDevices();
|
||||||
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === 'wallet:updateSigningSelection') {
|
||||||
|
const result = await updateSigningSelection(message?.payload || {});
|
||||||
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === 'wallet:prepareSignSignal') {
|
||||||
|
const result = await prepareSignSignal();
|
||||||
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (type === 'wallet:disconnectSession') {
|
if (type === 'wallet:disconnectSession') {
|
||||||
const result = await disconnectSession();
|
const result = await disconnectSession();
|
||||||
sendResponse({ ok: true, result, state: snapshot() });
|
sendResponse({ ok: true, result, state: snapshot() });
|
||||||
@ -318,6 +511,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
void loadStateFromStorage().then(() => resumeActiveSession()).catch((error) => {
|
void loadStateFromStorage().then(async () => {
|
||||||
|
if (state.activeSession?.login) {
|
||||||
|
await hydrateWalletProfile(state.activeSession.login).catch(() => {});
|
||||||
|
setStatus(`Wallet-session сохранена для @${state.activeSession.login}. Подключение будет открываться только по действию.`, 'info');
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
setStatus(error?.message || 'Не удалось инициализировать wallet plugin.', 'error');
|
setStatus(error?.message || 'Не удалось инициализировать wallet plugin.', 'error');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -69,6 +69,12 @@ export class ShineApiClient {
|
|||||||
return response.payload || {};
|
return response.payload || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listSessions() {
|
||||||
|
const response = await this.ws.request('ListSessions', {});
|
||||||
|
if (response.status !== 200) throw opError('ListSessions', response);
|
||||||
|
return Array.isArray(response?.payload?.sessions) ? response.payload.sessions : [];
|
||||||
|
}
|
||||||
|
|
||||||
async resumeSession(sessionRecord) {
|
async resumeSession(sessionRecord) {
|
||||||
const login = String(sessionRecord?.login || '').trim();
|
const login = String(sessionRecord?.login || '').trim();
|
||||||
const sessionId = String(sessionRecord?.sessionId || '').trim();
|
const sessionId = String(sessionRecord?.sessionId || '').trim();
|
||||||
|
|||||||
@ -69,21 +69,32 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
|||||||
let isServer = false;
|
let isServer = false;
|
||||||
let serverAddress = '';
|
let serverAddress = '';
|
||||||
let accessServers = [];
|
let accessServers = [];
|
||||||
|
let rootKey32 = null;
|
||||||
|
let deviceKey32 = null;
|
||||||
|
let blockchainKey32 = null;
|
||||||
|
let blockchainName = '';
|
||||||
|
let homeserverSessions = [];
|
||||||
|
|
||||||
for (let i = 0; i < blocksCount; i += 1) {
|
for (let i = 0; i < blocksCount; i += 1) {
|
||||||
const blockType = readU8(bytes, cursorRef);
|
const blockType = readU8(bytes, cursorRef);
|
||||||
cursorRef.value += 1; // block_version
|
cursorRef.value += 1; // block_version
|
||||||
|
|
||||||
if (blockType === 1 || blockType === 2) {
|
if (blockType === 1 || blockType === 2) {
|
||||||
cursorRef.value += 32;
|
const key32 = readBytes(bytes, cursorRef, 32);
|
||||||
|
if (blockType === 1) rootKey32 = key32;
|
||||||
|
if (blockType === 2) deviceKey32 = key32;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (blockType === 3) {
|
if (blockType === 3) {
|
||||||
const count = readU8(bytes, cursorRef);
|
const count = readU8(bytes, cursorRef);
|
||||||
for (let j = 0; j < count; j += 1) {
|
for (let j = 0; j < count; j += 1) {
|
||||||
cursorRef.value += 1;
|
cursorRef.value += 1;
|
||||||
readStrU8(bytes, cursorRef);
|
const currentBlockchainName = readStrU8(bytes, cursorRef);
|
||||||
cursorRef.value += 32;
|
const currentBlockchainKey32 = readBytes(bytes, cursorRef, 32);
|
||||||
|
if (!blockchainKey32) {
|
||||||
|
blockchainKey32 = currentBlockchainKey32;
|
||||||
|
blockchainName = currentBlockchainName;
|
||||||
|
}
|
||||||
cursorRef.value += 8 + 8 + 4 + 32 + 64;
|
cursorRef.value += 8 + 8 + 4 + 32 + 64;
|
||||||
const arPresent = readU8(bytes, cursorRef);
|
const arPresent = readU8(bytes, cursorRef);
|
||||||
if (arPresent === 1) readStrU8(bytes, cursorRef);
|
if (arPresent === 1) readStrU8(bytes, cursorRef);
|
||||||
@ -111,9 +122,19 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
|||||||
cursorRef.value += 1;
|
cursorRef.value += 1;
|
||||||
const sessionsCount = readU8(bytes, cursorRef);
|
const sessionsCount = readU8(bytes, cursorRef);
|
||||||
for (let j = 0; j < sessionsCount; j += 1) {
|
for (let j = 0; j < sessionsCount; j += 1) {
|
||||||
cursorRef.value += 1 + 1;
|
const sessionType = readU8(bytes, cursorRef);
|
||||||
readStrU8(bytes, cursorRef);
|
const sessionVersion = readU8(bytes, cursorRef);
|
||||||
cursorRef.value += 32;
|
const sessionName = readStrU8(bytes, cursorRef);
|
||||||
|
const sessionPubKey32 = readBytes(bytes, cursorRef, 32);
|
||||||
|
if (sessionType === 100) {
|
||||||
|
homeserverSessions.push({
|
||||||
|
sessionType,
|
||||||
|
sessionVersion,
|
||||||
|
sessionName,
|
||||||
|
sessionPubKeyBase58: new PublicKey(sessionPubKey32).toBase58(),
|
||||||
|
sessionPubKeyB64: `ed25519/${btoa(String.fromCharCode(...sessionPubKey32))}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -128,6 +149,13 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
|||||||
isServer,
|
isServer,
|
||||||
serverAddress: normalizeHostLike(serverAddress),
|
serverAddress: normalizeHostLike(serverAddress),
|
||||||
accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean),
|
accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean),
|
||||||
|
publicKeys: {
|
||||||
|
rootKeyBase58: rootKey32 ? new PublicKey(rootKey32).toBase58() : '',
|
||||||
|
deviceKeyBase58: deviceKey32 ? new PublicKey(deviceKey32).toBase58() : '',
|
||||||
|
blockchainKeyBase58: blockchainKey32 ? new PublicKey(blockchainKey32).toBase58() : '',
|
||||||
|
blockchainName,
|
||||||
|
},
|
||||||
|
homeserverSessions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,6 +204,17 @@ export async function resolveShineServerByServerLogin(serverLogin, solanaEndpoin
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readWalletProfileByLogin(login, solanaEndpoint = SOLANA_ENDPOINT_DEFAULT) {
|
||||||
|
const cleanLogin = normalizeServerLogin(login);
|
||||||
|
const parsed = await fetchUserPda(cleanLogin, solanaEndpoint);
|
||||||
|
return {
|
||||||
|
login: cleanLogin,
|
||||||
|
accessServers: parsed.accessServers,
|
||||||
|
publicKeys: parsed.publicKeys,
|
||||||
|
homeserverSessions: parsed.homeserverSessions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DEFAULT_SHINE_SERVER_ADDRESS,
|
DEFAULT_SHINE_SERVER_ADDRESS,
|
||||||
DEFAULT_SHINE_SERVER_LOGIN,
|
DEFAULT_SHINE_SERVER_LOGIN,
|
||||||
|
|||||||
@ -88,7 +88,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="password"] {
|
input[type="password"],
|
||||||
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid #314459;
|
border: 1px solid #314459;
|
||||||
@ -178,3 +179,33 @@ input[type="password"] {
|
|||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.device-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-row {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #243446;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0d141d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-state {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-state-online {
|
||||||
|
color: #b7f5ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-state-offline {
|
||||||
|
color: #ffc4cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-state-unknown {
|
||||||
|
color: #f8e2a0;
|
||||||
|
}
|
||||||
|
|||||||
@ -28,12 +28,36 @@
|
|||||||
<div class="summary-row"><span>Логин</span><strong id="session-login">—</strong></div>
|
<div class="summary-row"><span>Логин</span><strong id="session-login">—</strong></div>
|
||||||
<div class="summary-row"><span>Session ID</span><code id="session-id">—</code></div>
|
<div class="summary-row"><span>Session ID</span><code id="session-id">—</code></div>
|
||||||
<div class="summary-row"><span>Тип</span><strong id="session-type">wallet</strong></div>
|
<div class="summary-row"><span>Тип</span><strong id="session-type">wallet</strong></div>
|
||||||
|
<div class="summary-row"><span>deviceKey</span><code id="device-key-short">—</code></div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="resume-btn" class="btn secondary" type="button">Переподключить</button>
|
<button id="resume-btn" class="btn secondary" type="button">Проверить session</button>
|
||||||
|
<button id="refresh-devices-btn" class="btn secondary" type="button">Обновить устройства</button>
|
||||||
<button id="disconnect-btn" class="btn danger" type="button">Отключить</button>
|
<button id="disconnect-btn" class="btn danger" type="button">Отключить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="signing-card" class="card hidden">
|
||||||
|
<div class="card-title">Подготовка подписи</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>Ключ подписи</span>
|
||||||
|
<select id="sign-key-select"></select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Устройство homeserver</span>
|
||||||
|
<select id="device-select"></select>
|
||||||
|
</label>
|
||||||
|
<div id="homeserver-list" class="device-list"></div>
|
||||||
|
<p class="muted small">
|
||||||
|
Для выбора доступны homeserver-сессии, опубликованные в PDA аккаунта. Online-статус определяется без постоянного удержания соединения.
|
||||||
|
</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="prepare-sign-btn" class="btn primary" type="button">Запросить подпись</button>
|
||||||
|
</div>
|
||||||
|
<p class="muted small">
|
||||||
|
Сам signaling подтверждения подписи ещё не доделан. Сейчас доступен только каркас выбора ключа и устройства.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Войти через другое устройство</div>
|
<div class="card-title">Войти через другое устройство</div>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
|
|||||||
@ -16,8 +16,15 @@ const els = {
|
|||||||
sessionLogin: document.querySelector('#session-login'),
|
sessionLogin: document.querySelector('#session-login'),
|
||||||
sessionId: document.querySelector('#session-id'),
|
sessionId: document.querySelector('#session-id'),
|
||||||
sessionType: document.querySelector('#session-type'),
|
sessionType: document.querySelector('#session-type'),
|
||||||
|
deviceKeyShort: document.querySelector('#device-key-short'),
|
||||||
resumeBtn: document.querySelector('#resume-btn'),
|
resumeBtn: document.querySelector('#resume-btn'),
|
||||||
|
refreshDevicesBtn: document.querySelector('#refresh-devices-btn'),
|
||||||
disconnectBtn: document.querySelector('#disconnect-btn'),
|
disconnectBtn: document.querySelector('#disconnect-btn'),
|
||||||
|
signingCard: document.querySelector('#signing-card'),
|
||||||
|
signKeySelect: document.querySelector('#sign-key-select'),
|
||||||
|
deviceSelect: document.querySelector('#device-select'),
|
||||||
|
homeserverList: document.querySelector('#homeserver-list'),
|
||||||
|
prepareSignBtn: document.querySelector('#prepare-sign-btn'),
|
||||||
connectionPill: document.querySelector('#connection-pill'),
|
connectionPill: document.querySelector('#connection-pill'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,6 +69,34 @@ function formatRemaining(ms) {
|
|||||||
return `${minutes} мин ${seconds} сек`;
|
return `${minutes} мин ${seconds} сек`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shortKey(value, size = 10) {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
return raw ? `${raw.slice(0, size)}...` : '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHomeserverList(items = []) {
|
||||||
|
els.homeserverList.innerHTML = '';
|
||||||
|
if (!items.length) {
|
||||||
|
const empty = document.createElement('p');
|
||||||
|
empty.className = 'muted small';
|
||||||
|
empty.textContent = 'В PDA пока нет опубликованных homeserver-сессий.';
|
||||||
|
els.homeserverList.append(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items.forEach((item) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'summary-row device-row';
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.textContent = `${item.sessionName} (${shortKey(item.sessionPubKeyBase58, 8)})`;
|
||||||
|
const badge = document.createElement('strong');
|
||||||
|
const stateValue = String(item.onlineState || 'unknown');
|
||||||
|
badge.textContent = stateValue;
|
||||||
|
badge.className = `device-state device-state-${stateValue}`;
|
||||||
|
row.append(label, badge);
|
||||||
|
els.homeserverList.append(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function applyState(nextState) {
|
function applyState(nextState) {
|
||||||
state = nextState || state;
|
state = nextState || state;
|
||||||
const serverValue = String(state?.settings?.serverLogin || 'shineupme');
|
const serverValue = String(state?.settings?.serverLogin || 'shineupme');
|
||||||
@ -78,18 +113,46 @@ function applyState(nextState) {
|
|||||||
setStatus(state?.status?.text || '', state?.status?.kind || 'info');
|
setStatus(state?.status?.text || '', state?.status?.kind || 'info');
|
||||||
|
|
||||||
const session = state?.session;
|
const session = state?.session;
|
||||||
|
const walletProfile = state?.walletProfile;
|
||||||
|
const signing = state?.signing || {};
|
||||||
if (session) {
|
if (session) {
|
||||||
els.sessionCard.classList.remove('hidden');
|
els.sessionCard.classList.remove('hidden');
|
||||||
els.sessionLogin.textContent = session.login || '—';
|
els.sessionLogin.textContent = session.login || '—';
|
||||||
els.sessionId.textContent = session.sessionId || '—';
|
els.sessionId.textContent = session.sessionId || '—';
|
||||||
els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—');
|
els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—');
|
||||||
|
els.deviceKeyShort.textContent = shortKey(walletProfile?.publicKeys?.deviceKeyBase58 || '');
|
||||||
|
els.signingCard.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
els.sessionCard.classList.add('hidden');
|
els.sessionCard.classList.add('hidden');
|
||||||
els.sessionLogin.textContent = '—';
|
els.sessionLogin.textContent = '—';
|
||||||
els.sessionId.textContent = '—';
|
els.sessionId.textContent = '—';
|
||||||
els.sessionType.textContent = 'wallet';
|
els.sessionType.textContent = 'wallet';
|
||||||
|
els.deviceKeyShort.textContent = '—';
|
||||||
|
els.signingCard.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signKeyOptions = Array.isArray(walletProfile?.signingKeyOptions) ? walletProfile.signingKeyOptions : [];
|
||||||
|
els.signKeySelect.innerHTML = '';
|
||||||
|
signKeyOptions.forEach((item) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = item.id;
|
||||||
|
option.textContent = item.label;
|
||||||
|
option.selected = item.id === signing.selectedKeyId;
|
||||||
|
els.signKeySelect.append(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : [];
|
||||||
|
els.deviceSelect.innerHTML = '';
|
||||||
|
homeservers.forEach((item) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = item.sessionName;
|
||||||
|
option.textContent = `${item.sessionName} [${item.onlineState || 'unknown'}]`;
|
||||||
|
option.selected = item.sessionName === signing.selectedDeviceName;
|
||||||
|
els.deviceSelect.append(option);
|
||||||
|
});
|
||||||
|
renderHomeserverList(homeservers);
|
||||||
|
els.prepareSignBtn.disabled = !session || !signing.selectedKeyId || !signing.selectedDeviceName;
|
||||||
|
|
||||||
const pairing = state?.pairing || {};
|
const pairing = state?.pairing || {};
|
||||||
if (pairing.active) {
|
if (pairing.active) {
|
||||||
els.pairingCard.classList.remove('hidden');
|
els.pairingCard.classList.remove('hidden');
|
||||||
@ -201,6 +264,35 @@ async function disconnectSession() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshDevices() {
|
||||||
|
setStatus('Обновляем trusted homeserver-устройства...', 'info');
|
||||||
|
try {
|
||||||
|
await sendMessage('wallet:refreshWalletDevices');
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error.message || 'Не удалось обновить список устройств.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSigningSelection() {
|
||||||
|
try {
|
||||||
|
await sendMessage('wallet:updateSigningSelection', {
|
||||||
|
selectedKeyId: String(els.signKeySelect.value || '').trim(),
|
||||||
|
selectedDeviceName: String(els.deviceSelect.value || '').trim(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error.message || 'Не удалось обновить выбор для подписи.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareSignSignal() {
|
||||||
|
setStatus('Готовим каркас запроса подписи...', 'info');
|
||||||
|
try {
|
||||||
|
await sendMessage('wallet:prepareSignSignal');
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(error.message || 'Не удалось подготовить запрос подписи.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function startUiRefreshLoop() {
|
function startUiRefreshLoop() {
|
||||||
stopUiRefreshLoop();
|
stopUiRefreshLoop();
|
||||||
refreshTimer = window.setInterval(() => {
|
refreshTimer = window.setInterval(() => {
|
||||||
@ -229,7 +321,11 @@ function bindUi() {
|
|||||||
els.startBtn.addEventListener('click', () => { void startPairing(); });
|
els.startBtn.addEventListener('click', () => { void startPairing(); });
|
||||||
els.cancelBtn.addEventListener('click', () => { void cancelPairing(); });
|
els.cancelBtn.addEventListener('click', () => { void cancelPairing(); });
|
||||||
els.resumeBtn.addEventListener('click', () => { void resumeSession(); });
|
els.resumeBtn.addEventListener('click', () => { void resumeSession(); });
|
||||||
|
els.refreshDevicesBtn.addEventListener('click', () => { void refreshDevices(); });
|
||||||
els.disconnectBtn.addEventListener('click', () => { void disconnectSession(); });
|
els.disconnectBtn.addEventListener('click', () => { void disconnectSession(); });
|
||||||
|
els.signKeySelect.addEventListener('change', () => { void updateSigningSelection(); });
|
||||||
|
els.deviceSelect.addEventListener('change', () => { void updateSigningSelection(); });
|
||||||
|
els.prepareSignBtn.addEventListener('click', () => { void prepareSignSignal(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.206
|
client.version=1.2.207
|
||||||
server.version=1.2.195
|
server.version=1.2.196
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<link rel="manifest" href="./manifest.webmanifest" />
|
<link rel="manifest" href="./manifest.webmanifest" />
|
||||||
<title>Shine UI Demo</title>
|
<title>Shine UI Demo</title>
|
||||||
<script>
|
<script>
|
||||||
window.__SHINE_BUILD_HASH__ = '20260616124000';
|
window.__SHINE_BUILD_HASH__ = '20260616130000';
|
||||||
window.__SHINE_CLIENT_VERSION__ = '1.2.8';
|
window.__SHINE_CLIENT_VERSION__ = '1.2.8';
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -41,6 +41,16 @@ export function render({ navigate }) {
|
|||||||
statusText.className = 'meta-muted';
|
statusText.className = 'meta-muted';
|
||||||
statusText.textContent = 'Проверка логина: не выполнена';
|
statusText.textContent = 'Проверка логина: не выполнена';
|
||||||
|
|
||||||
|
const serverNotice = document.createElement('div');
|
||||||
|
serverNotice.className = 'card stack';
|
||||||
|
serverNotice.innerHTML = `
|
||||||
|
<p class="field-label">Первый сервер SHiNE</p>
|
||||||
|
<p class="meta-muted">Сейчас вашим первым и основным сервером будет серверный аккаунт <strong>${state.entrySettings.shineServerLogin || 'shineupme'}</strong>.</p>
|
||||||
|
<p class="meta-muted">Текущий адрес этого сервера: <strong>${state.entrySettings.shineServerHttp || 'https://shineup.me'}</strong>.</p>
|
||||||
|
<p class="meta-muted">При регистрации этот сервер будет записан в вашу PDA как первый сервер доступа.</p>
|
||||||
|
<p class="meta-muted">При желании его можно изменить в настройках до регистрации или позже. В будущем будет поддерживаться мультисерверная модель, но сейчас используется один основной сервер.</p>
|
||||||
|
`;
|
||||||
|
|
||||||
const formError = document.createElement('p');
|
const formError = document.createElement('p');
|
||||||
formError.className = 'status-line is-unavailable';
|
formError.className = 'status-line is-unavailable';
|
||||||
formError.style.display = 'none';
|
formError.style.display = 'none';
|
||||||
@ -201,7 +211,7 @@ export function render({ navigate }) {
|
|||||||
`;
|
`;
|
||||||
form.children[0].append(loginInput);
|
form.children[0].append(loginInput);
|
||||||
form.children[1].append(passwordInput);
|
form.children[1].append(passwordInput);
|
||||||
form.append(checkButton, statusText, advanced, formError);
|
form.append(serverNotice, checkButton, statusText, advanced, formError);
|
||||||
actions.innerHTML = '';
|
actions.innerHTML = '';
|
||||||
actions.append(backButton, nextButton);
|
actions.append(backButton, nextButton);
|
||||||
backButton.disabled = false;
|
backButton.disabled = false;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user