import { importPkcs8Ed25519, signBase64 } from './crypto-utils.js'; import { WsJsonClient } from './ws-client.js'; const SESSION_TYPE_WALLET = 50; function normalizeServerUrl(url) { const value = String(url || '').trim(); if (!value) return 'wss://shineup.me/ws'; if (value.startsWith('ws://') || value.startsWith('wss://')) return value; if (value.startsWith('http://') || value.startsWith('https://')) { const parsed = new URL(value); parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:'; if (!parsed.pathname || parsed.pathname === '/') parsed.pathname = '/ws'; return parsed.toString(); } return value; } function opError(op, response) { const payload = response?.payload || {}; const message = payload?.message || response?.message || payload?.error || response?.error || 'Unknown server error'; const code = String(payload?.code || response?.code || payload?.error || response?.error || 'UNKNOWN').toUpperCase(); const error = new Error(`${op}: ${message} (${code})`); error.op = op; error.code = code; error.status = response?.status || 0; return error; } export class ShineApiClient { constructor(serverUrl) { this.serverUrl = normalizeServerUrl(serverUrl); this.ws = new WsJsonClient(this.serverUrl); } async getUser(login) { const response = await this.ws.request('GetUser', { login: String(login || '').trim() }); if (response.status !== 200) throw opError('GetUser', response); return response.payload || {}; } 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(), requesterSessionType: SESSION_TYPE_WALLET, requesterClientPlatform: 'Chrome Extension Wallet', payloadType: Number(payloadType) || 1, }); if (response.status !== 200) throw opError('StartTrustedDeviceLogin', response); return response.payload || {}; } async getTrustedDeviceLoginStatus(pairingId) { const response = await this.ws.request('GetTrustedDeviceLoginStatus', { pairingId: String(pairingId || '').trim(), }); if (response.status !== 200) throw opError('GetTrustedDeviceLoginStatus', response); return response.payload || {}; } 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('CancelTrustedDeviceLogin', response); return response.payload || {}; } async listSessions() { const response = await this.ws.request('ListSessions', {}); if (response.status !== 200) throw opError('ListSessions', response); return Array.isArray(response?.payload?.sessions) ? response.payload.sessions : []; } async callSignalToSession({ toLogin, targetSessionId, callId, type, data = '' }) { const response = await this.ws.request('CallSignalToSession', { toLogin: String(toLogin || '').trim(), targetSessionId: String(targetSessionId || '').trim(), callId: String(callId || '').trim(), type: Number(type) || 0, data: String(data || ''), }); if (response.status !== 200) throw opError('CallSignalToSession', response); return response.payload || {}; } onEvent(op, handler) { return this.ws.on(op, handler); } async resumeSession(sessionRecord) { const login = String(sessionRecord?.login || '').trim(); const sessionId = String(sessionRecord?.sessionId || '').trim(); const sessionKey = String(sessionRecord?.sessionKey || '').trim(); const sessionPrivPkcs8 = String(sessionRecord?.sessionPrivPkcs8 || '').trim(); if (!login || !sessionId || !sessionKey || !sessionPrivPkcs8) { throw new Error('Сохранённая wallet-session неполная'); } const privateKey = await importPkcs8Ed25519(sessionPrivPkcs8); const challengeResp = await this.ws.request('SessionChallenge', { sessionId }); if (challengeResp.status !== 200) throw opError('SessionChallenge', challengeResp); const nonce = challengeResp?.payload?.nonce; if (!nonce) throw new Error('SessionChallenge: сервер не вернул nonce'); const timeMs = Date.now(); const preimage = `SESSION_LOGIN:${sessionId}:${timeMs}:${nonce}`; const signatureB64 = await signBase64(privateKey, preimage); const loginResp = await this.ws.request('SessionLogin', { sessionId, sessionKey, timeMs, signatureB64, sessionType: Number(sessionRecord?.sessionType || SESSION_TYPE_WALLET) || SESSION_TYPE_WALLET, clientPlatform: 'Chrome Extension Wallet', clientInfo: 'SHiNE Browser Plugin Wallet', }); if (loginResp.status !== 200) throw opError('SessionLogin', loginResp); return { login, sessionId, storagePwd: String(loginResp?.payload?.storagePwd || '').trim(), }; } close() { this.ws.close(); } }