Этап 5: история — экспорт и импорт .bare, устройства, место, пагинация
Экспорт: ключ архива из секрета аккаунта через HKDF, заголовок ровно 65 байт (magic, версия, соль, 32 сырых байта отпечатка владельца, iv) и он же целиком AAD шифротекста. Ника владельца в файле нет. Импорт сверяет отпечаток до расшифровки, сливает идемпотентно по id, а записи peers берёт только для ников, которых в локальном TOFU ещё нет: архивом доверие к ключу не перебить. Настройки: устройства с датой и пометкой «это устройство», «занято N МБ», кнопка «экспортировать» в подтверждении выхода и в подтверждении входа под другим ником — долг этапа 1 и обещание ADR-029 закрыты. Лента: страницы по 50 с подгрузкой вверх без прыжка прокрутки; новая страница вставляется, а не пересобирает ленту. ADR-050: импорт не перезаписывает лежащую запись — у своей есть состояние отправки, которого в архиве нет. ADR-054: архив — недоверенный ввод. Ревью собрало архивы с ts вне диапазона Date, мусорным lastId, ником с bidi-переопределением и roomId с обходом пути: каждый из них навсегда ломал ленту или счётчик. Теперь форма ника, roomId, id, ts и автора проверяется, а lastId из файла не читается вовсе. ADR-051, 052, 053: тексты и кнопки, устройства и место, страницы ленты без виртуализации. Приёмка: формат сверен побайтно на модулях, скачанных с боевого сервера, — смещения заголовка, отпечаток сырыми байтами, новые соль и iv на каждый экспорт, подмена любого байта заголовка ломает расшифровку, чужой секрет не открывает, семь видов битых файлов отвергнуты. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
This commit is contained in:
+21
-5
@@ -76,20 +76,36 @@ export function button(text, className = "button") {
|
||||
return node;
|
||||
}
|
||||
|
||||
// confirmPanel — вопрос и две кнопки; спрятан, пока не спросили.
|
||||
// Вопрос отдаётся наружу: его текст бывает известен только к моменту показа.
|
||||
export function confirmPanel(question, yesLabel) {
|
||||
// confirmPanel — вопрос и кнопки; спрятан, пока не спросили. Вопрос
|
||||
// отдаётся наружу: его текст бывает известен только к моменту показа.
|
||||
//
|
||||
// extraLabel — необязательная третья кнопка перед «да». Там, где
|
||||
// подтверждение уносит историю, это «экспортировать»: единственная копия
|
||||
// не должна исчезать без предложения сохранить её (docs/ui.md, ADR-029).
|
||||
// Отдаётся отдельным полем; без неё оно null.
|
||||
export function confirmPanel(question, yesLabel, extraLabel = null) {
|
||||
const root = el("div");
|
||||
root.hidden = true;
|
||||
const text = el("p", "confirm", question);
|
||||
const yes = button(yesLabel);
|
||||
const no = button("отмена");
|
||||
const row = el("div", "row");
|
||||
const extra = extraLabel === null ? null : button(extraLabel);
|
||||
// Три кнопки в один ряд помещаются не всегда: «экспортировать» шире
|
||||
// трети колонки, и ряд переносится (ADR-051).
|
||||
const row = el("div", extra === null ? "row" : "row row--wrap");
|
||||
if (extra !== null) {
|
||||
row.append(extra);
|
||||
}
|
||||
row.append(yes, no);
|
||||
root.append(text, row);
|
||||
return { root, text, yes, no };
|
||||
return { root, text, yes, no, extra };
|
||||
}
|
||||
|
||||
// EXPORT_FAILED — экспорт не собрался: истории не прочитать или ключей
|
||||
// аккаунта на устройстве нет (docs/ui.md, «Тексты состояний»). Текст один
|
||||
// на все три места, где стоит кнопка «экспортировать».
|
||||
export const EXPORT_FAILED = "экспорт не удался";
|
||||
|
||||
// message — строка состояния под формой: ошибка цветом mark, ответ — mute.
|
||||
export function message() {
|
||||
const node = el("p", "message");
|
||||
|
||||
Reference in New Issue
Block a user