Checkpoint: первая рабочая версия звонков, сигналинг будет переделан

This commit is contained in:
AidarKC
2026-05-02 18:13:22 +03:00
parent b05da86197
commit 310863faec
20 changed files with 572 additions and 37 deletions
@@ -1,4 +1,9 @@
import { buildArweaveDataUrl, validateArweaveTxId } from './arweave-file-service.js';
import {
buildArweaveDataUrl,
sha256HexFromArrayBuffer,
validateArweaveTxId,
validateSha256Hex,
} from './arweave-file-service.js';
const DB_NAME = 'shine-ui-avatar-cache';
const DB_VERSION = 1;
@@ -55,6 +60,14 @@ async function putRecord(record) {
});
}
async function verifyBlobSha256(blob, expectedSha256Hex) {
const expected = String(expectedSha256Hex || '').trim().toLowerCase();
if (!validateSha256Hex(expected)) return true;
if (!(blob instanceof Blob)) return false;
const actual = await sha256HexFromArrayBuffer(await blob.arrayBuffer());
return actual === expected;
}
async function getAllRecords() {
return withStore('readonly', (store, _tx, resolve, reject) => {
const req = store.getAll();
@@ -146,22 +159,41 @@ function detectImageMime(bytes) {
return '';
}
async function getBlobFromCacheOrGateway({ gateway, txId }) {
async function getBlobFromCacheOrGateway({ gateway, txId, expectedSha256Hex = '' }) {
const expected = String(expectedSha256Hex || '').trim().toLowerCase();
try {
const cached = await getRecord(txId);
if (cached?.blob instanceof Blob) {
return cached.blob;
if (!validateSha256Hex(expected)) return cached.blob;
const cacheSha = String(cached?.sha256Hex || '').trim().toLowerCase();
if (cacheSha && cacheSha === expected) {
return cached.blob;
}
const ok = await verifyBlobSha256(cached.blob, expected);
if (ok) {
return cached.blob;
}
// кэш повреждён или не совпадает с ожидаемым хэшем: удаляем и перекачиваем
await deleteRecords([txId]);
}
} catch {
// ignore IndexedDB errors and fallback to fetch
}
const blob = await fetchAvatarBlob({ gateway, txId });
if (validateSha256Hex(expected)) {
const ok = await verifyBlobSha256(blob, expected);
if (!ok) {
throw new Error('SHA256_MISMATCH');
}
}
const computedSha256Hex = await sha256HexFromArrayBuffer(await blob.arrayBuffer());
const record = {
txId,
blob,
contentType: String(blob.type || 'application/octet-stream'),
sizeBytes: Number(blob.size || 0),
sha256Hex: computedSha256Hex,
cachedAtMs: Date.now(),
};
try {
@@ -173,12 +205,16 @@ async function getBlobFromCacheOrGateway({ gateway, txId }) {
return blob;
}
export async function getCachedAvatarObjectUrl({ gateway, txId }) {
export async function getCachedAvatarObjectUrl({ gateway, txId, expectedSha256Hex = '' }) {
const cleanTxId = String(txId || '').trim();
if (!validateArweaveTxId(cleanTxId)) {
throw new Error('Некорректный Transaction ID Arweave');
}
const blob = await getBlobFromCacheOrGateway({ gateway, txId: cleanTxId });
const blob = await getBlobFromCacheOrGateway({
gateway,
txId: cleanTxId,
expectedSha256Hex,
});
return URL.createObjectURL(blob);
}
+46 -7
View File
@@ -4,6 +4,7 @@ const MAX_AVATAR_SOURCE_BYTES = 10 * 1024 * 1024;
const MAX_AVATAR_SIDE_PX = 768;
const AVATAR_QUALITY = 0.86;
const TX_ID_RE = /^[A-Za-z0-9_-]{43}$/;
const SHA256_HEX_RE = /^[A-Fa-f0-9]{64}$/;
let arweaveLibPromise = null;
@@ -92,29 +93,64 @@ function canvasToBlob(canvas, type, quality) {
});
}
function bytesToHex(bytes) {
return Array.from(bytes, (item) => item.toString(16).padStart(2, '0')).join('');
}
export function validateArweaveTxId(txId) {
const value = String(txId || '').trim();
return TX_ID_RE.test(value);
}
export function buildArweaveAvatarValue(txId) {
export function validateSha256Hex(sha256Hex) {
const value = String(sha256Hex || '').trim();
return SHA256_HEX_RE.test(value);
}
export async function sha256HexFromArrayBuffer(buffer) {
if (!(buffer instanceof ArrayBuffer)) throw new Error('Некорректные данные для SHA256');
const digest = await crypto.subtle.digest('SHA-256', buffer);
return bytesToHex(new Uint8Array(digest));
}
export function buildArweaveAvatarValue(txId, sha256Hex = '') {
const cleanTxId = String(txId || '').trim();
if (!validateArweaveTxId(cleanTxId)) {
throw new Error('Некорректный Transaction ID Arweave');
}
return `AR:${cleanTxId}`;
const cleanSha = String(sha256Hex || '').trim().toLowerCase();
if (!cleanSha) return `AR:${cleanTxId}`;
if (!validateSha256Hex(cleanSha)) {
throw new Error('Некорректный SHA256 хэш аватара');
}
return `SHA256:${cleanSha},AR:${cleanTxId}`;
}
export function parseArweaveAvatarValue(value) {
const raw = String(value || '').trim();
if (!raw.startsWith('AR:')) {
return { ok: false, network: '', txId: '' };
if (!raw) {
return { ok: false, network: '', txId: '', sha256Hex: '' };
}
const arMatch = raw.match(/(?:^|,)\s*AR:([A-Za-z0-9_-]{43})\s*(?:,|$)/);
let txId = String(arMatch?.[1] || '').trim();
if (!txId) {
// fallback для старых/кривых значений без запятых: "...AR:<txid>..."
const fallbackAr = raw.match(/AR:([A-Za-z0-9_-]{43})/);
txId = String(fallbackAr?.[1] || '').trim();
}
const txId = raw.slice(3).trim();
if (!validateArweaveTxId(txId)) {
return { ok: false, network: '', txId: '' };
return { ok: false, network: '', txId: '', sha256Hex: '' };
}
return { ok: true, network: 'AR', txId };
const shaMatch = raw.match(/(?:^|,)\s*SHA256:([A-Fa-f0-9]{64})\s*(?:,|$)/);
let sha256Hex = String(shaMatch?.[1] || '').trim().toLowerCase();
if (!sha256Hex) {
const fallbackSha = raw.match(/SHA256:([A-Fa-f0-9]{64})/);
sha256Hex = String(fallbackSha?.[1] || '').trim().toLowerCase();
}
return { ok: true, network: 'AR', txId, sha256Hex };
}
export function buildArweaveDataUrl({ gateway, txId }) {
@@ -209,6 +245,8 @@ export async function prepareAvatarImageFile(file) {
}
const optimizedFile = blobToFile(blob, fileName, contentType);
const optimizedArrayBuffer = await optimizedFile.arrayBuffer();
const sha256Hex = await sha256HexFromArrayBuffer(optimizedArrayBuffer);
return {
file: optimizedFile,
originalSizeBytes: Number(file.size || 0),
@@ -218,6 +256,7 @@ export async function prepareAvatarImageFile(file) {
width,
height,
contentType,
sha256Hex,
};
} catch (error) {
if (error instanceof Error) throw error;
+6 -1
View File
@@ -199,7 +199,12 @@ export async function loadUserProfileCard(login) {
gender: String(snapshot?.gender || 'unknown').trim().toLowerCase() || 'unknown',
official: Boolean(toggles.official),
shine: Boolean(toggles.shine),
avatar: snapshot?.avatar?.txId ? { ar: String(snapshot.avatar.txId).trim() } : null,
avatar: snapshot?.avatar?.txId
? {
ar: String(snapshot.avatar.txId).trim(),
sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase(),
}
: null,
};
}
+15 -4
View File
@@ -1,5 +1,10 @@
import { authService, state } from '../state.js';
import { buildArweaveAvatarValue, parseArweaveAvatarValue, validateArweaveTxId } from './arweave-file-service.js';
import {
buildArweaveAvatarValue,
parseArweaveAvatarValue,
validateArweaveTxId,
validateSha256Hex,
} from './arweave-file-service.js';
export const profileFieldDefs = [
{ key: 'first_name', readKeys: ['first_name'], label: 'Имя', placeholder: 'Введите имя' },
@@ -123,15 +128,17 @@ export async function loadProfileSnapshot(login) {
const parsedAvatar = parseArweaveAvatarValue(latestAvatar?.value || '');
const avatar = parsedAvatar.ok
? {
value: buildArweaveAvatarValue(parsedAvatar.txId),
value: buildArweaveAvatarValue(parsedAvatar.txId, parsedAvatar.sha256Hex),
source: 'arweave',
txId: parsedAvatar.txId,
sha256Hex: parsedAvatar.sha256Hex || '',
timeMs: latestAvatar?.timeMs || 0,
}
: {
value: '',
source: '',
txId: '',
sha256Hex: '',
timeMs: latestAvatar?.timeMs || 0,
};
@@ -175,10 +182,14 @@ export async function saveProfileGender(login, gender) {
});
}
export async function saveProfileAvatarArweave(login, txId) {
export async function saveProfileAvatarArweave(login, txId, sha256Hex) {
const cleanTxId = String(txId || '').trim();
const cleanSha = String(sha256Hex || '').trim().toLowerCase();
if (!validateArweaveTxId(cleanTxId)) {
throw new Error('Некорректный Transaction ID Arweave');
}
await saveProfileParamBlock(login, 'ava', buildArweaveAvatarValue(cleanTxId));
if (!validateSha256Hex(cleanSha)) {
throw new Error('Некорректный SHA256 хэш аватара');
}
await saveProfileParamBlock(login, 'ava', buildArweaveAvatarValue(cleanTxId, cleanSha));
}