Сервер: регистрация устройств и 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
38 lines
1.4 KiB
Go
38 lines
1.4 KiB
Go
package api
|
|
|
|
import "strings"
|
|
|
|
// ULID — 48 бит миллисекунд и 80 бит случайности, Crockford base32,
|
|
// 26 символов (docs/crypto.md, «Идентификаторы»). Сервер читает из него
|
|
// только время: расхождение с серверными часами больше пяти минут —
|
|
// clock_skew (ADR-017).
|
|
const ulidLen = 26
|
|
|
|
// crockford — алфавит Crockford base32: без I, L, O и U. Канонический
|
|
// ULID записывается заглавными; строчные буквы сервер не принимает —
|
|
// идентификатор входит в AAD шифротекста побайтно (docs/crypto.md).
|
|
const crockford = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
|
|
// ulidTime разбирает ULID и отдаёт его метку времени в миллисекундах.
|
|
func ulidTime(id string) (int64, bool) {
|
|
if len(id) != ulidLen {
|
|
return 0, false
|
|
}
|
|
var ms int64
|
|
for i := 0; i < ulidLen; i++ {
|
|
v := strings.IndexByte(crockford, id[i])
|
|
if v < 0 {
|
|
return 0, false
|
|
}
|
|
if i < 10 {
|
|
ms = ms<<5 | int64(v)
|
|
}
|
|
}
|
|
// Первые десять символов — 50 бит, времени отведено 48: старшие два
|
|
// обязаны быть нулевыми.
|
|
if ms > 1<<48-1 {
|
|
return 0, false
|
|
}
|
|
return ms, true
|
|
}
|