Přeskočit obsah

S3 Data Exchange — unifikovaná datová rovina AVAX

Status: Draft 1.0 · 2026-06-06 · ultrathink spec Sjednocuje + rozšiřuje: s3-architecture.md, sync-backup.md, chat.md, android-call-capture.md, android-v0.5.md, per-app S3 provisioning. Cíl: JEDNA S3-centrická async vrstva pro veškerý přenos dat — device↔cloud (Android/desktop folder sync), app↔app handoff (hotline-capture→ hotline a desítky dalších), user↔user (chat). Místo 3 ad-hoc reimplementací.


0. Proč (motivace)

Dnes existují tři oddělené S3 mechanizmy se STEJNÝM principem:

Mechanizmus Spec Vzor
Chat chat.md avax-chat bucket, JSON-per-message soubory, pending_deliveries DB queue + adaptive polling
Folder sync sync-backup.md manifest.json + checksums + delta + snapshots (desktop)
Hotline capture android-call-capture.md Android folder → App#2 Uploader → S3 (STS) → app-hotline ingestion

Plus app-owned data, kde appky řeší S3 ad-hoc (app-hotline reuse APPDIST creds).

Princip je všude stejný — S3 = store, DB queue = notifikace, polling = doručení — jen 3× reimplementovaný. Tahle spec to sjednocuje a rozšiřuje o Android device sync + generic app↔app handoff.

Závazný princip (z chat.md, držet): ŽÁDNÝ WebSocket server, ŽÁDNÝ Redis Pub/Sub. S3 store + DB notif queue (pending_* / exchange_items) + adaptive polling. Škáluje na 100 000+ uživatelů při nízké HW zátěži (důvod původního rozhodnutí pro chat).


1. Kde data leží — roviny úložiště

Rovina Umístění Píše Čte Auth
User private companies/{cid}/users/{uid}/… user (device) user; backend on-behalf STS prefix-scoped
Company shared companies/{cid}/shared/{all\|roles\|custom/{did}}/ dle ACL dle ACL STS
App-owned companies/{cid}/apps/{slug}/data/ (nebo dedikovaný app-{slug} bucket) app backend app backend M2M static creds
Exchange / handoff companies/{cid}/exchange/{target}/inbox/{source}/… producer (app/device/user) target app ingestion STS (producer) / static (consumer)
Chat (special case) avax-chat bucket (cross-company, SSE) chat-service chat-service system IAM

Bucket-per-firma zůstává základ (s3-architecture.md); chat má vlastní cross-company bucket (je cross-company); exchange je per-company (izolace).


2. ROZHODNUTÍ — odpovědi na klíčové otázky

2.1 „Firemní složka uživatele NEBO bucket?" → SLOŽKA (prefix), NE bucket

Android sync cíl = prefix v company bucketu: companies/{cid}/users/{uid}/ sync/<folder>/. Bucket-per-FIRMA, ne per-user.

Proč (per s3-architecture.md): - GDPR výmaz firmy = drop 1 bucket (ne hon na 1000 user bucketů). - Kvóta + billing per firma. - Ceph/S3 nemá rád 100k+ bucketů; prefixů zvládne neomezeně. - Cross-user sdílení (shared/) ve stejném bucketu = triviální.

User izolace je stejně silná přes STS — token scoped na users/{uid}/ prefix nemůže číst cizí adresář. Bucket-per-user by bezpečnost nezlepšil.

2.2 Jak Android dostane S3 (mobile-auth → STS)

  1. Pairing (android-v0.5.md §7.1): launcher2 desktop → Settings → Devices → „Pair Android" → pairing JWT (QR 256×256, exp 10 min, scope device_pairing) → Android scan (CameraX+MLKit) → POST /v1/devices/register {pairing_jwt, device_info}mobile_device row + refresh_token (Android Keystore / EncryptedSharedPreferences) + access_token.
  2. STS per sync: Android → GET /storage/token?scope=sync&folder=<name> (Bearer access_token) → STS scoped na users/{uid}/sync/<name>/ (TTL 1 h) → přímý S3 upload/download (bez proxy přes backend).

Nikdy permanentní S3 klíč na telefonu. Vše STS, max 1 h.

2.3 Auth matrix

Kdo Token S3 přístup
Desktop/Android user JWT (launcher IPC / mobile refresh) → STS prefix-scoped, 1 h
App backend — vlastní data M2M → static app-data creds (.env) app prefix / bucket
App backend — ingestion z exchange M2M → STS scope=exchange (nebo company creds) exchange inbox

2.4 Test QR pairing (S3 creds → mobil) — vývojový bootstrap

Pro rychlé testování Android S3 přístupu (než je hotový plný mobile-auth STS pairing §2.2): launcher2 → Můj profil → „Spárovat mobil (S3 — TEST)" vygeneruje PIN-šifrovaný QR s company S3 creds + user prefix. Mobil naskenuje + zadá PIN → dešifruje → drží zašifrovaně v Android Keystore, plaintext jen v paměti.

Flow: 1. Launcher: GET /storage/mobile-pairing-creds (admin gate) → {endpoint, bucket: backup-{ico}, prefix: users/{uid}/, access_key, secret_key, user_id, company_id}. 2. Launcher: PIN (6 číslic) + mobile_pairing.build_pairing_payload(creds, pin) → QR text avaxs3:<b64url>. PIN zobrazen ZVLÁŠŤ, ne v QR. 3. Mobil: scan → user zadá PIN → dekóduj (kontrakt) → creds → re-encrypt pod Keystore klíčem → EncryptedSharedPreferences → AWS SDK na prefix.

Crypto kontrakt (reference: desktop/launcher2/mobile_pairing.py, self-test OK): - KDF: PBKDF2-HMAC-SHA256, 200 000 iter, salt 16 B → 32 B klíč. - Cipher: AES-256-GCM, nonce 12 B, bez AAD. - Obálka: {v:1, kdf, iter, salt, nonce, ct} (b64url bez padding); QR = avaxs3: + b64url(obálka). ~458 B.

Android (Kotlin) dekodér (parita):

// data.removePrefix("avaxs3:") → Base64.urlSafe → JSON env
// key = PBKDF2WithHmacSHA256(pin, salt, iter=200000, keyLen=256)
// AES/GCM/NoPadding .doFinal(ct) s nonce → creds JSON
// → EncryptedSharedPreferences (MasterKey AES256_GCM)

⚠️ Proč TEST-only: - Nese permanentní company creds (širší než STS) — únik = celý company bucket. Produkce = STS scoped na users/{uid}/ (§2.2). - 6-místný PIN je slabý proti offline brute-force zachyceného QR (PBKDF2 200k zvedá laťku, 10^6 v dosahu). QR krátkodobý, nikde neuložený — nefotit. - Gate: jen company_admin / super_admin. → Pro vývoj/pilot OK; před produkcí přepnout na §2.2 STS pairing.


3. Device folder sync (Android + desktop)

Manifest-based protokol (sync-backup.md §215+), zobecněný pro libovolnou složku a Android.

3.1 Cíl v S3

companies/{cid}/users/{uid}/sync/<folder-slug>/
├── manifest.json              ← stav: device_id, version, files[]{path,sha256,size,mtime}, snapshot ref
├── files/<relativní cesta>    ← obsah
└── .snapshots/<ts>/manifest.json   ← historie (rollback)

3.2 Sync cyklus

  1. Lokální manifest → spočítej checksums změněných souborů.
  2. Stáhni S3 manifest.json.
  3. Diff per soubor:
  4. S3 novější → download.
  5. Lokální novější → upload delta → zapiš nový manifest → snapshot.
  6. Oba změnili (konflikt) → uchovej obě (<name>.conflict-<device>), user resolve (per sync-backup.md §263).
  7. Atomicita: nový manifest.json se zapíše POSLEDNÍ = commit marker. Konzument nikdy nečte half-uploaded stav.

3.3 Android specifika

  • FileObserver na watched folder → foreground service → offline queue (bez sítě čeká) → STS refresh → upload.
  • Ready-marker: soubor uploadni až je stabilní (size unchanged ~3 s) — stejně jako hotline-capture <id>.m4a.partial→atomic rename.
  • Battery/data: WiFi-only toggle, batch, exponential backoff.
  • Refresh access_token před expirací (refresh_token z Keystore).

3.4 Sync-options katalog (co smí telefon zálohovat) — _options.json

Telefon v test-pairingu má jen S3 creds, NE JWT → nemůže volat katalog API. Proto backend předgeneruje nabídku kategorií jako JSON na S3, telefon si ho stáhne svými creds a zobrazí jako zaškrtávací seznam v Nastavení.

Cíl v S3: users/{uid}/sync/_options.json (v company bucketu, user-private prefix; telefon ho čte přes spárované creds).

Producer: backend POST /storage/sync-options/publish (admin gate přes get_company_credentials). Bere standardní kategorie + přístupné aplikace uživatele (get_catalog_for_company → required_roles + active subscription filter) → každá přístupná app = kategorie kind:"app" s default_checked=true. Launcher2 ji volá při QR pairingu (creds + options se publikují společně).

Schéma sync-options/v1:

{
  "schema": "sync-options/v1",
  "user_id": "<uuid>", "company_id": "<uuid>",
  "generated_at": "2026-06-06T…Z",
  "categories": [
    { "id": "photos",           "label": "Fotky",            "kind": "media",
      "default_checked": false, "suggested": "DCIM/Camera",  "s3_subdir": "photos" },
    { "id": "service-settings", "label": "Nastavení služeb", "kind": "settings",
      "default_checked": false, "s3_subdir": "service-settings" },
    { "id": "app-<slug>",       "label": "<Name> — zálohy",  "kind": "app",
      "app_slug": "<slug>", "has_access": true,
      "default_checked": true,  "s3_subdir": "apps/<slug>" }
  ]
}
- kind: media (fotky/galerie) · settings (konfig služeb) · app (záloha adresáře konkrétní AVAX appky — předzaškrtnuto když má user přístup). - s3_subdir: relativní cíl pod users/{uid}/sync/ (= <folder-slug> z §3.1). - suggested: výchozí device cesta (telefon ji předvyplní v SAF pickeru).

Consumer (Android Nastavení): stáhne _options.json → checklist (předzaškrtnuto dle default_checked) → per zaškrtnutá kategorie SAF folder picker (ACTION_OPEN_DOCUMENT_TREE) → uloží mapping {category_id → tree URI} do DataStore. Sync (§3.2) pak iteruje vybrané kategorie do users/{uid}/sync/<s3_subdir>/.


4. App↔App data exchange (handoff / inbox) — JÁDRO

Zobecnění hotline-capture→hotline a chat pending_deliveries do jednoho reusable vzoru pro „spoustu aplikací co si synchronizují data do aplikací".

4.1 Inbox vzor (producer → S3 → consumer ingestion)

Producer (app A backend / device / user) předává data target appce B:

  1. Zapíše do companies/{cid}/exchange/{B}/inbox/{source}/{item_id}/:
  2. payload.<ext> (audio, DWG, JSON, export…)
  3. meta.json zapsán POSLEDNÍ (ready-marker — atomicita).
  4. Registruje exchange item (notifikace) — jeden ze dvou módů:
  5. Pull (default): INSERT do exchange_items (pending) → B polluje.
  6. Push (volitelně): POST /apps/{B}/v1/ingest/notify (M2M).
  7. Consumer (B ingestion worker): claim item → stáhni payload → zpracuj → přesun do processed/{item_id}/ (nebo delete) → status done.

4.2 meta.json (ready-marker — drž stabilní)

{
  "schema": "exchange/v1",
  "item_id": "uuid",
  "source_app": "app-hotline-capture", "target_app": "app-hotline",
  "company_id": "uuid", "user_id": "uuid",
  "payload_file": "payload.m4a", "payload_schema": "hotline-capture/v1",
  "size_bytes": 0, "checksum_sha256": "…", "created_at": "iso8601"
}
Konzument item zpracuje AŽ když existuje meta.json (payload zapsán dřív).

4.3 exchange_items (DB notif queue — analog chat pending_deliveries)

CREATE TABLE exchange_items (
  id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  company_id   UUID NOT NULL,
  source_app   VARCHAR(100) NOT NULL,
  target_app   VARCHAR(100) NOT NULL,
  s3_key       TEXT NOT NULL,            -- …/inbox/{source}/{item_id}/
  item_meta    JSONB NOT NULL,
  status       VARCHAR(20) DEFAULT 'pending',  -- pending|processing|done|failed
  created_at   TIMESTAMPTZ DEFAULT now(),
  picked_at    TIMESTAMPTZ, done_at TIMESTAMPTZ, error TEXT
);
CREATE INDEX idx_exch_target ON exchange_items (target_app, status, company_id);
Target ingestion = adaptive polling: GET /v1/exchange/next?target=B (M2M) → claim (status→processing, FOR UPDATE SKIP LOCKED) → process → POST /v1/exchange/ {id}/ack (done/failed). Stejný anti-thundering-herd polling jako chat.

4.4 Konkrétně: app-hotline-capture → app-hotline

  • App#1 (Android capture) → fixní folder /sdcard/avaxhotline/ (lokálně, bez sítě).
  • App#2 (Uploader) = exchange producer: STS upload do users/{uid}/sync/ avaxhotline/ (device sync, §3) + INSERT exchange_items target=app-hotline.
  • app-hotline ingestion (android-call-capture.md §12) = consumer: claim → POST /calls (call_source=android, customer_phone_hash) → PUT audio_s3_key.

→ App#2 (uploader) je generický „exchange producer" — stejný kód pro jakoukoli device→app cestu.

4.5 Connector

Platform connector data-exchange 1.0 (provider __platform__): provides /v1/exchange/{next,ack,notify} + STS scope=exchange. Apps deklarují v connector.json pár produces / consumes (kdo komu posílá). Platform při register (per connector-registration-discipline.md) vytvoří inbox prefix + grant. Cross-company exchange jen přes explicit grant (default per-company).


5. Chat jako instance tohoto vzoru

chat.md (avax-chat, JSON-per-message soubory, pending_deliveries + polling) JE přesně tenhle vzor, kde: - producer + consumer = users, target = DM/group prefix, - „ingestion" = delivery (pending_deliveries → klient polluje).

Chat zůstává na dedikovaném avax-chat bucketu (cross-company, SSE-S3) — protože je cross-company. Ostatní exchange je per-company. Sjednocení = sdílená exchange lib (S3 write + ready-marker + DB queue + polling); chat je nejvyzrálejší instance, kód se z něj extrahuje.


6. Provisioning (navazuje na per-app S3 gap)

Rozšířit backend.yml „Provision" step + create-app: - App-data creds: dešifruj přiřazený system_app_data klíč → .env APP_S3_ENDPOINT/BUCKET/ACCESS_KEY/SECRET_KEY. - Exchange: app v connector.json deklaruje exchange.{produces,consumes} → platform vytvoří inbox prefix + grant + (consumer) ingestion worker. - boto3 + avax-exchange client lib do template backend/requirements.txt. - Device sync: app, co chce Android sync, deklaruje sync.folders[].


7. Bezpečnost / GDPR

Hrozba Ochrana
Únik klíčů firmy A → data firmy B Per-company bucket; STS i static creds platí jen pro svůj bucket
User vidí cizí adresář STS scoped na users/{uid}/ prefix
Producer píše mimo target STS scope=exchange scoped na exchange/{target}/inbox/
Permanentní klíč na telefonu Nikdy — jen STS (1 h) + refresh_token v Keystore
Replay / stale token Ceph STS jednorázové; access_token krátký
Ransomware/mazání DELETE zakázán bucket policy; jen GC job
GDPR výmaz firmy drop company bucket + exchange_items WHERE company_id
Retence processed/ + .snapshots/ TTL; GC (garbage-collector.md)

8. Implementační fáze

Fáze Co Stav
F0 Desktop folder sync (manifest) sync-backup.md
F1 Android device sync: mobile-auth pairing + STS sync + FileObserver upload. Pilot = app-hotline-capture App#2 (Uploader)
F2 Generic app↔app exchange: exchange_items + data-exchange connector + /v1/exchange/* + ingestion lib
F3 Refactor chat na sdílenou exchange lib později (ne nutné)
F4 Global tier (cross-company shared app data) ⏳ viz §9

9. Otevřené otázky (rozhodnout s Michalem)

  1. Globální vrstva (data co app sdílí VŠEM firmám — číselníky, šablony DWG, referenční normy): dedikovaný avax-global-{app} bucket (read-all přes presigned/CDN, write jen app backend)? Doporučuju zvlášť bucket per provider-app, read public, write M2M — mimo per-company izolaci.
  2. Exchange umístění: per-company companies/{cid}/exchange/ (izolace, doporučeno) vs central avax-exchange. Cross-company přenos jen přes explicit connector grant.
  3. Notif mód: podporovat pull (default, polling — bez webhooku do appky) i push; default pull.
  4. Velké soubory (DWG/video): multipart upload + presigned GET; size limity per firma (sync-backup.md limity).
  5. Sdílená exchange lib jazyk: Python (backend) + Kotlin (Android) — dvě implementace stejného kontraktu (meta.json/ready-marker/STS).

10. Související

  • s3-architecture.md — bucket-per-firma, STS, šifrování
  • sync-backup.md — manifest sync, limity, GDPR identity.json
  • chat.md — avax-chat, pending_deliveries, polling (vzor)
  • s3backup.md, garbage-collector.md — retence/GC
  • android-call-capture.md, android-v0.5.md — Android capture + mobile-auth pairing
  • connector.md, connector-registration-discipline.mddata-exchange connector
  • per-app S3 provisioning (app-data creds v .env)