SHA256
Wallet-session pairing и browser plugin wallet, оплаты пока не работают
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
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';
|
||||
|
||||
const state = {
|
||||
api: null,
|
||||
settings: {
|
||||
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 loadStateFromStorage() {
|
||||
const settings = await loadPluginSettings();
|
||||
state.settings = {
|
||||
serverUrl: String(settings?.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws',
|
||||
login: String(settings?.login || '').trim(),
|
||||
};
|
||||
state.activeSession = await loadSessionMaterial();
|
||||
}
|
||||
|
||||
async function persistSettings(nextSettings = {}) {
|
||||
state.settings = {
|
||||
...state.settings,
|
||||
...nextSettings,
|
||||
};
|
||||
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({
|
||||
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,
|
||||
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, serverUrl }) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
if (!cleanLogin) {
|
||||
throw new Error('Введите логин.');
|
||||
}
|
||||
|
||||
await persistSettings({
|
||||
serverUrl: String(serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws',
|
||||
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');
|
||||
});
|
||||
Reference in New Issue
Block a user