import { createRequesterPairingMaterial, decryptPairingPayloadFromEnvelope, deriveEspPairingPasswordHash } from './js/lib/device-pairing.js'; import { loadPluginSettings, loadSessionMaterial, savePluginSettings, saveSessionMaterial, clearSessionMaterial } from './js/lib/session-store.js'; import { ShineApiClient } from './js/lib/shine-api.js'; import { DEFAULT_SHINE_SERVER_LOGIN, buildHttpBase, normalizeServerLogin, resolveShineServerByServerLogin, } from './js/lib/shine-server-resolver.js'; const state = { api: null, settings: { serverLogin: DEFAULT_SHINE_SERVER_LOGIN, serverHttp: buildHttpBase('shineup.me'), serverUrl: 'wss://shineup.me/ws', login: '', }, requesterMaterial: null, pairingId: '', expiresAtMs: 0, shortCode: '', trustedSessionOnline: false, pollTimer: 0, activeSession: null, connectionOnline: false, statusText: '', statusKind: 'info', }; function setStatus(message = '', kind = 'info') { state.statusText = String(message || ''); state.statusKind = kind === 'error' ? 'error' : 'info'; } function stopPoll() { if (state.pollTimer) { clearTimeout(state.pollTimer); state.pollTimer = 0; } } function clearPairingState() { stopPoll(); state.requesterMaterial = null; state.pairingId = ''; state.expiresAtMs = 0; state.shortCode = ''; state.trustedSessionOnline = false; } function ensureApi(serverUrl = state.settings.serverUrl) { const normalized = String(serverUrl || '').trim() || 'wss://shineup.me/ws'; if (!state.api || state.api.serverUrl !== normalized) { state.api?.close(); state.api = new ShineApiClient(normalized); } 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, 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(), }; state.activeSession = await loadSessionMaterial(); } async function persistSettings(nextSettings = {}) { const resolved = await resolveSettingsServer(nextSettings); state.settings = { ...state.settings, ...nextSettings, ...resolved, }; await savePluginSettings(state.settings); return state.settings; } async function resumeActiveSession() { const sessionRecord = await loadSessionMaterial(); state.activeSession = sessionRecord; if (!sessionRecord) { state.connectionOnline = false; setStatus('Wallet-session ещё не подключена.', 'info'); return { ok: true, connected: false }; } try { await persistSettings({ serverLogin: String(sessionRecord?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(), serverHttp: String(sessionRecord?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim(), serverUrl: String(sessionRecord?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim(), login: String(sessionRecord?.login || state.settings.login || '').trim(), }); const resumed = await ensureApi().resumeSession(sessionRecord); state.connectionOnline = true; setStatus(`Wallet-session активна для @${resumed.login}.`, 'info'); return { ok: true, connected: true, login: resumed.login, sessionId: resumed.sessionId }; } catch (error) { state.connectionOnline = false; setStatus(error.message || 'Не удалось восстановить wallet-session.', 'error'); return { ok: false, connected: false, error: state.statusText }; } } async function attachApprovedSession(payload) { if (String(payload?.type || '') !== 'shine-esp-session-attach') { throw new Error('Доверенное устройство вернуло неподдерживаемый payload. Для plugin нужен session-only approve.'); } const login = String(payload?.login || state.settings.login || '').trim(); const approvedSession = payload?.session || {}; const sessionRecord = { login, sessionId: String(approvedSession?.sessionId || '').trim(), sessionKey: state.requesterMaterial?.sessionKey || '', sessionPrivPkcs8: state.requesterMaterial?.sessionPrivPkcs8 || '', sessionType: Number(approvedSession?.sessionType || 50) || 50, serverLogin: state.settings.serverLogin, serverHttp: state.settings.serverHttp, serverUrl: state.settings.serverUrl, }; if (!sessionRecord.login || !sessionRecord.sessionId || !sessionRecord.sessionKey || !sessionRecord.sessionPrivPkcs8) { throw new Error('Получен неполный session-only payload'); } await clearSessionMaterial(); await saveSessionMaterial(sessionRecord); state.activeSession = sessionRecord; await persistSettings({ login: sessionRecord.login, serverUrl: sessionRecord.serverUrl, }); await resumeActiveSession(); } async function pollPairingStatus() { if (!state.pairingId || !state.requesterMaterial) return; try { const payload = await ensureApi().getEspPairingStatus(state.pairingId); const stateValue = String(payload?.state || ''); if (stateValue === 'created') { state.pollTimer = setTimeout(() => { void pollPairingStatus(); }, 2200); return; } if (stateValue === 'approved') { const decoded = await decryptPairingPayloadFromEnvelope(payload?.encryptedPayload, state.requesterMaterial); await attachApprovedSession(decoded); clearPairingState(); setStatus('Wallet-session создана и сохранена.', 'info'); return; } if (stateValue === 'rejected') { clearPairingState(); setStatus('Заявка отклонена на доверенном устройстве.', 'error'); return; } if (stateValue === 'expired' || stateValue === 'canceled') { clearPairingState(); setStatus('Ожидание подключения завершено.', 'error'); return; } state.pollTimer = setTimeout(() => { void pollPairingStatus(); }, 2200); } catch (error) { clearPairingState(); setStatus(error.message || 'Не удалось проверить pairing-статус.', 'error'); } } async function startPairing({ login, usePassword, password, serverLogin }) { 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, }); clearPairingState(); setStatus('Проверяем пользователя и создаём wallet-session заявку...', 'info'); const api = ensureApi(); const user = await api.getUser(cleanLogin); if (user?.exists !== true) { throw new Error('Пользователь не найден.'); } state.requesterMaterial = await createRequesterPairingMaterial(); const passwordHash = usePassword ? await deriveEspPairingPasswordHash(cleanLogin, String(password || '')) : ''; const payload = await api.startEspPairing({ login: cleanLogin, passwordHash, requesterSessionKey: state.requesterMaterial.sessionKey, payloadType: 1, }); state.pairingId = String(payload?.pairingId || '').trim(); state.expiresAtMs = Number(payload?.expiresAtMs || 0); state.shortCode = String(payload?.shortCode || '0000000'); state.trustedSessionOnline = !!payload?.trustedSessionOnline; if (!state.pairingId) { throw new Error('Сервер не вернул pairingId.'); } state.pollTimer = setTimeout(() => { void pollPairingStatus(); }, 1800); setStatus('Wallet-session заявка создана. Ожидаем подтверждение на доверенном устройстве.', 'info'); return { pairingId: state.pairingId, shortCode: String(payload?.shortCode || '0000000'), expiresAtMs: state.expiresAtMs, trustedSessionOnline: !!payload?.trustedSessionOnline, }; } async function cancelPairing() { if (!state.pairingId || !state.requesterMaterial?.sessionKey) { clearPairingState(); return { ok: true }; } await ensureApi().cancelEspPairing(state.pairingId, state.requesterMaterial.sessionKey); clearPairingState(); setStatus('Ожидание подключения отменено.', 'info'); return { ok: true }; } async function disconnectSession() { await clearSessionMaterial(); state.activeSession = null; state.connectionOnline = false; setStatus('Сохранённая wallet-session удалена из plugin.', 'info'); return { ok: true }; } function snapshot() { return { settings: { ...state.settings }, pairing: { active: !!state.pairingId, pairingId: state.pairingId, expiresAtMs: state.expiresAtMs, shortCode: state.shortCode, trustedSessionOnline: state.trustedSessionOnline, }, session: state.activeSession ? { ...state.activeSession } : null, connectionOnline: state.connectionOnline, status: { text: state.statusText, kind: state.statusKind, }, }; } chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { (async () => { const type = String(message?.type || ''); if (type === 'wallet:getState') { await loadStateFromStorage(); sendResponse({ ok: true, state: snapshot() }); return; } if (type === 'wallet:saveSettings') { await persistSettings(message?.payload || {}); sendResponse({ ok: true, state: snapshot() }); return; } if (type === 'wallet:startPairing') { const result = await startPairing(message?.payload || {}); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:cancelPairing') { const result = await cancelPairing(); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:resumeSession') { const result = await resumeActiveSession(); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:disconnectSession') { const result = await disconnectSession(); sendResponse({ ok: true, result, state: snapshot() }); return; } sendResponse({ ok: false, error: 'UNKNOWN_MESSAGE' }); })().catch((error) => { setStatus(error?.message || 'Unknown error', 'error'); sendResponse({ ok: false, error: error?.message || 'Unknown error', state: snapshot() }); }); return true; }); void loadStateFromStorage().then(() => resumeActiveSession()).catch((error) => { setStatus(error?.message || 'Не удалось инициализировать wallet plugin.', 'error'); });