Skip to content

Kontinuum Node — App Integration

Client-facing flows: как kontinuum-app (Tauri) взаимодействует с kontinuum-node.

Audience: app developers. Pairing flows, identity recovery, free-tier / Standard / PRO UX, family-mode warnings, lapse timeline на стороне app.

Связанные документы:

Section codes (§6, §7.3.x, §9.5, §10.5, §13, §14, §15.2) сохранены от единой v0.6 спеки.


6. Identity, bootstrap, pairing, recovery

6.1 Onboarding клиента (kontinuum-app)

Free-tier (без покупок):

  1. App генерирует Ed25519 keypair, identity_id = blake3(pubkey). Ключ кладётся в Stronghold vault.
  2. App читает вшитые в код Tier 0 pubkeys + multiaddrs.
  3. App читает global DHT (для будущего pairing lookup), не публикует identity-stub.
  4. Cross-network sync не работает (мотивация купить relay).
  5. mDNS LAN sync доступен сразу.

С купленным relay (Standard Tier 1):

  1. Billing flow на сайте org → cert issuance Tier 0 → provisioning.
  2. Node boots, NodeHello к Tier 0, попадает в global DHT.
  3. Node создаёт Personal Space DHT владельца.
  4. Node обновляет запись node:{node_id} в global DHT.
  5. App владельца замечает обновление (push от Tier 0), подключается к Personal Space.

Opt-in публикация identity:

Settings → Privacy → «Allow others to find me by identity» (по умолчанию off).

  • ON → app публикует identity-stub в global DHT: identity:{hash(identity_id)} → Signed{ pubkey, personal_space_hint, last_seen }.
  • Объяснение в UI: «Друзья смогут спейриться с вами удалённо без предварительного контакта. Эта опция также нужна для удалённого восстановления identity через recovery phrase (см. §6.5). Trade-off: существование вашего identity_id становится глобально видимым.»
  • При выключении (toggle ON → OFF) — confirmation dialog: «⚠️ Отключение этой опции сломает удалённое восстановление identity по recovery phrase. Если потеряете все устройства — потребуется ручной URL relay-ноды (вы его помните?). [Disable anyway] [Cancel]»
  • Без paid infra checkbox видим, но greyed-out. При tap/click → tooltip: «Для публикации идентичности нужна купленная relay-нода или PRO-подписка — иначе identity_stub не на что ссылаться. Также: без paid infra и без opt-in publication удалённое восстановление identity невозможно. Купить relay → Settings → Buy.»

6.2 Pairing — local (in-person)

QR / PIN — как в kontinuum-app::pairing. Payload:

rust
struct PairInvitation {
    inviter_identity_id: String,
    inviter_pubkey: Vec<u8>,
    inviter_personal_space_id: Option<SpaceId>,
    inviter_dht_bootstrap_addrs: Vec<Multiaddr>,
    inviter_display_name: String,
    one_time_token: [u8; 32],
    valid_until: u64,
    signature: Vec<u8>,
}

6.3 Pairing — удалённый

Primary: Mediator-initiated social introduction.

Инициирует mediator (Charlie), не requester. Это даёт максимальный privacy: Alice не может «фишить» Charlie на наличие у него Bob'а как contact'а.

  1. Charlie open's app → Contacts → выбирает Alice и Bob → «Introduce these two».

  2. Charlie's app шлёт signed introduction обоим:

    Introduction {
      introducer: charlie_identity,
      introducee_a: alice_pubkey + alice_personal_space_hint,
      introducee_b: bob_pubkey + bob_personal_space_hint,
      signature_by_charlie: ...
    }
  3. Alice notification: «Charlie wants to introduce you to Bob. Accept?»

  4. Bob notification: «Charlie wants to introduce you to Alice. Accept?»

  5. Если оба accept → mutual pair complete. Trust наследуется от mediator (Charlie).

  6. Если кто-то reject → introduction отменена; mediator не уведомляется (privacy).

Fallback: signed invite link (без mediator).

  1. Alice генерит invite URL: kontinuum://pair?inv=<base64_signed_payload> (TTL 24 часа, одноразовый).
  2. Alice отправляет URL Bob'у любым каналом.
  3. Bob кликает → app разбирает → показывает Alice's display_name + усечённый identity hash.
  4. Bob принимает on-trust. UI предупреждает про MITM-риск.

Optional secure mode (для параноиков): OOB-fingerprint verification — отдельный setting.

6.4 Device re-discovery через rendezvous

Это не pairing-flow — это механизм для уже paired identities находить устройства друг друга при смене сети. Identity_id известны заранее (paired через §6.2 / §6.3); rendezvous отвечает только «где сейчас online устройство этого identity».

См. §6.6.

6.5 Identity recovery

Базовый механизм наследован из kontinuum-app::vault::recovery (BIP39 12-словная фраза + Argon2id + ChaCha20Poly1305).

Размещение vault backup:

Уровень пользователяКуда бэкап vault'a
Free-tierНет автоматического remote backup — manual export (USB / стороннее облако)
Standard + relayAuto-replicated на собственной relay-node (RF = 1)
PRO + N attached инфраAuto-replicated на нодах (RF = N, geo-diverse если возможно)

Flow восстановления:

  1. User устанавливает app на новом устройстве.
  2. Settings → Restore → вводит 12-словную BIP39 phrase.
  3. App деривирует master_key → identity_id.
  4. App ищет в global DHT identity-record по hash(identity_id) (требует чтобы был opt-in publish или fallback recovery stub — open question).
  5. Если не нашёл — manual URL relay-ноды.
  6. App connect'ится, signing-challenge от recovery phrase.
  7. Relay отдаёт encrypted vault backup → расшифровка → identity restored.
  8. New device генерирует свой device_id, добавляется в Personal Space registry.
  9. UI предлагает revoke старые device_id через DeviceRevoked.

UX при первичной настройке recovery phrase (onboarding):

При генерации recovery phrase app показывает:

⚠️ Recovery phrase позволит восстановить identity на новом устройстве. Однако для удалённого восстановления нужно включить «Allow others to find me by identity» в Privacy settings.

Без opt-in вы сможете восстановить identity только если запомните URL вашей relay-node вручную.

[Enable now] [Maybe later — I will remember the relay URL]

6.6 Rendezvous service

Что это: opt-in cross-network device-discovery для уже-paired identities без долгоживущей записи в DHT.

rust
struct PresenceToken {
    identity_id: IdentityId,
    device_id: DeviceId,
    public_multiaddrs: Vec<Multiaddr>,
    capabilities: Vec<Capability>,
    expires_at: u64,    // now + 60 sec
    signature: Vec<u8>, // подписан device key
}

Devices публикуют presence каждые ~30 секунд, TTL 60 секунд, без refresh → пропадает.

Где живёт:

  • Tier 0 — всегда (hardcoded fallback)
  • Tier 1 — по умолчанию ON
  • Tier 2 (PRO-attached) — по умолчанию OFF, user переключает

Three-level toggle в app:

  1. Identity-level (Settings → Privacy): «Allow my devices to find each other across networks» (по умолчанию OFF).
  2. Device-level: per-device «Advertise this device» (по умолчанию ON, если identity-level ON).
  3. Visibility scope (только два варианта — Public намеренно отсутствует, чтобы rendezvous не превращался в pairing-discovery):
    • OwnDevicesOnly — только устройства этой identity могут найти друг друга.
    • PairedContacts — paired identities могут lookup.

Rendezvous никогда не используется для discovery незнакомых identity — для этого служат pairing flows §6.2 / §6.3.

Rate limits (технические, не маркетинговые):

Защита от DDoS / перегрузки. Per-source-IP token bucket: e.g. 100 publish-ops/min, 1000 lookup-ops/min. Превышение → reject 429 Too Many Requests. При перегрузке ноды глобально → новые publish reject'ятся с указанием соседних rendezvous-нод. Не различает «free» vs «paid» пользователей.

Установление соединения после rendezvous:

  • LAN — mDNS быстрее, rendezvous пропускается.
  • Cross-network free-tier — только hole-punching через DCUtR (~70% NAT). При failure UI показывает объяснение: «Прямое соединение не удалось из-за NAT-конфигурации (вашей или peer's). Купите relay-node для гарантии соединения.»
  • Cross-network paid — circuit-relay-v2 через собственный Tier 1/2 node (100% success).

§7.3 Family / team mode — client UX

Server-side mechanics family-mode описаны в operations.md. Здесь — UX-side: предупреждения при подключении tenant'а к чужой ноде и flow выхода.

7.3.1 UX warnings при подключении к чужой ноде

Перед первым join'ом к host-ноде другого пользователя tenant обязательно видит два warning'а:

Warning #1 — Риск потери доступа:

⚠️ Эта нода управляется пользователем {Charlie}. Если он:

  • выключит ноду или отключит её от сети — ваши данные станут недоступны;
  • удалит вас из whitelist — у вас будет 14 дней на миграцию данных;
  • перестанет оплачивать инфраструктуру — нода перейдёт в read-only после 7 дней;
  • не продлит cert через 28 дней неоплаты — local data удаляется (см. §13.3).

Для критически важных данных рекомендуем купить собственный relay-node и pin'ить контент туда.

Warning #2 — Privacy implications:

⚠️ Владелец host-ноды {Charlie} имеет admin-доступ к серверу и физический доступ к диску. Ваш контент шифруется E2E — Charlie не может прочитать plaintext через обычные API.

Однако теоретически возможные attack vectors:

  • Физический доступ к ciphertext blob'ов на диске (без знания вашего space_key — useless).
  • Network metadata (timing, sizes, traffic patterns) — наблюдаемо для admin host-ноды.
  • При компрометации Charlie's identity ключа — может быть compromised и его доступ.

Для maximum privacy рекомендуем купить собственную relay-node — данные физически на инфре, которой управляете только вы.

Tenant подтверждает оба warning'а перед join'ом. Settings → My Hosts → видит warnings заново при любом изменении host's status.

7.3.2 Tenant exit flow

При удалении tenant identity из whitelist owner'ом:

ПериодTenant-режим
Day 0 → 14Graceful migration window — tenant в read-only на host's ноде; UI показывает «Charlie removed you, you have 14 days to migrate data» + кнопка «Export to USB / Buy own relay»
Day 14+Hard removal — tenant теряет доступ; данные на host's ноде удаляются

Если у tenant'а нет собственной paid infra — он fallback'ит в free-tier (LAN-sync only).

§9.5 User pins (UX side)

Полные placement-механики pins на сетевом уровне — в operations.md. Здесь — UX angle.

9.5 User pins / важный контент

User может пометить любой blob или весь Space как pinned. Это влияет на placement:

rust
struct Pin {
    target: PinTarget,           // Blob(hash) или Space(space_id)
    pinned_by: IdentityId,
    pinned_at: u64,
}

enum PinTarget {
    Blob(BlobHash),
    Space(SpaceId),
}

Эффект:

  • Pinned blob/space → forced replica в owner's primary storage (через Virtual Pool placement: LocalRelayFree + по крайней мере одна копия в OrgPaid / ExternalPRO для 11-девяток durability — см. §9.1).
  • Unpinned blob → может жить только в peer's cache, может GC'нуться когда peer чистит transit-cache (по quota pressure).
  • Это даёт user'у контроль durability per-blob.

Scope: pin'ятся только blob'ы и Spaces — не отдельные mailbox entries.

  • Если blob упомянут только в mailbox entry (не в Space) и не pinned → blob eligible для GC через transit-cache eviction, как и сам mailbox entry (по §10.2).
  • Чтобы сохранить blob из mailbox — user должен сначала «принять» message в какой-то Space (например, save attachment in personal photo album), а затем pin'нуть Space или конкретный blob внутри Space.

UX: на каждом blob/space в app есть «📌 pin to my storage» toggle. Если pin включён и Virtual Pool заполнен → app предупреждает «пора докупить S3 или удалить старые pin'ы».

Free-tier: toggle disabled с tooltip:

📌 Pinning требует primary storage. Купите relay-node или подпишитесь на PRO чтобы получить собственное хранилище для pin'ов. Без paid infra ваш контент живёт только на собственных устройствах — pin'ить нечего.

§10.5 Free-tier UX (продолжение §10 mailbox)

Mailbox structure / GC / TTL — в protocols.md. Здесь — client-facing warning flows для free-tier пользователей.

10.5 Free-tier UX: explicit messaging

Free-tier пользователь (без paid infra) не может получать offline-сообщения — у него нет mailbox.

Sender UX:

При попытке отправить сообщение или Share-блоб получателю-free-tier, sender видит warning:

💬 Получатель не имеет relay-ноды. Сообщение будет ждать в очереди до 30 дней пока получатель не окажется online. Если хотите гарантированную offline-доставку — попросите его купить relay.

Receiver UX (free-tier):

Persistent banner в app:

⚠️ У вас нет relay-ноды. Сообщения от друзей доставляются только когда вы online. Купите relay для гарантии получения. [Buy relay]


13. Модель PRO-подписки

13.1 Дистрибуция codebase — Scenario D (primary)

Один codebase, web checkout + license key с auto-activation, fallback на in-app subscription.

  • Один app билд (kontinuum-app).
  • PRO-функции gated через license-key.
  • License покупается на сайте kontinuum.org (вне Apple/Google).
  • Auto-activation flow (primary):
    1. После оплаты web-checkout шлёт user'у deep-link kontinuum://activate?token=<key> + email с тем же link как fallback.
    2. Click на deep-link → app получает token → отправляет на org backend → backend проверяет → возвращает signed PRO-cert → app активируется в PRO-режиме.
    3. App при первом старте проверяет clipboard на presence такого URL как UX-shortcut.
  • Manual token entry (workaround для desktop / Linux):
    • Settings → Activate PRO → paste field. User вставляет token из email вручную.
    • Нужно для случаев когда kontinuum:// deep-link не зарегистрирован в OS (свежая инсталляция на Linux без user интервенции, headless desktop, sandboxed browser).
    • Email с copy-friendly token блок обязательно отправляется параллельно с deep-link.
  • Преимущества: нет 30% cut Apple/Google, расширяемо на будущие подписки.
  • Risk: Apple App Store rejection.

Mitigation для Apple:

  • В app не упоминать что PRO покупается где-то ещё («reader app» exemption).
  • «Manage subscription» button → external browser → kontinuum.org/account (Apple разрешает).
  • Marketing через web, blog, Telegram-канал, word of mouth.

Fallback A (in-app subscription) — если Apple reject'ит, переключаемся на in-app sub с 30% cut.

13.2 Multi-VPS / home server attachment

Без cap на количество. PRO admin UI:

  • Add infrastructure wizard в Settings → My Infrastructure → Add node.
    • Тип: VPS у внешнего хостера (byo:vps) или домашний сервер (byo:home).
    • URL/multiaddrs.
    • Optional: external S3 binding для этой ноды — детальный UX design brief в design-prompts/external-s3-attachments.md.
  • List of attached nodes с per-node health, storage stats, geo-zone.
  • Replication policy editor — задать RF target (cross-provider / cross-region) в пределах собственной инфры.
  • Family/team mode toggle — расшарить ноду с whitelisted identities (§7.3).
  • Detach mechanism (graceful drain → remove from network).

13.3 PRO subscription lapse — Hybrid timeline (Variant 3)

Byo-нода следует unified cert lifecycle из §12.1 (single source of truth для всех Tier 1/2 нод). App-режим следует параллельным таймлайном:

ПериодApp-режимСостояние byo-ноды в сети (по §12.1)External S3
Day 0 → 7full PRO, daily reminderfull service (cert валиден)работает
Day 7 → 14app read-onlyread-only freeze — reads OK, writes reject, replication-in OKработает; нода продолжает serve'ить existing blob'ы peer'ам сети
Day 14 → 21graceful degrade в Free-режим, PRO UI скрытcold archive — reads медленные, paid restore offerработает; нода редиректит запросы на external bucket (§13.6)
Day 21 → 28Free-режимtombstone — reads = 410 Gone (для blob'ов в LocalRelayFree), restore возможенработает; редирект продолжается
Day 28+Free-режим, attached unmanagedhard delete local data; byo физически крутится, user может выключитьработает (org перестаёт «знать» про bucket); blob'ы остаются у user'а

Reactivation → cert renew → возвращается в full PRO, byo-ноды восстановлены, external S3 reconnect.

Важно: byo-нода физически продолжает работать на VPS / home server независимо от cert статуса. До этапа tombstone (day 21) она serve'ит existing blob'ы peer'ам сети — это поддерживает доступность контента для друзей владельца, не зависящего от своевременной оплаты. После tombstone — local data удаляется, но references на external S3 продолжают работать через redirect (§13.6).

Housekeeping API calls разрешены на всех этапах кроме hard delete (day 28+):

«App read-only» ограничивает user-facing writes — publish, share, send message. Maintenance операции продолжают работать:

  • Refresh presigned URLs для external S3 redirect-mode (§13.6).
  • Re-encryption job для forward secrecy после Revoke (§5.2.3).
  • Anti-entropy gossip для DHT consistency.
  • Provider records refresh.
  • Cert renewal attempt (для reactivation flow).

Это обеспечивает что lapse не ломает базовую infrastructure work — только блокирует новые пользовательские действия.

13.5 PRO mailbox scope

  • PRO нода byo × single хостит mailbox только владельца.
  • PRO нода byo × multi (family-mode) хостит mailboxes владельца + whitelisted tenants.
  • Mailbox paired-contacts (не в whitelist) — на ИХ собственной инфре, не на этой ноде.

13.6 External S3 при lapse — redirect mode

External S3 (Backblaze / R2 / AWS, прикручен в PRO) — invariant к PRO lifecycle:

  • Это собственность user'а у внешнего провайдера. User сам платит, мы не контролируем.
  • При PRO lapse → app переходит в read-only, перестаёт делать новые upload'ы в external bucket.
  • Существующие blob'ы в bucket — продолжают существовать. User может извлечь напрямую через S3-клиент.
  • После reactivation → app reconnect'ит bucket, продолжает использовать как primary storage.

Redirect mode для решения stale DHT references:

Чтобы peer'ы, лукающие blob'ы через DHT provider records, не получали stale references после PRO lapse:

  • При cert revocation byo-нода переключается в redirect-mode для blob'ов, физически лежащих в external S3.
  • На GET /presign для такого blob'а нода возвращает HTTP 307 Redirect на presigned URL external bucket'а.
  • Provider records в Space DHT обновляются: provider_kind = ExternalRedirect; URL bucket'а включён в запись.
  • Peer-клиент следует redirect и получает blob напрямую из external bucket — без участия orphan byo-ноды.

Multi-layer metadata защита (по умолчанию — privacy-preferred):

E2E-ciphertext защищает содержимое blob'ов, но public-readable bucket exposes metadata: factor существования identity, количество blob'ов, размеры, timestamp'ы, traffic patterns. Поэтому:

  1. Default = long-lived presigned URLs, не public-readable. App периодически refresh'ит URLs (см. §13.3 — housekeeping API calls разрешены даже в read-only режиме).
  2. Obscure bucket naming: вместо предсказуемого cnt-${identity_id_short}cnt-${blake3(identity_id || creation_nonce)[:16]}. Без знания creation_nonce атакующий не может target-enumerate bucket конкретного user'а.
  3. Disable LIST permission: только direct GET по известному blob_hash. Bucket listing rejected на provider level.
  4. Minimal HEAD responses: customize HEAD responses чтобы не возвращать timestamp / etag — только existence + size.
  5. Public-readable как opt-in для пользователей публичных Spaces (Settings → external bucket → «Allow public read») с explicit UX-warning о metadata-leak.

Эффект: blob'ы в external S3 остаются доступны сети даже после полного отключения byo-ноды, без участия app'a владельца, и без раскрытия metadata случайным observer'ам.


14. Bridge и миграции

14.1 PRO + relay одновременно

Если user одновременно купил relay-node (Standard) и подписался на PRO:

  • Primary — PRO byo-инфра (storage, DHT, mailbox).
  • Купленный relay-node — дополнительная geo-replica для Personal Space + free S3 в Virtual Pool.
  • Биллим оба параллельно: PRO subscription + relay rental.

14.2 Upgrade Standard → PRO

User купил relay-node как Standard, затем перешёл на PRO:

  • Купленный relay сохраняется как доп. реплика и доп. источник Free S3 в Virtual Pool.
  • Personal Space DHT мигрирует с дублированием на новые byo-ноды.
  • Relay → переходит в роль secondary, byo-нода становится primary.

14.3 Downgrade PRO → Standard

User отказался от PRO subscription, но сохранил relay-node:

  • Через timeline §13.3 byo-ноды становятся orphans.
  • Personal Space мигрирует обратно на relay-node как primary.
  • External S3 — invariant (см. §13.6), но больше не интегрирован в Virtual Pool до reactivation.
  • Пользователь продолжает как Standard user.

§15.2 App-side: self-hosted control

Org-side admin (Grafana / Directus / operator chat bot) — в operations.md.

15.2 App-side: self-hosted control

Только для operator ∈ {byo:vps, byo:home} (PRO):

  • Управление встроено в kontinuum-app (Settings → My Infrastructure).
  • Identity-аутентификация (owner identity_id = node's owner field).
  • Действия: view storage, view quotas, restart, view logs (tail), upgrade software, decommission, manage replication policy, family-mode whitelist.