Skip to content

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/startstart_meet_svcОбъявить устройство видимым для meet (mDNS + Swarm protocol)
POST/api/meet/stopstop_meet_svcСнять объявление
POST/api/meet/scanscan_meet_peers_svcmDNS-сканировать LAN на устройства в режиме meet
POST/api/meet/requestsubmit_meet_request_svcОтправить запрос знакомства discovered peer
POST/api/meet/approveapprove_meet_svcОдобрить входящий meet-запрос (по token)
POST/api/meet/rejectreject_meet_svcОтклонить входящий meet-запрос
POST/api/meet/invitecreate_meet_invite_svcСоздать invite-payload (QR / PIN / URL) для out-of-band обмена
POST/api/meet/invite/acceptaccept_meet_invite_svcПринять чужой invite-payload
POST/api/meet/introduceintroduce_meet_svcMediator: представить две своих acquaintances друг другу
GET/api/meet/pendingget_pending_meet_svcСписок pending входящих / исходящих meet-запросов
GET/api/meet/acquaintancesget_acquaintances_svcСписок known identities (результат успешных meet)
POST/api/meet/verifyverify_safety_number_svcОтметить out-of-band verified (SAS совпал)
POST/api/meet/revokerevoke_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_idBLOBPK — blake3(pubkey) чужой identity
pubkeyBLOBEd25519 public key (32 B)
display_nameTEXT?Имя, как назвал себя owner identity (необязательно, не доверяемо)
met_atINTEGERunix-эпоха момента meet
met_viaTEXTlan-mdns | invite-link | invite-qr | invite-pin | mediator | node-mailbox
mediator_identity_idBLOB?Если met_via = 'mediator' — кто представил
meet_proofBLOBПодписанный MeetCompletedPayload (для audit + повторной верификации)
safety_number_verifiedINTEGER0/1 — out-of-band SAS подтверждён
safety_number_verified_atINTEGER?
statusTEXTactive | pending | revoked
notesTEXT?Пользовательская заметка

Привязка к identity, не к device. Запись принадлежит owning identity, не конкретному устройству. Синхронизация между devices одного owner — через существующий device-pairing channel (см. §multi-device ниже).

Существующая таблица peersSharingManager) — это runtime-discovery (mDNS + libp2p Swarm), identity-scope acquaintances хранятся отдельно. Связь: peers.identity_id (если уже известен) JOIN acquaintances.identity_id.

Wire format: meet payloads

Все payload'ы — CBOR (parity с kontinuum-node wire-types). Подписи — Ed25519, ключ — identity signing key.

MeetInvite (создаётся Alice'ой)

text
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 экрана.
  • URLkontinuum://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'ом в ответ)

text
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)

text
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)

text
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) и решает — принять / отклонить. Если оба принимают, обмениваются MeetAcceptMeetCompleted через тот же канал, что и 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 в LANPIN = blake3(nonce) усечён до 6 цифр. Оба видят один номер — anti-MITM на mDNS.
MediatorПодпись Charlie над Introduction; верифицируется Bob'ом, у которого Charlie уже acquaintance.

SAS (Safety Number) — детерминированно derive'ится из обеих pubkey'ев после meet:

text
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)

plantuml Diagram

Сценарий 2 — Оба online, разные сети (relay)

plantuml Diagram

Сценарий 3 — Получатель offline (mailbox)

plantuml Diagram

Сценарий 4 — Mediator-initiated introduction

plantuml Diagram

UX-flow

ШагКто видитДействие
1. Создать inviteAlice«Добавить контакт» → выбрать способ (QR/link/PIN)
2. Передать inviteAlice → BobOOB-канал
3. Открыть inviteBobApp автоматически распарсит URL / scan QR
4. Подтвердить acquaintBobUI prompt: «Alice (display_name) хочет познакомиться. Принять?»
5. Получить notificationAliceUI prompt: «Bob (display_name) принял знакомство. Принять?»
6. ПодтвердитьAliceAccept → запись в acquaintances обоих
7. (Опц.) Сверить SASAlice ↔ 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 подменили в transitBob увидит чужие 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 потеряла ключи и recoveredKontinuum recovery → новая identity (новый pubkey). Прежние acquaintances НЕ узнают автоматически. Требуется ручной re-meet (см. ниже).
Bob revoke'нул AliceAlice'а удаляется из acquaintances Bob'а. Существующие шаринги не разрываются — отдельный flow (unshare_all_from(identity_id)).
MeetCompleted signature mismatchЗапись не вставляется, EventBus::MeetSignatureMismatch, UI показывает security warning.
Сетевой сбой между accept и completedTimeout 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):

  1. Помечает acquaintances.status = 'revoked' (soft) или удаляет запись (hard) — UI choice.
  2. Не уведомляет другую сторону (privacy parity с messengers; чужая сторона видит revoke только по факту, когда первый share_space_svc после revoke завершится с IdentityRevokedLocally).
  3. 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.rsMeetManager.
  • 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_space flow — 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:

  1. После device-pairing между двумя устройствами Bob'а, Bob's laptop получает initial snapshot acquaintances от phone.
  2. Дальнейшие изменения (INSERT после нового meet, UPDATE при verify, revoke) реплицируются через тот же sync-bus, что и spaces (опираемся на existing sync infrastructure).
  3. Это не делает 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/processInviter обрабатывает Accept, выдаёт Completed
POST/api/meet/completed/finalizeAcceptor завершает handshake (Pending → Active)
GET/api/meet/acquaintancesСписок acquaintance rows
POST/api/meet/acquaintances/revokeSoft revoke (status='revoked')
POST/api/meet/acquaintances/deleteHard delete row
POST/api/meet/safety-numberCompute 60-digit SAS для пары identity
POST/api/meet/safety-number/verifyMark SAS as verified out-of-band
POST/api/meet/swarm/start / …/stopЗапуск/остановка meet swarm (libp2p)
POST/api/meet/swarm/send-invitePush direction — отправить invite peer'у напрямую
POST/api/meet/swarm/accept-via-libp2pPull direction — Bob лично dial'ит Alice по hint
POST/api/meet/swarm/send-completedЗавершить handshake поверх libp2p
GET/api/meet/swarm/discovered-peersmDNS + dialed peers
GET/api/meet/swarm/listen-addrsBound multiaddrs локального swarm'а
POST/api/meet/mailbox/deposit-inviteПоложить invite в mailbox получателя
POST/api/meet/mailbox/pollРучной poll цикл
POST/api/meet/mailbox/polling/start / …/stopBackground 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/recipientOOB-маршрут к чужой identity (без DHT)
POST/api/meet/mailbox/host/publishОпубликовать host record в global DHT
POST/api/meet/mailbox/host/lookupDHT 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/bootstrapTriggered 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. Стек:
    • MailboxClient trait (backend/src/meet/mailbox.rs) — общая абстракция (deposit / read / ack) с двумя реализациями: InMemoryMailboxClient (для тестов) и SqliteMailboxClient (mailbox_sqlite.rs) для in-process persistence.
    • LibP2pMailboxClient (mailbox_libp2p.rs) — адаптер от MailboxEnvelope к wire-типам MailboxDepositReq / MailboxReadReq kontinuum-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).awaitunseal_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 берётся из inbound MeetInvite.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_revoked counter инкрементируется, 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_totalcounteroperation ∈ {create_invite, accept_invite, process_accept, finalize_completed, revoke, delete, mark_verified} × status
kontinuum_meet_mailbox_envelopes_totalcounteraction
kontinuum_meet_mailbox_deposits_totalcounterstatus
kontinuum_meet_acquaintances_totalgaugestatus
kontinuum_meet_mailbox_poll_duration_mshistogram

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 unions AcquaintanceStatus / MetVia с forward-compat brand (string & {}).
  • api.ts — обёртки над meetCommands с unwrapResult. Покрывает весь handshake + acquaintance CRUD.
  • useAcquaintancesStore.ts — Pinia store с optimistic local mutations, подпиской на meet_mailbox_polled event для авто-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-1libp2p codec + inbound handler
C-2libp2p swarm + outbound handle
C-3lifecycle integration + push services
C-4pull direction over libp2p
C-5peer discovery service
C-6mailbox fallback scaffolding
C-7bidirectional async mailbox
C-8mailbox cursor persistence
C-9background mailbox polling
C-10sqlite-backed mailbox client
C-12libp2p mailbox client adapter
C-13swarm-backed mailbox transport
C-14mailbox host config (sqlite-persisted)
C-15per-recipient mailbox host routing
C-16DHT-based mailbox host discovery
C-18DHT bootstrap nodes
C-19E2E encryption for mailbox payloads
C-20multi-device mailbox cursors
C-21device-key signing for cursor commits
C-22CRL gate on mailbox poll
C-23frontend entities/acquaintance
C-24Prometheus metrics
C-25edge-case hardening (identity binding + service-level CRL)
C-26re-export CBOR helpers из kontinuum-core