TrustedDeviceLogin API и настройки входа через устройство

Что сделано:\n- публичный API сценария входа через доверенное устройство переведён на TrustedDeviceLogin\n- добавлен GetTrustedDeviceLoginSettings\n- отсутствие записи настроек на сервере теперь трактуется как enabled=true и hasPassword=false\n- ttlSeconds убран из клиентского API, TTL заявки фиксирован на сервере: 300 секунд\n- в shine-UI добавлен отдельный экран настроек входа через устройство и статус на основном экране\n- browser wallet переведён на новые TrustedDeviceLogin операции\n- в wallet добавлен выбор rootKey/deviceKey для будущего запроса подписи\n- документация API обновлена\n\nЧто ещё не проверено вручную end-to-end:\n- полный сценарий UI/plugin после этого деплоя не прогонялся руками до конца\n- сам signaling подписи в wallet всё ещё не реализован
This commit is contained in:
AidarKC
2026-06-18 14:19:31 +04:00
parent cf2152dcfc
commit 56db6d0add
29 changed files with 689 additions and 241 deletions
+81 -35
View File
@@ -4,9 +4,8 @@ import { ShineApiClient } from './js/lib/shine-api.js';
import {
DEFAULT_SHINE_SERVER_LOGIN,
buildHttpBase,
normalizeServerLogin,
readWalletProfileByLogin,
resolveShineServerByServerLogin,
resolveShineServerByUserLogin,
} from './js/lib/shine-server-resolver.js';
const state = {
@@ -65,23 +64,10 @@ function ensureApi(serverUrl = state.settings.serverUrl) {
return state.api;
}
async function resolveSettingsServer(nextSettings = {}) {
const serverLogin = normalizeServerLogin(nextSettings?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN)
|| DEFAULT_SHINE_SERVER_LOGIN;
const resolved = await resolveShineServerByServerLogin(serverLogin);
return {
serverLogin: resolved.serverLogin,
serverHttp: resolved.serverHttp,
serverUrl: resolved.serverUrl,
};
}
async function loadStateFromStorage() {
const settings = await loadPluginSettings();
const storedServerLogin = normalizeServerLogin(settings?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN)
|| DEFAULT_SHINE_SERVER_LOGIN;
state.settings = {
serverLogin: storedServerLogin,
serverLogin: String(settings?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(),
serverHttp: String(settings?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim() || buildHttpBase('shineup.me'),
serverUrl: String(settings?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws',
login: String(settings?.login || '').trim(),
@@ -96,16 +82,38 @@ async function loadStateFromStorage() {
}
async function persistSettings(nextSettings = {}) {
const resolved = await resolveSettingsServer(nextSettings);
state.settings = {
...state.settings,
...nextSettings,
...resolved,
};
await savePluginSettings(state.settings);
return state.settings;
}
async function resolveServerForLogin(login) {
const cleanLogin = String(login || state.settings.login || '').trim();
if (!cleanLogin) {
state.settings = {
...state.settings,
login: '',
serverLogin: '',
};
await savePluginSettings(state.settings);
return { ok: true, resolved: false };
}
const resolved = await resolveShineServerByUserLogin(cleanLogin);
state.settings = {
...state.settings,
login: cleanLogin,
serverLogin: resolved.serverLogin,
serverHttp: resolved.serverHttp,
serverUrl: resolved.serverUrl,
};
await savePluginSettings(state.settings);
return { ok: true, resolved: true, ...resolved };
}
async function saveActiveSessionRecord() {
if (!state.activeSession) return;
const nextRecord = {
@@ -124,15 +132,47 @@ function shortKey(value = '', size = 10) {
return raw ? raw.slice(0, size) : '';
}
function extractErrorCode(message = '') {
const match = String(message || '').match(/\(([A-Z0-9_]+)\)\s*$/i);
return match ? String(match[1]).toUpperCase() : '';
}
function toWalletErrorMessage(error, fallback = 'Не удалось выполнить операцию кошелька.') {
const raw = String(error?.message || '').trim();
const code = String(error?.code || extractErrorCode(raw) || '').toUpperCase();
if (code === 'PAIRING_NOT_AVAILABLE') {
return 'Для этого логина ещё не включено подключение по коду. На доверенном устройстве откройте «Подключить по коду» и нажмите «Включить подключение по коду» или задайте дополнительный пароль.';
}
if (code === 'PAIRING_NO_TRUSTED_SESSION_ONLINE') {
return 'Сейчас нет ни одной онлайн доверенной сессии этого пользователя. Откройте SHiNE на другом уже подключённом устройстве и держите его в сети.';
}
if (code === 'PAIRING_PASSWORD_INVALID') {
return 'Дополнительный пароль подключения не подходит.';
}
return raw || fallback;
}
function buildSigningKeyOptions(walletProfile) {
const rootKey = String(walletProfile?.publicKeys?.rootKeyBase58 || '').trim();
const deviceKey = String(walletProfile?.publicKeys?.deviceKeyBase58 || '').trim();
if (!deviceKey) return [];
return [{
id: 'device',
label: `deviceKey (ed25519, ${shortKey(deviceKey)})`,
keyType: 'ed25519',
publicKeyBase58: deviceKey,
}];
const options = [];
if (rootKey) {
options.push({
id: 'root',
label: `rootKey (ed25519, ${shortKey(rootKey)})`,
keyType: 'ed25519',
publicKeyBase58: rootKey,
});
}
if (deviceKey) {
options.push({
id: 'device',
label: `deviceKey (ed25519, ${shortKey(deviceKey)})`,
keyType: 'ed25519',
publicKeyBase58: deviceKey,
});
}
return options;
}
function mergeHomeserverStatuses(publishedHomeservers = [], serverSessions = []) {
@@ -248,6 +288,8 @@ async function attachApprovedSession(payload) {
await saveActiveSessionRecord();
await persistSettings({
login: sessionRecord.login,
serverLogin: sessionRecord.serverLogin,
serverHttp: sessionRecord.serverHttp,
serverUrl: sessionRecord.serverUrl,
});
state.connectionOnline = false;
@@ -256,7 +298,7 @@ async function attachApprovedSession(payload) {
async function pollPairingStatus() {
if (!state.pairingId || !state.requesterMaterial) return;
try {
const payload = await ensureApi().getEspPairingStatus(state.pairingId);
const payload = await ensureApi().getTrustedDeviceLoginStatus(state.pairingId);
const stateValue = String(payload?.state || '');
if (stateValue === 'created') {
state.pollTimer = setTimeout(() => {
@@ -290,16 +332,14 @@ async function pollPairingStatus() {
}
}
async function startPairing({ login, usePassword, password, serverLogin }) {
async function startPairing({ login, usePassword, password }) {
const cleanLogin = String(login || '').trim();
if (!cleanLogin) {
throw new Error('Введите логин.');
}
await persistSettings({
serverLogin: String(serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(),
login: cleanLogin,
});
await persistSettings({ login: cleanLogin });
await resolveServerForLogin(cleanLogin);
clearPairingState();
setStatus('Проверяем пользователя и создаём wallet-session заявку...', 'info');
@@ -313,7 +353,7 @@ async function startPairing({ login, usePassword, password, serverLogin }) {
const passwordHash = usePassword
? await deriveEspPairingPasswordHash(cleanLogin, String(password || ''))
: '';
const payload = await api.startEspPairing({
const payload = await api.startTrustedDeviceLogin({
login: cleanLogin,
passwordHash,
requesterSessionKey: state.requesterMaterial.sessionKey,
@@ -346,7 +386,7 @@ async function cancelPairing() {
clearPairingState();
return { ok: true };
}
await ensureApi().cancelEspPairing(state.pairingId, state.requesterMaterial.sessionKey);
await ensureApi().cancelTrustedDeviceLogin(state.pairingId, state.requesterMaterial.sessionKey);
clearPairingState();
setStatus('Ожидание подключения отменено.', 'info');
return { ok: true };
@@ -468,6 +508,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
sendResponse({ ok: true, state: snapshot() });
return;
}
if (type === 'wallet:resolveServerInfo') {
const result = await resolveServerForLogin(String(message?.payload?.login || '').trim());
sendResponse({ ok: true, result, state: snapshot() });
return;
}
if (type === 'wallet:startPairing') {
const result = await startPairing(message?.payload || {});
sendResponse({ ok: true, result, state: snapshot() });
@@ -505,8 +550,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
}
sendResponse({ ok: false, error: 'UNKNOWN_MESSAGE' });
})().catch((error) => {
setStatus(error?.message || 'Unknown error', 'error');
sendResponse({ ok: false, error: error?.message || 'Unknown error', state: snapshot() });
const message = toWalletErrorMessage(error, 'Unknown error');
setStatus(message, 'error');
sendResponse({ ok: false, error: message, state: snapshot() });
});
return true;
});