SHA256
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:
@@ -4,7 +4,7 @@ Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login.
|
||||
|
||||
## Что уже умеет
|
||||
|
||||
- создать `wallet-session` через `StartEspPairing`;
|
||||
- создать `wallet-session` через `StartTrustedDeviceLogin`;
|
||||
- показать код подключения;
|
||||
- дождаться подтверждения на доверенном устройстве;
|
||||
- принять `session-only` payload без передачи `deviceKey/rootKey/blockchainKey`;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -39,8 +39,8 @@ export class ShineApiClient {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async startEspPairing({ login, passwordHash, requesterSessionKey, payloadType = 1 }) {
|
||||
const response = await this.ws.request('StartEspPairing', {
|
||||
async startTrustedDeviceLogin({ login, passwordHash, requesterSessionKey, payloadType = 1 }) {
|
||||
const response = await this.ws.request('StartTrustedDeviceLogin', {
|
||||
login: String(login || '').trim(),
|
||||
passwordHash: String(passwordHash || '').trim(),
|
||||
requesterSessionKey: String(requesterSessionKey || '').trim(),
|
||||
@@ -48,24 +48,24 @@ export class ShineApiClient {
|
||||
requesterClientPlatform: 'Chrome Extension Wallet',
|
||||
payloadType: Number(payloadType) || 1,
|
||||
});
|
||||
if (response.status !== 200) throw opError('StartEspPairing', response);
|
||||
if (response.status !== 200) throw opError('StartTrustedDeviceLogin', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async getEspPairingStatus(pairingId) {
|
||||
const response = await this.ws.request('GetEspPairingStatus', {
|
||||
async getTrustedDeviceLoginStatus(pairingId) {
|
||||
const response = await this.ws.request('GetTrustedDeviceLoginStatus', {
|
||||
pairingId: String(pairingId || '').trim(),
|
||||
});
|
||||
if (response.status !== 200) throw opError('GetEspPairingStatus', response);
|
||||
if (response.status !== 200) throw opError('GetTrustedDeviceLoginStatus', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async cancelEspPairing(pairingId, requesterSessionKey) {
|
||||
const response = await this.ws.request('CancelEspPairing', {
|
||||
async cancelTrustedDeviceLogin(pairingId, requesterSessionKey) {
|
||||
const response = await this.ws.request('CancelTrustedDeviceLogin', {
|
||||
pairingId: String(pairingId || '').trim(),
|
||||
requesterSessionKey: String(requesterSessionKey || '').trim(),
|
||||
});
|
||||
if (response.status !== 200) throw opError('CancelEspPairing', response);
|
||||
if (response.status !== 200) throw opError('CancelTrustedDeviceLogin', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
|
||||
@@ -204,6 +204,25 @@ export async function resolveShineServerByServerLogin(serverLogin, solanaEndpoin
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveShineServerByUserLogin(login, solanaEndpoint = SOLANA_ENDPOINT_DEFAULT) {
|
||||
const cleanLogin = normalizeServerLogin(login);
|
||||
if (!cleanLogin) throw new Error('Не указан логин пользователя.');
|
||||
const parsed = await fetchUserPda(cleanLogin, solanaEndpoint);
|
||||
const serverLogin = normalizeServerLogin(parsed.accessServers?.[0] || '');
|
||||
if (!serverLogin) {
|
||||
throw new Error(`У пользователя @${cleanLogin} в PDA не найден первый сервер доступа.`);
|
||||
}
|
||||
const resolved = await resolveShineServerByServerLogin(serverLogin, solanaEndpoint);
|
||||
return {
|
||||
login: cleanLogin,
|
||||
accessServers: parsed.accessServers,
|
||||
serverLogin: resolved.serverLogin,
|
||||
serverAddress: resolved.serverAddress,
|
||||
serverHttp: resolved.serverHttp,
|
||||
serverUrl: resolved.serverUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export async function readWalletProfileByLogin(login, solanaEndpoint = SOLANA_ENDPOINT_DEFAULT) {
|
||||
const cleanLogin = normalizeServerLogin(login);
|
||||
const parsed = await fetchUserPda(cleanLogin, solanaEndpoint);
|
||||
|
||||
@@ -17,11 +17,8 @@
|
||||
<span id="connection-pill" class="pill pill-offline">offline</span>
|
||||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span>Логин сервера SHiNE</span>
|
||||
<input id="server-url" type="text" placeholder="shineupme" />
|
||||
</label>
|
||||
<p id="server-address" class="muted small">Текущий адрес: https://shineup.me</p>
|
||||
<p id="server-login-info" class="muted small">Сервер SHiNE: —</p>
|
||||
<p id="server-address" class="muted small">Адрес: —</p>
|
||||
|
||||
<div id="session-card" class="card hidden">
|
||||
<div class="card-title">Подключённая wallet-session</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const els = {
|
||||
serverUrl: document.querySelector('#server-url'),
|
||||
serverLoginInfo: document.querySelector('#server-login-info'),
|
||||
serverAddress: document.querySelector('#server-address'),
|
||||
loginInput: document.querySelector('#login-input'),
|
||||
usePassword: document.querySelector('#use-password'),
|
||||
@@ -99,13 +99,16 @@ function renderHomeserverList(items = []) {
|
||||
|
||||
function applyState(nextState) {
|
||||
state = nextState || state;
|
||||
const serverValue = String(state?.settings?.serverLogin || 'shineupme');
|
||||
const serverAddressValue = String(state?.settings?.serverHttp || 'https://shineup.me');
|
||||
const loginValue = String(state?.settings?.login || '');
|
||||
if (document.activeElement !== els.serverUrl) {
|
||||
els.serverUrl.value = serverValue;
|
||||
const resolvedServerLogin = String(state?.settings?.serverLogin || '').trim();
|
||||
const resolvedServerAddress = String(state?.settings?.serverHttp || '').trim();
|
||||
if (loginValue && resolvedServerLogin && resolvedServerAddress) {
|
||||
els.serverLoginInfo.textContent = `Сервер SHiNE: ${resolvedServerLogin}`;
|
||||
els.serverAddress.textContent = `Адрес: ${resolvedServerAddress}`;
|
||||
} else {
|
||||
els.serverLoginInfo.textContent = 'Сервер SHiNE: —';
|
||||
els.serverAddress.textContent = 'Адрес: —';
|
||||
}
|
||||
els.serverAddress.textContent = `Текущий адрес: ${serverAddressValue}`;
|
||||
if (document.activeElement !== els.loginInput) {
|
||||
els.loginInput.value = loginValue;
|
||||
}
|
||||
@@ -202,11 +205,23 @@ async function refreshState() {
|
||||
|
||||
async function saveSettings() {
|
||||
await sendMessage('wallet:saveSettings', {
|
||||
serverLogin: String(els.serverUrl.value || '').trim(),
|
||||
login: String(els.loginInput.value || '').trim(),
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveServerInfo() {
|
||||
const login = String(els.loginInput.value || '').trim();
|
||||
if (!login) {
|
||||
await sendMessage('wallet:saveSettings', { login: '' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await sendMessage('wallet:resolveServerInfo', { login });
|
||||
} catch (error) {
|
||||
setStatus(error.message || 'Не удалось определить сервер SHiNE по PDA.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSaveSettings() {
|
||||
if (saveSettingsTimer) {
|
||||
window.clearTimeout(saveSettingsTimer);
|
||||
@@ -230,7 +245,6 @@ async function startPairing() {
|
||||
login,
|
||||
usePassword: !!els.usePassword.checked,
|
||||
password: String(els.passwordInput.value || ''),
|
||||
serverLogin: String(els.serverUrl.value || '').trim(),
|
||||
});
|
||||
applyState(response.state);
|
||||
} catch (error) {
|
||||
@@ -314,10 +328,11 @@ function bindUi() {
|
||||
els.passwordInput.value = '';
|
||||
}
|
||||
});
|
||||
els.serverUrl.addEventListener('input', () => { scheduleSaveSettings(); });
|
||||
els.serverUrl.addEventListener('change', () => { void saveSettings(); });
|
||||
els.loginInput.addEventListener('input', () => { scheduleSaveSettings(); });
|
||||
els.loginInput.addEventListener('change', () => { void saveSettings(); });
|
||||
els.loginInput.addEventListener('change', () => {
|
||||
void saveSettings();
|
||||
void resolveServerInfo();
|
||||
});
|
||||
els.startBtn.addEventListener('click', () => { void startPairing(); });
|
||||
els.cancelBtn.addEventListener('click', () => { void cancelPairing(); });
|
||||
els.resumeBtn.addEventListener('click', () => { void resumeSession(); });
|
||||
|
||||
Reference in New Issue
Block a user