SHiNE-server/SHiNE-browser-plugin-wallet/js/lib/shine-api.js

134 lines
5.1 KiB
JavaScript

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();
}
}