Wallet-session pairing и browser plugin wallet, оплаты пока не работают

This commit is contained in:
AidarKC
2026-06-16 16:23:08 +04:00
parent 5c155ef503
commit 3efa8bb7ee
41 changed files with 3260 additions and 37 deletions
+97 -18
View File
@@ -1,14 +1,17 @@
import { renderHeader } from '../components/header.js';
import {
authService,
authorizeSession,
refreshSessions,
setAuthError,
setAuthInfo,
state,
terminateCurrentSession,
} from '../state.js';
import { formatRelativeTime, showToast } from '../services/channels-ux.js';
import {
buildSecretsPayload,
buildSessionAttachPayload,
deriveEspPairingPasswordHash,
encryptPairingPayloadForRequester,
} from '../services/device-pairing-service.js';
@@ -53,26 +56,73 @@ function buildTransferKeys(savedKeys, { withExtras = false }) {
function requestCardHtml(request) {
const shortCode = String(request?.shortCode || '').trim() || '0000000';
const client = String(request?.requesterClientPlatform || 'unknown');
const requesterSessionType = Number(request?.requesterSessionType || 0);
const expiresText = request?.expiresAtMs ? formatRelativeTime(request.expiresAtMs) : '—';
const sessionOnly = requesterSessionType === 50;
return `
<div class="card stack" data-pairing-id="${String(request?.pairingId || '')}">
<div class="row" style="align-items:flex-start;">
<div class="stack" style="gap:4px;">
<div style="font-size:28px; font-weight:700; letter-spacing:0.14em;">${shortCode}</div>
<span class="meta-muted">Платформа: ${client}</span>
<span class="meta-muted">Тип сессии: ${requesterSessionType || '—'}</span>
<span class="meta-muted">Тип payload: ${Number(request?.payloadType || 0)}</span>
<span class="meta-muted">Истекает: ${expiresText}</span>
</div>
</div>
<div class="row" style="flex-wrap:wrap;">
<button class="ghost-btn" type="button" data-action="approve-device">Подключить без доп. ключей</button>
<button class="primary-btn" type="button" data-action="approve-full">Подключить и передать ключи</button>
<button class="ghost-btn" type="button" data-action="approve-device">${sessionOnly ? 'Подключить wallet-session' : 'Подключить без доп. ключей'}</button>
${sessionOnly ? '' : '<button class="primary-btn" type="button" data-action="approve-full">Подключить и передать ключи</button>'}
<button class="text-btn" type="button" data-action="reject">Отклонить</button>
</div>
</div>
`;
}
async function restoreAuthorizedSessionForPairing() {
const login = String(state.session.login || '').trim();
const sessionId = String(state.session.sessionId || '').trim();
if (!login || !sessionId) {
throw new Error('Нет активной сохранённой сессии для восстановления pairing-доступа.');
}
const resumed = await authService.resumeSession(login, sessionId);
authorizeSession({
login: resumed.login || login,
sessionId: resumed.sessionId || sessionId,
storagePwd: resumed.storagePwd || state.session.storagePwdInMemory,
});
await refreshSessions().catch(() => {});
return resumed;
}
async function runPairingOpWithSessionRestore(runAction) {
try {
return await runAction();
} catch (error) {
const code = String(error?.code || '').trim().toUpperCase();
if (code !== 'PAIRING_REQUIRES_AUTH_SESSION' && code !== 'NOT_AUTHENTICATED') {
throw error;
}
try {
await restoreAuthorizedSessionForPairing();
} catch (restoreError) {
const restoreCode = String(restoreError?.code || '').trim().toUpperCase();
if (restoreCode === 'SESSION_NOT_FOUND'
|| restoreCode === 'SESSION_KEY_NOT_ACTUAL'
|| restoreCode === 'SESSION_OF_ANOTHER_USER') {
await terminateCurrentSession({
infoMessage: 'Сохранённая сессия устарела. Выполните вход заново.',
});
}
throw restoreError;
}
return runAction();
}
}
function makePasswordToggleIcons() {
return {
eye: `
@@ -154,7 +204,7 @@ export function render({ navigate }) {
</label>
<button class="ghost-btn" type="button" id="refresh-pairing-requests">Обновить заявки</button>
</div>
<p class="meta-muted">Если код не введён, показываются все активные заявки. Для подключения без доп. ключей передаётся только device key.</p>
<p class="meta-muted">Если код не введён, показываются все активные заявки. Для wallet-заявки можно выпустить отдельную session-only сессию без передачи постоянных ключей.</p>
<div class="stack" id="pairing-requests-list"></div>
`;
@@ -301,11 +351,11 @@ export function render({ navigate }) {
};
const removeAdditionalPassword = async () => {
const payload = await authService.upsertEspPairingSettings({
const payload = await runPairingOpWithSessionRestore(() => authService.upsertEspPairingSettings({
enabled: true,
passwordHash: '',
ttlSeconds: 180,
});
}));
pairingPasswordConfigured = false;
saveLocalPairingPasswordState(state.session.login, state.entrySettings.shineServer, false);
setAuthInfo(`Подключение по коду без дополнительного пароля включено. TTL: ${payload?.ttlSeconds || 180} сек.`);
@@ -417,7 +467,7 @@ export function render({ navigate }) {
const reloadRequests = async ({ silent = false } = {}) => {
try {
requests = await authService.listEspPairingRequests();
requests = await runPairingOpWithSessionRestore(() => authService.listEspPairingRequests());
renderRequests();
if (!silent) {
setStatus(status, 'Список pairing-заявок обновлён.', 'info');
@@ -435,16 +485,45 @@ export function render({ navigate }) {
const approveRequest = async (request, mode) => {
const withExtras = mode === 'with-extras';
const keys = buildTransferKeys(savedKeys, { withExtras });
const payload = buildSecretsPayload({
login: state.session.login,
keys,
mode: withExtras ? 'with-extras' : 'device-only',
});
let payload;
if (!withExtras && Number(request?.requesterSessionType || 0) === 50) {
const delegatedSession = await authService.createDelegatedSessionWithDeviceKey({
login: state.session.login,
devicePrivPkcs8: String(savedKeys?.deviceKey || '').trim(),
sessionKey: String(request?.requesterSessionKey || '').trim(),
sessionType: Number(request?.requesterSessionType || 50) || 50,
clientPlatform: String(request?.requesterClientPlatform || '').trim() || 'Wallet plugin',
clientInfo: 'Wallet session approved via device pairing',
});
payload = buildSessionAttachPayload({
login: state.session.login,
session: delegatedSession,
});
} else {
const keys = buildTransferKeys(savedKeys, { withExtras });
payload = buildSecretsPayload({
login: state.session.login,
keys,
mode: withExtras ? 'with-extras' : 'device-only',
});
}
const encryptedPayload = await encryptPairingPayloadForRequester(request?.requesterSessionKey, payload);
await authService.approveEspPairing(request?.pairingId, encryptedPayload);
showToast(withExtras ? 'Ключи переданы на новое устройство' : 'Новое устройство подключено');
setAuthInfo(withExtras ? 'Заявка подтверждена, ключи переданы.' : 'Заявка подтверждена без передачи доп. ключей.');
await runPairingOpWithSessionRestore(() => authService.approveEspPairing(request?.pairingId, encryptedPayload));
const sessionOnly = !withExtras && Number(request?.requesterSessionType || 0) === 50;
showToast(
withExtras
? 'Ключи переданы на новое устройство'
: sessionOnly
? 'Wallet-session выпущена для нового устройства'
: 'Новое устройство подключено',
);
setAuthInfo(
withExtras
? 'Заявка подтверждена, ключи переданы.'
: sessionOnly
? 'Заявка подтверждена, для нового устройства создана отдельная wallet-session без передачи постоянных ключей.'
: 'Заявка подтверждена без передачи доп. ключей.',
);
await refreshSessions().catch(() => {});
await reloadRequests({ silent: true });
};
@@ -513,11 +592,11 @@ export function render({ navigate }) {
dialogSaveBtn.disabled = true;
try {
const passwordHash = await deriveEspPairingPasswordHash(state.session.login, password);
const payload = await authService.upsertEspPairingSettings({
const payload = await runPairingOpWithSessionRestore(() => authService.upsertEspPairingSettings({
enabled: true,
passwordHash,
ttlSeconds: 180,
});
}));
pairingPasswordConfigured = true;
saveLocalPairingPasswordState(state.session.login, state.entrySettings.shineServer, true);
closePasswordDialog();
@@ -562,7 +641,7 @@ export function render({ navigate }) {
} else if (action === 'approve-full') {
await approveRequest(request, 'with-extras');
} else if (action === 'reject') {
await authService.rejectEspPairing(pairingId, 'rejected_by_user');
await runPairingOpWithSessionRestore(() => authService.rejectEspPairing(pairingId, 'rejected_by_user'));
showToast('Заявка отклонена', { kind: 'error' });
await reloadRequests({ silent: true });
}