SHA256
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:
@@ -5,6 +5,7 @@ import {
|
||||
DEFAULT_SHINE_SERVER_LOGIN,
|
||||
buildHttpBase,
|
||||
normalizeServerLogin,
|
||||
readWalletProfileByLogin,
|
||||
resolveShineServerByServerLogin,
|
||||
} from './js/lib/shine-server-resolver.js';
|
||||
|
||||
@@ -24,6 +25,12 @@ const state = {
|
||||
pollTimer: 0,
|
||||
activeSession: null,
|
||||
connectionOnline: false,
|
||||
walletProfile: null,
|
||||
signing: {
|
||||
selectedKeyId: 'device',
|
||||
selectedDeviceName: '',
|
||||
devicesResolvedAtMs: 0,
|
||||
},
|
||||
statusText: '',
|
||||
statusKind: 'info',
|
||||
};
|
||||
@@ -80,6 +87,12 @@ async function loadStateFromStorage() {
|
||||
login: String(settings?.login || '').trim(),
|
||||
};
|
||||
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 = {}) {
|
||||
@@ -93,7 +106,89 @@ async function persistSettings(nextSettings = {}) {
|
||||
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();
|
||||
state.activeSession = sessionRecord;
|
||||
if (!sessionRecord) {
|
||||
@@ -110,8 +205,14 @@ async function resumeActiveSession() {
|
||||
login: String(sessionRecord?.login || state.settings.login || '').trim(),
|
||||
});
|
||||
const resumed = await ensureApi().resumeSession(sessionRecord);
|
||||
state.connectionOnline = true;
|
||||
setStatus(`Wallet-session активна для @${resumed.login}.`, 'info');
|
||||
state.connectionOnline = !!keepConnected;
|
||||
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 };
|
||||
} catch (error) {
|
||||
state.connectionOnline = false;
|
||||
@@ -142,13 +243,14 @@ async function attachApprovedSession(payload) {
|
||||
}
|
||||
|
||||
await clearSessionMaterial();
|
||||
await saveSessionMaterial(sessionRecord);
|
||||
state.activeSession = sessionRecord;
|
||||
await hydrateWalletProfile(login);
|
||||
await saveActiveSessionRecord();
|
||||
await persistSettings({
|
||||
login: sessionRecord.login,
|
||||
serverUrl: sessionRecord.serverUrl,
|
||||
});
|
||||
await resumeActiveSession();
|
||||
state.connectionOnline = false;
|
||||
}
|
||||
|
||||
async function pollPairingStatus() {
|
||||
@@ -166,7 +268,7 @@ async function pollPairingStatus() {
|
||||
const decoded = await decryptPairingPayloadFromEnvelope(payload?.encryptedPayload, state.requesterMaterial);
|
||||
await attachApprovedSession(decoded);
|
||||
clearPairingState();
|
||||
setStatus('Wallet-session создана и сохранена.', 'info');
|
||||
setStatus('Wallet-session создана и сохранена. Кошелёк остаётся офлайн до запроса подписи.', 'info');
|
||||
return;
|
||||
}
|
||||
if (stateValue === 'rejected') {
|
||||
@@ -251,13 +353,87 @@ async function cancelPairing() {
|
||||
}
|
||||
|
||||
async function disconnectSession() {
|
||||
ensureApi().close();
|
||||
state.api = null;
|
||||
await clearSessionMaterial();
|
||||
state.activeSession = null;
|
||||
state.connectionOnline = false;
|
||||
state.walletProfile = null;
|
||||
state.signing = {
|
||||
selectedKeyId: 'device',
|
||||
selectedDeviceName: '',
|
||||
devicesResolvedAtMs: 0,
|
||||
};
|
||||
setStatus('Сохранённая wallet-session удалена из plugin.', 'info');
|
||||
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() {
|
||||
return {
|
||||
settings: { ...state.settings },
|
||||
@@ -270,6 +446,8 @@ function snapshot() {
|
||||
},
|
||||
session: state.activeSession ? { ...state.activeSession } : null,
|
||||
connectionOnline: state.connectionOnline,
|
||||
walletProfile: state.walletProfile ? { ...state.walletProfile } : null,
|
||||
signing: { ...state.signing },
|
||||
status: {
|
||||
text: state.statusText,
|
||||
kind: state.statusKind,
|
||||
@@ -305,6 +483,21 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
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') {
|
||||
const result = await disconnectSession();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
@@ -318,6 +511,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user