SHA256
- styles/main.css: роли цветов (по умолчанию «Индиго»), шкала отступов/скруглений/шрифтов, цвета отношений для обеих тем; жёсткие цвета в стилях заменены на роли. - Палитра: пресеты и личные правки, «Оформление» Авто/День/Ночь, долгое нажатие — редактор цветов с экспортом/импортом. - Каналы: пузыри постов автора, плашки дней, строка «Написать в канал…», «О канале», создание канала с адресом из названия; лента открывается на свежих постах. - Чаты: плоский список, чипы-фильтры, пузыри, плашки дней; нижняя панель скрыта в переписке. - Корневые разделы — единая шапка; профиль и чужой профиль — общая карточка; настройки, кошелёк, сеансы — меню-списки. - Нижняя панель: иконки без подписей, бейджи на иконках. - confirmDialog вместо window.confirm/alert; на телефоне диалоги — шторки снизу. - docs/UI-Design/ISSUES-for-dev.md — найденные ошибки сервера/UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
2.8 KiB
JavaScript
88 lines
2.8 KiB
JavaScript
import qrcode from '../vendor-qrcode-generator.js';
|
|
|
|
const TRANSFER_PREFIX = 'shine-key-transfer-v1:';
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder();
|
|
|
|
function bytesToBase64Url(bytes) {
|
|
let binary = '';
|
|
bytes.forEach((b) => {
|
|
binary += String.fromCharCode(b);
|
|
});
|
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
}
|
|
|
|
function base64UrlToBytes(value) {
|
|
const normalized = String(value || '').trim().replace(/-/g, '+').replace(/_/g, '/');
|
|
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
|
|
const binary = atob(padded);
|
|
const out = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
|
|
return out;
|
|
}
|
|
|
|
export function keyLabel(id) {
|
|
if (id === 'root') return 'root';
|
|
if (id === 'blockchain') return 'blockchain';
|
|
if (id === 'device') return 'device';
|
|
return id;
|
|
}
|
|
|
|
export function describeTransferKeys(keys = {}) {
|
|
const out = [];
|
|
if (keys.clientKey) out.push('device');
|
|
if (keys.blockchainKey) out.push('blockchain');
|
|
if (keys.rootKey) out.push('root');
|
|
return out;
|
|
}
|
|
|
|
export function makeKeyTransferText({ login, keys }) {
|
|
const payload = {
|
|
v: 1,
|
|
type: 'shine-key-transfer',
|
|
login: String(login || '').trim(),
|
|
keys: {
|
|
clientKey: String(keys?.clientKey || ''),
|
|
blockchainKey: String(keys?.blockchainKey || ''),
|
|
rootKey: String(keys?.rootKey || ''),
|
|
},
|
|
createdAtMs: Date.now(),
|
|
};
|
|
const json = JSON.stringify(payload);
|
|
return `${TRANSFER_PREFIX}${bytesToBase64Url(encoder.encode(json))}`;
|
|
}
|
|
|
|
export function parseKeyTransferText(text) {
|
|
const raw = String(text || '').trim();
|
|
if (!raw.startsWith(TRANSFER_PREFIX)) {
|
|
throw new Error('Это не QR-код переноса ключей Сияния');
|
|
}
|
|
const json = decoder.decode(base64UrlToBytes(raw.slice(TRANSFER_PREFIX.length)));
|
|
const payload = JSON.parse(json);
|
|
if (payload?.v !== 1 || payload?.type !== 'shine-key-transfer') {
|
|
throw new Error('Неподдерживаемый формат QR-кода');
|
|
}
|
|
const login = String(payload.login || '').trim();
|
|
if (!login) throw new Error('В QR-коде нет логина');
|
|
const keys = payload.keys && typeof payload.keys === 'object' ? payload.keys : {};
|
|
if (!keys.clientKey && !keys.blockchainKey && !keys.rootKey) {
|
|
throw new Error('В QR-коде нет ключей');
|
|
}
|
|
return {
|
|
login,
|
|
keys: {
|
|
clientKey: String(keys.clientKey || ''),
|
|
blockchainKey: String(keys.blockchainKey || ''),
|
|
rootKey: String(keys.rootKey || ''),
|
|
},
|
|
keyTypes: describeTransferKeys(keys),
|
|
};
|
|
}
|
|
|
|
export function renderQrSvg(text, { cellSize = 4, margin = 4 } = {}) {
|
|
const qr = qrcode(0, 'L');
|
|
qr.addData(String(text || ''), 'Byte');
|
|
qr.make();
|
|
return qr.createSvgTag(cellSize, margin);
|
|
}
|