Meet Service
Файл:
backend/src/services/meet.rs(новый)
Сервис знакомства между разными identity (meet). Семантическое зеркало Pairing Service — но если pairing объединяет устройства одной identity, то meet связывает identity разных пользователей. После успешного meet локальная БД получает запись о чужой identity (public-key, display name, метаданные), и только тогда становится корректным вызов share_space_svc(space_id, target_identity_id).
«Handshake» намеренно не используется — этот термин закреплён за Noise Protocol handshake на транспортном уровне libp2p (см. sovereignty-stack).
Эндпоинты
| Метод | Путь | Функция | Описание |
|---|---|---|---|
| POST | /api/meet/start | start_meet_svc | Объявить устройство видимым для meet (mDNS + Swarm protocol) |
| POST | /api/meet/stop | stop_meet_svc | Снять объявление |
| POST | /api/meet/scan | scan_meet_peers_svc | mDNS-сканировать LAN на устройства в режиме meet |
| POST | /api/meet/request | submit_meet_request_svc | Отправить запрос знакомства discovered peer |
| POST | /api/meet/approve | approve_meet_svc | Одобрить входящий meet-запрос (по token) |
| POST | /api/meet/reject | reject_meet_svc | Отклонить входящий meet-запрос |
| POST | /api/meet/invite | create_meet_invite_svc | Создать invite-payload (QR / PIN / URL) для out-of-band обмена |
| POST | /api/meet/invite/accept | accept_meet_invite_svc | Принять чужой invite-payload |
| POST | /api/meet/introduce | introduce_meet_svc | Mediator: представить две своих acquaintances друг другу |
| GET | /api/meet/pending | get_pending_meet_svc | Список pending входящих / исходящих meet-запросов |
| GET | /api/meet/acquaintances | get_acquaintances_svc | Список known identities (результат успешных meet) |
| POST | /api/meet/verify | verify_safety_number_svc | Отметить out-of-band verified (SAS совпал) |
| POST | /api/meet/revoke | revoke_meet_svc | Локально забыть identity (revoke). Шаринги не трогает по умолчанию |
Зависимости
MeetManager(новый,backend/src/meet/manager.rs) — оркестрация meet поверх существующего libp2p Swarm; mDNS service type_kontinuum-meet._tcp(отличается от device-pairing_kontinuum-pair._tcp).SharingManager— переиспользует существующий Swarm и Noise/Yamux транспорт; meet добавляет свой request-response протокол/kontinuum/meet/1.0.0.IdentityManager— own identity_id + signing_key для подписания meet-payload'ов.AcquaintanceStore(новый,backend/src/meet/store.rs) — таблицаacquaintancesв SQLite, см. §БД ниже.- kontinuum-node — опционально:
mailbox— async-доставка meet-payload, когда получатель offline.rendezvous— discovery online-устройств чужой identity (после того, как она стала known).
EventBus— UI-уведомления (MeetPending,MeetCompleted,MeetRejected,SafetyNumberMismatch).
Хранение результата: таблица acquaintances
Файл миграции — задача не этого промпта; формат полей:
| Поле | Тип | Описание |
|---|---|---|
identity_id | BLOB | PK — blake3(pubkey) чужой identity |
pubkey | BLOB | Ed25519 public key (32 B) |
display_name | TEXT? | Имя, как назвал себя owner identity (необязательно, не доверяемо) |
met_at | INTEGER | unix-эпоха момента meet |
met_via | TEXT | lan-mdns | invite-link | invite-qr | invite-pin | mediator | node-mailbox |
mediator_identity_id | BLOB? | Если met_via = 'mediator' — кто представил |
meet_proof | BLOB | Подписанный MeetCompletedPayload (для audit + повторной верификации) |
safety_number_verified | INTEGER | 0/1 — out-of-band SAS подтверждён |
safety_number_verified_at | INTEGER? | |
status | TEXT | active | pending | revoked |
notes | TEXT? | Пользовательская заметка |
Привязка к identity, не к device. Запись принадлежит owning identity, не конкретному устройству. Синхронизация между devices одного owner — через существующий device-pairing channel (см. §multi-device ниже).
Существующая таблица
peers(вSharingManager) — это runtime-discovery (mDNS + libp2p Swarm), identity-scope acquaintances хранятся отдельно. Связь:peers.identity_id(если уже известен) JOINacquaintances.identity_id.
Wire format: meet payloads
Все payload'ы — CBOR (parity с kontinuum-node wire-types). Подписи — Ed25519, ключ — identity signing key.
MeetInvite (создаётся Alice'ой)
MeetInvite {
0: schema = "kontinuum/meet/v1/invite",
1: alice_identity_id, ;; bytes(32) = blake3(alice_pubkey)
2: alice_pubkey, ;; bytes(32) Ed25519
3: alice_display_name?, ;; text — необязательно (Алиса даёт своё имя)
4: issued_at, ;; uint
5: expires_at, ;; uint (issued_at + TTL, default 24h)
6: nonce, ;; bytes(16) — uniqueness/replay-protect
7: usage, ;; "single" | "multi"
8: relay_hints?, ;; [multiaddr] — где Alice'у искать
9: signature ;; ed25519(alice_priv, body[0..8])
}Encoded form:
- QR — base32(cbor(MeetInvite)). Для in-person sharing экрана.
- URL —
kontinuum://meet?inv=<base64url(cbor(MeetInvite))>. Для link-sharing (email/мессенджер/clipboard). - PIN — 6-значный код, derived из BLAKE3(nonce). Не self-contained payload — служит проверкой in-person, что обе стороны видят один и тот же invite (anti-MITM на mDNS-канале).
MeetAccept (отправляется Bob'ом в ответ)
MeetAccept {
0: schema = "kontinuum/meet/v1/accept",
1: invite_nonce, ;; bytes(16) — из MeetInvite, корреляция
2: bob_identity_id, ;; bytes(32)
3: bob_pubkey, ;; bytes(32)
4: bob_display_name?, ;; text
5: accepted_at, ;; uint
6: invite_hash, ;; bytes(32) = blake3(cbor(MeetInvite))
7: signature ;; ed25519(bob_priv, body[0..6])
}MeetCompleted (хранится у обеих сторон как meet_proof)
MeetCompleted {
0: schema = "kontinuum/meet/v1/completed",
1: alice_identity_id,
2: bob_identity_id,
3: alice_pubkey,
4: bob_pubkey,
5: completed_at,
6: met_via, ;; text (см. таблицу выше)
7: mediator_identity_id?, ;; bytes(32)
8: alice_sig, ;; ed25519(alice_priv, body[0..7])
9: bob_sig ;; ed25519(bob_priv, body[0..7])
}MeetCompleted симметричен: каждая сторона держит one and the same blob с двумя подписями. Это audit anchor — при дальнейшем shared Space можно proof'ом доказать, что обмен identity состоялся.
Introduction (mediator flow)
Introduction {
0: schema = "kontinuum/meet/v1/introduction",
1: mediator_identity_id,
2: introducee_a_identity_id,
3: introducee_a_pubkey,
4: introducee_b_identity_id,
5: introducee_b_pubkey,
6: issued_at,
7: expires_at,
8: signature ;; ed25519(mediator_priv, body[0..7])
}Mediator отправляет копию обоим introducee. Каждый верифицирует подпись mediator'а (mediator уже в их acquaintances) и решает — принять / отклонить. Если оба принимают, обмениваются MeetAccept → MeetCompleted через тот же канал, что и invite-flow, но met_via = "mediator" и mediator_identity_id заполнен.
Транспорт
Meet поддерживает три транспорта, выбираемых автоматически по ситуации (best-effort fallback):
| Транспорт | Когда работает | Обязательность | Канал |
|---|---|---|---|
| libp2p Swarm + mDNS | Оба online, один LAN | Обязателен | Тот же Swarm, что и sharing; mDNS type _kontinuum-meet._tcp |
| libp2p Swarm + relay | Оба online, разные сети | Обязателен | Через kontinuum-node relay; lookup target через rendezvous |
| kontinuum-node mailbox | Получатель offline | Опционален | Async; meet-payload как SharingMessage::MeetInvite/Accept |
| Out-of-band (QR/link) | Нет общего сетевого канала | Опционален | Invite-payload передаётся вне сети; pickup один из трёх выше |
Out-of-band — это способ доставки MeetInvite, не отдельный on-line канал. После того как Bob открыл QR / link / ввёл PIN, его accept_meet_invite_svc всё равно отправляет MeetAccept через один из online-каналов (Swarm direct, Swarm-через-relay, или node-mailbox если Alice offline).
Meet не использует DHT для долгоживущей публикации identity. DHT в Kontinuum-Node — это discovery space-records; идентити-pubkey публикуется только во время активной meet-сессии и потом — только в локальных
acquaintancesобеих сторон.
Криптографический протокол
Подписи и ключи
- Подпись MeetInvite / MeetAccept / Introduction / MeetCompleted — Ed25519 identity signing key. Совпадает с ключом, который уже используется в
IdentityManagerдля подписи space-операций. - ECDH-handshake не нужен на этапе meet — обмен идёт публичными ключами и подписанными payload'ами; конфиденциальность канала обеспечивается нижележащим Noise XX. ECDH-обмен делается позже, в момент
share_space_svc(там Curve25519 ephemeral keypair поверх identity pubkey'ев — текущая логика sharing). identity_pubkey(Ed25519) →identity_x25519_pubkey(Curve25519) derive — стандартный clamping (existing code в IdentityManager). Meet не передаёт Curve25519 ключи отдельно; они выводятся из Ed25519 pubkey на момент шаринга.
Защита от MITM на out-of-band канале
| Сценарий | Защита |
|---|---|
| QR в одной комнате | Алиса показывает экран — атакующий physically не подменяет. |
| Link через мессенджер | InviteURL self-signed Алисой. Атакующий может подменить ссылку на свою, но Bob увидит другое имя/SAS. |
| PIN-flow в LAN | PIN = blake3(nonce) усечён до 6 цифр. Оба видят один номер — anti-MITM на mDNS. |
| Mediator | Подпись Charlie над Introduction; верифицируется Bob'ом, у которого Charlie уже acquaintance. |
SAS (Safety Number) — детерминированно derive'ится из обеих pubkey'ев после meet:
sas = blake3("kontinuum/sas/v1" || sort([alice_pubkey, bob_pubkey])).truncate_to(60_decimal_digits)Показывается в UI обеим сторонам; они сравнивают вслух / через другой канал. При совпадении — verify_safety_number_svc помечает safety_number_verified = 1. Verification опциональна (см. trade-offs).
Сценарии (поддерживаемые)
Сценарий 1 — Оба online, один LAN (mDNS + явный meet)
Сценарий 2 — Оба online, разные сети (relay)
Сценарий 3 — Получатель offline (mailbox)
Сценарий 4 — Mediator-initiated introduction
UX-flow
| Шаг | Кто видит | Действие |
|---|---|---|
| 1. Создать invite | Alice | «Добавить контакт» → выбрать способ (QR/link/PIN) |
| 2. Передать invite | Alice → Bob | OOB-канал |
| 3. Открыть invite | Bob | App автоматически распарсит URL / scan QR |
| 4. Подтвердить acquaint | Bob | UI prompt: «Alice (display_name) хочет познакомиться. Принять?» |
| 5. Получить notification | Alice | UI prompt: «Bob (display_name) принял знакомство. Принять?» |
| 6. Подтвердить | Alice | Accept → запись в acquaintances обоих |
| 7. (Опц.) Сверить SAS | Alice ↔ Bob | Сравнить 6-блочный номер вслух / другим каналом |
| 8. (Опц.) Mark as verified | оба | Tap «verified», апдейт safety_number_verified = 1 |
По умолчанию — двусторонняя confirmation (parity с device pairing), SAS verification опциональна. Можно включить «требовать SAS до первого шаринга» в settings (см. trade-offs §6).
Failure modes
| Сценарий | Поведение |
|---|---|
| QR подменили в transit | Bob увидит чужие display_name и SAS. Если SAS check включён — увидит mismatch. По умолчанию запись помечается safety_number_verified=0. |
Invite просрочен (expires_at <= now) | accept_meet_invite_svc возвращает MeetInviteExpired. Bob запрашивает новый у Alice. |
Invite уже использован (usage="single") | Alice держит set использованных nonce. Повторное использование → MeetInviteAlreadyUsed. |
| Bob уже добавил Alice c другого устройства | Multi-device sync: запись acquaintances отзеркаливается всем (identity_id == bob's own) устройствам через device-pairing channel (см. ниже). |
| Alice потеряла ключи и recovered | Kontinuum recovery → новая identity (новый pubkey). Прежние acquaintances НЕ узнают автоматически. Требуется ручной re-meet (см. ниже). |
| Bob revoke'нул Alice | Alice'а удаляется из acquaintances Bob'а. Существующие шаринги не разрываются — отдельный flow (unshare_all_from(identity_id)). |
| MeetCompleted signature mismatch | Запись не вставляется, EventBus::MeetSignatureMismatch, UI показывает security warning. |
| Сетевой сбой между accept и completed | Timeout 30 сек на каждое сообщение; pending запись остаётся в acquaintances.status = 'pending', можно retry. |
Re-meet после recovery
Когда Alice восстанавливается из 12 слов — её signing key прежний (BIP39 derive детерминирован). Прежняя identity сохраняется. Re-meet не требуется. Это отличается от scenario, когда identity создана с нуля без recovery (нет 12-слов, или backup потерян) — тогда новая identity, new pubkey, и Alice должна re-meet всех вручную через обычный meet-flow.
Convenience-эндпоинт
request_remeet_svc(post-recovery re-meet поверх mediator-flow) в v0.1 не реализован и отложен до решения по recovery без 12-слов — см. GitLab-issue #50.
Revoke
revoke_meet_svc(identity_id):
- Помечает
acquaintances.status = 'revoked'(soft) или удаляет запись (hard) — UI choice. - Не уведомляет другую сторону (privacy parity с messengers; чужая сторона видит revoke только по факту, когда первый
share_space_svcпосле revoke завершится сIdentityRevokedLocally). - Existing shared spaces — остаются shared unless user opt-in:
revoke_meet_with_unshare_svc(identity_id)— комбо: revoke +unshare_all_from(identity_id)для всех собственных Space'ов where target = revoked identity.
Совместимость с существующим кодом
Расширяется
SharingService(libp2p) — добавляется request-response протокол/kontinuum/meet/1.0.0поверх существующего Noise/Yamux. Без второго Swarm'a — share одну инфраструктуру.IdentityManager— новый методidentity_proof_for_meet() -> MeetSigner(capability-token, чтобы meet-операции могли подписывать без прямого доступа к raw signing key).- БД — новая таблица
acquaintances. Не пересекается сpeers(та — runtime, эта — identity-scope).
Новый код
backend/src/services/meet.rs— endpoints (этот документ).backend/src/meet/manager.rs—MeetManager.backend/src/meet/store.rs— CRUD надacquaintances.backend/src/meet/wire.rs— CBOR encode/decode + signing canonical bodies для MeetInvite / MeetAccept / MeetCompleted / Introduction.backend/src/meet/sas.rs— derive + format safety number.
НЕ трогается
PairingService/PairingManager— device-pairing остаётся как есть. Просто другой mDNS service type, другой libp2p protocol.SharingService::share_spaceflow — pre-condition остаётся «target_identity_id известен», но теперь meet это формально обеспечивает.recovery.rs— recovery flow не меняется.
Multi-device meet (sync acquaintances между устройствами одного user)
После того как acquaintances стала identity-scope таблицей, Bob'овский phone и laptop обязаны разделять её. Решение — reuse device-pairing channel:
- После device-pairing между двумя устройствами Bob'а, Bob's laptop получает initial snapshot
acquaintancesот phone. - Дальнейшие изменения (
INSERTпосле нового meet,UPDATEпри verify,revoke) реплицируются через тот же sync-bus, что и spaces (опираемся на existing sync infrastructure). - Это не делает acquaintances частью DHT/kontinuum-node — sync остаётся приватным между устройствами одной identity, никаких ноды-side records.
Альтернатива — публиковать acquaintances в node-mailbox identity'и Bob'а как self-message. Дороже трафиком, но избавляет от необходимости phone + laptop быть одновременно online.
Открытые продуктовые решения по meet (симметрия, online-обязательность, OOB-verification, multi-device sync, revoke side-effects, invite TTL, roll-out endpoint'ов) вынесены в GitLab-issue #50.
Implementation log (phases A–C)
Раздел отражает текущее реализованное состояние. Эндпоинты выше из первоначального дизайна частично сокращены — итоговый surface ниже.
Шипящие эндпоинты (backend/src/services/meet.rs)
| Метод | Путь | Назначение |
|---|---|---|
| POST | /api/meet/invite | Создать MeetInvite, вернуть URL + base64-blob |
| POST | /api/meet/invite/parse | Превью invite без принятия |
| POST | /api/meet/invite/accept | Принять invite, записать issuer как Pending |
| POST | /api/meet/accept/process | Inviter обрабатывает Accept, выдаёт Completed |
| POST | /api/meet/completed/finalize | Acceptor завершает handshake (Pending → Active) |
| GET | /api/meet/acquaintances | Список acquaintance rows |
| POST | /api/meet/acquaintances/revoke | Soft revoke (status='revoked') |
| POST | /api/meet/acquaintances/delete | Hard delete row |
| POST | /api/meet/safety-number | Compute 60-digit SAS для пары identity |
| POST | /api/meet/safety-number/verify | Mark SAS as verified out-of-band |
| POST | /api/meet/swarm/start / …/stop | Запуск/остановка meet swarm (libp2p) |
| POST | /api/meet/swarm/send-invite | Push direction — отправить invite peer'у напрямую |
| POST | /api/meet/swarm/accept-via-libp2p | Pull direction — Bob лично dial'ит Alice по hint |
| POST | /api/meet/swarm/send-completed | Завершить handshake поверх libp2p |
| GET | /api/meet/swarm/discovered-peers | mDNS + dialed peers |
| GET | /api/meet/swarm/listen-addrs | Bound multiaddrs локального swarm'а |
| POST | /api/meet/mailbox/deposit-invite | Положить invite в mailbox получателя |
| POST | /api/meet/mailbox/poll | Ручной poll цикл |
| POST | /api/meet/mailbox/polling/start / …/stop | Background poller lifecycle |
| POST | /api/meet/mailbox/host | Сконфигурировать host для локальной identity |
| GET | /api/meet/mailbox/host | Прочитать host config |
| POST | /api/meet/mailbox/host/clear | Сбросить host |
| POST | /api/meet/mailbox/host/recipient | OOB-маршрут к чужой identity (без DHT) |
| POST | /api/meet/mailbox/host/publish | Опубликовать host record в global DHT |
| POST | /api/meet/mailbox/host/lookup | DHT lookup mailbox host чужой identity |
| POST | /api/meet/dht/bootstrap-node | Зарегистрировать bootstrap peer |
| GET | /api/meet/dht/bootstrap-nodes | Список bootstrap nodes |
| POST | /api/meet/dht/bootstrap-node/remove | Удалить bootstrap peer |
| POST | /api/meet/dht/bootstrap | Triggered DHT bootstrap |
Эндпоинты из исходного дизайна, не реализованные в v0.1: start_meet_svc/stop_meet_svc, scan_meet_peers_svc, submit_meet_request_svc, approve_meet_svc/reject_meet_svc, get_pending_meet_svc, introduce_meet_svc. LAN-сценарий вместо этих ручек обслуживается meet swarm + mDNS discovery (/api/meet/swarm/*), out-of-band invite flow заменил approve/reject UI.
Транспорт (phases C-1…C-18)
- libp2p meet swarm (
backend/src/meet/swarm.rs) — request-response на протоколе/kontinuum/meet/1.0.0поверх существующего libp2p stack. Push (Alice → Bob черезsend-invite) и pull (Bob дозванивается до Alice через invite hint) обе направления покрыты. - Mailbox fallback (C-6 → C-18) — когда peer offline, payload идёт через node-hosted mailbox. Стек:
MailboxClienttrait (backend/src/meet/mailbox.rs) — общая абстракция (deposit/read/ack) с двумя реализациями:InMemoryMailboxClient(для тестов) иSqliteMailboxClient(mailbox_sqlite.rs) для in-process persistence.LibP2pMailboxClient(mailbox_libp2p.rs) — адаптер отMailboxEnvelopeк wire-типамMailboxDepositReq/MailboxReadReqkontinuum-node'а. ИспользуетSwarmMailboxTransportдля маршрутизации запросов через swarm к нужному mailbox host'у.MailboxHostStore(mailbox_host.rs) — sqlite-таблица(identity_id, peer_id, multiaddr), заполняется UI или DHT lookup.DhtMailboxHostResolver— global DHT под ключомmailbox:{identity_id}, валидирует подписьMailboxHostRecordпод embedded identity pubkey.BootstrapNodeStore(bootstrap.rs) — persistent список known bootstrap peers для DHT join.
- Background poller (
/api/meet/mailbox/polling/start) — tokio task, таймер наinterval_secs(clamp ≥ 5s), эмититmeet_mailbox_polledчерез EventBus после каждой ненулевой обработки.
E2E-шифрование payload'ов (slice 16c)
MailboxEnvelope::for_request_sealed оборачивает payload в SealedEnvelope — hybrid X25519 fan-out + AES-256-GCM на recipient'ов DeviceBoxKey (генерируется внутри Stronghold). Receiver-сторона: MailboxEnvelope::decrypt_payload(stronghold).await → unseal_envelope.
- HKDF info:
b"kontinuum meet mailbox key-wrap". - AAD bound в оба AEAD слоя:
"kontinuum meet mailbox; <recipient_id> <sender_id> <payload_type>"— любое drift между sender'ом и receiver'ом fail'ится чисто на AEAD-верификации. - Envelope несёт:
is_encrypted: bool,sender_identity_pubkey: Option<[u8; 32]>(для attribution receiver'а),payload_cborсодержит CBOR(SealedEnvelope) когда encrypted. - Counterpart's
device_box_pubkeyберётся из inboundMeetInvite.issuer_device_box_pubkey/MeetAccept.accepter_device_box_pubkey(оба зашиты в signed body) —derive_outbound_envelopeподхватывает автоматически. - Если counterpart device-box pubkey =
[0u8; 32](legacy / pre-v0.8 peer) — fallback на plaintext envelope с warning'ом. - Удалён в slice 16d:
meet/encryption.rsс ECDH-on-signing-key — путать sign-key и box-key больше нельзя.
Multi-device cursors (C-20)
Cursor table keyed на (identity_id, device_id) вместо просто identity_id. Mirror server-side device_cursors схемы. Без этого второе устройство под одной identity молча проглатывало envelopes, уже ack'нутые первым.
MailboxCursorStore::{load,save,clear}принимают(identity_id, device_id).MailboxStateInner.device_cursors: HashMap<(identity, device), u64>кэширует cursor in-memory.run_mailbox_pollрезолвит device_id черезDeviceManager::get_current_device().- Legacy single-key schema (pre-C-20) детектится через
PRAGMA table_infoи дропается на boot — следующий poll переиграет один envelope batch (handle_inbound идемпотентен).
Device-key signing for cursor commits (C-21, обновлено в slice 17)
LibP2pMailboxClient использует два независимых Ed25519 ключа:
identity_signing_key(legacy, из vault) подписываетdeposit.sender_signature(issuer attribution).DeviceSignKey(Stronghold, slice 6+) подписываетCommitCursor.device_signature(per-device cursor row attribution) черезStrongholdManager::sign_ed25519(DeviceSignKey, body).await.
Разделение позволяет ротировать одно без касания другого. После slice 17 device-private-key больше не существует — на месте DeviceManager::get_device_signing_key() теперь Stronghold-async-signer. Server в v0.1 валидирует только длину device_signature (64 байт), но наш sign-body (commit_sign_body) уже канонический — Ed25519 verify против device_sign_pubkey (из devices.device_sign_pubkey колонки) проходит.
CRL gates (C-22 + C-25)
Локальный AcquaintanceStatus::Revoked теперь реально блокирует входящий трафик revoked counterpart'а.
- Poll gate (C-22):
run_mailbox_pollсмотритis_revoked(sender_id)перед decode/decrypt. На hit envelope ack'ается, cursor продвигается,dropped_revokedcounter инкрементируется, handle_inbound пропускается. - Service gate (C-25):
process_meet_accept_svcиfinalize_meet_completed_svcтоже консультируются сis_revoked— без этого OOB-paste путь обходил poll gate. Если counterpart revoked, service возвращает ошибку, оператор должен сначалаdelete_acquaintanceчтобы re-establish.
Edge-case hardening (C-25)
verify_invite/accept/completed теперь проверяют identity_id == blake3(pubkey) для каждой identity_id в фрейме (MeetError::IdentityPubkeyMismatch). Без этого attacker мог подсунуть чужой identity_id со своим keypair — подпись валидна, recipient создаёт acquaintance row binding identity жертвы → pubkey атакующего. Cheap проверка (один blake3 + constant-time compare).
Observability (C-24)
Все метрики Prometheus префикс kontinuum_meet_*:
| Имя | Тип | Labels |
|---|---|---|
kontinuum_meet_operations_total | counter | operation ∈ {create_invite, accept_invite, process_accept, finalize_completed, revoke, delete, mark_verified} × status ∈ |
kontinuum_meet_mailbox_envelopes_total | counter | action ∈ |
kontinuum_meet_mailbox_deposits_total | counter | status ∈ |
kontinuum_meet_acquaintances_total | gauge | status ∈ |
kontinuum_meet_mailbox_poll_duration_ms | histogram | — |
Operation counter подключён через MeetOpGuard (Drop-guard, default outcome failure, happy-path вызывает g.ok() перед Ok). ? propagation автоматически считается как failure без manual вызова на каждом error-site.
Acquaintances gauge обновляется на каждом list_acquaintances — piggyback на rows которые уже в руках.
Frontend entity (C-23)
kontinuum-app/frontend/src/entities/acquaintance/ — FSD entity layer:
types.ts— narrowed unionsAcquaintanceStatus/MetViaс forward-compat brand(string & {}).api.ts— обёртки надmeetCommandsсunwrapResult. Покрывает весь handshake + acquaintance CRUD.useAcquaintancesStore.ts— Pinia store с optimistic local mutations, подпиской наmeet_mailbox_polledevent для авто-reload.lib.ts— formatters:mapBackendAcquaintance,displayLabel,shortIdentity,metViaLabel,statusGroup.
UI components (features / widgets / pages) на момент C-26 не написаны — только entity-слой как foundation для будущих интеграций.
Workspace unification (C-26)
meet/wire.rs re-export'ит encode / decode из kontinuum_core::node::wire вместо своей копии — оба crate'а гонят mailbox payload через один CBOR config. Wire types и sign-body функции пока живут в app (crate::meet::wire); большая экстракция в kontinuum-core::meet отложена.
Phase index
| Phase | Тема |
|---|---|
| C-1 | libp2p codec + inbound handler |
| C-2 | libp2p swarm + outbound handle |
| C-3 | lifecycle integration + push services |
| C-4 | pull direction over libp2p |
| C-5 | peer discovery service |
| C-6 | mailbox fallback scaffolding |
| C-7 | bidirectional async mailbox |
| C-8 | mailbox cursor persistence |
| C-9 | background mailbox polling |
| C-10 | sqlite-backed mailbox client |
| C-12 | libp2p mailbox client adapter |
| C-13 | swarm-backed mailbox transport |
| C-14 | mailbox host config (sqlite-persisted) |
| C-15 | per-recipient mailbox host routing |
| C-16 | DHT-based mailbox host discovery |
| C-18 | DHT bootstrap nodes |
| C-19 | E2E encryption for mailbox payloads |
| C-20 | multi-device mailbox cursors |
| C-21 | device-key signing for cursor commits |
| C-22 | CRL gate on mailbox poll |
| C-23 | frontend entities/acquaintance |
| C-24 | Prometheus metrics |
| C-25 | edge-case hardening (identity binding + service-level CRL) |
| C-26 | re-export CBOR helpers из kontinuum-core |