Files
bare/web/js/ui/contact.js
T
mayatnikovandClaude Opus 5 63a7a1ef52 Этап 2: чат 1:1 — устройства, очередь, SSE, шифрование сообщений
Сервер: регистрация устройств и X-Device, hub с одним потоком на устройство,
очередь per-device с фан-аутом без эха отправителю, POST /api/messages
с проверками в порядке protocol.md, ACK, SSE с воспроизведением очереди,
ready и пингом раз в 20 секунд, контакты в обе стороны при первом сообщении,
лимит 30 сообщений в минуту.

Клиент: ULID, ключ 1:1 из ECDH через HKDF, шифрование конверта с AAD,
sync.js как единственный писатель в IndexedDB, ACK строго после записи,
список чатов, экран чата по эталону, разделители дат и «новые»,
pending и failed с повтором, полоса «нет соединения».

ADR-033: у неотправленного есть текст отказа — clock_skew стало видно.
ADR-034: входящее с известным id не перезаписывает запись. Собеседник знает
открытый id конверта и подменял им чужое сообщение в чужой истории — вплоть
до стирания своего присланного, чего «удалить у всех не существует» не допускает.
ADR-035: один поток событий на браузерный профиль (locks + BroadcastChannel):
две вкладки отбирали поток друг у друга и оставались без живой доставки.
ADR-036: повтор отправки сохраняет ULID, пока он в пределах окна часов, —
иначе потерянный ответ давал у собеседника два сообщения вместо одного.

Приёмка на боевом сервере: два аккаунта, пять устройств, живая доставка,
копия на второе устройство, очередь офлайн-устройству, ACK, подмена from
игнорируется, чужой deviceId и запрос без Origin отбиваются, плейнтекста
в базе и WAL ноль вхождений.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
2026-08-22 18:18:04 +03:00

62 lines
2.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Карточка контакта — docs/ui.md, «Карточка контакта».
//
// Смена ключа собеседника и «доверять новому ключу» появятся вместе
// с TOFU (этап 3, ADR-016): до тех пор у записи peers нет pending.
import * as sync from "../sync.js";
import { fingerprintGroups } from "../crypto.js";
import { button, el, message, setError, setNote } from "./dom.js";
export function renderContact(root, ctx, nick) {
root.append(head(ctx, nick));
const body = el("div", "body settings");
root.append(body);
const card = el("section", "block block--first");
body.append(card, remove(ctx, nick));
// Отпечаток лежит в записи TOFU; её может ещё не быть, если чат
// открыли до первого ключа.
sync.peer(nick).then((record) => {
if (!record?.fingerprint || !card.isConnected) {
return;
}
const groups = fingerprintGroups(record.fingerprint);
card.append(
el("p", "fp", groups.slice(0, 8).join(" ")),
el("p", "fp", groups.slice(8).join(" ")),
el("p", "fp-hint", "сверьте с собеседником голосом или лично"),
);
}).catch(() => {});
}
function head(ctx, nick) {
const bar = el("div", "head");
const back = el("button", "back", "назад");
back.type = "button";
back.addEventListener("click", () => ctx.go(`#/dm/${nick}`));
bar.append(back, el("span", "title", `@${nick}`));
return bar;
}
// remove — «убрать из списка»: строка контакта уходит с сервера, история
// на устройстве остаётся (ADR-019).
function remove(ctx, nick) {
const box = el("section", "block");
const note = message();
const drop = button("убрать из списка");
drop.addEventListener("click", async () => {
drop.disabled = true;
setNote(note, "");
try {
await sync.forgetChat(sync.dmChatId(nick));
ctx.go("#/");
} catch (err) {
setError(note, ctx.errorText(err));
drop.disabled = false;
}
});
box.append(drop, note);
return box;
}