# Shared mobile contract (#91) `crates/mobile-bridge` is the single UniFFI source for phone records, enums, conversions, objects, operations and OS callbacks. `crates/android` packages it without features. `crates/apple` packages it with the explicit `watch` feature. Neither packaging crate copies converters. The component namespace is `ironstorage_mobile`; the native library names remain `ironstorage_android` and `ironstorage_apple`. Swift source/header names follow the shared namespace. ## Public workflow inventory | Storage owner | Shared phone exports | Identity / concurrency boundary | | --- | --- | --- | | mobile | shell, fixtures, tab selection | typed tabs/states/semantic icons | | mobile_home / git | Home page, commits/changes/actions, controlled operations/progress | typed actions, repository operation serialization and cancellation | | mobile_passwords | browse, search, folder/entry rows | storage entry paths and row IDs | | mobile_authentication / authentication | unlock, reveal/copy, preferences, identity/appearance/timeout, lifecycle | generation-scoped authentication handles, monotonic expiry | | mobile_entry / document / generate | structured fields, editor sessions, OTP image import, generation, save/discard | session u64 and field IDs; original ciphertext/conflict checks | | mobile_mutation / mutation | preparation, request, commit, outcome | opaque revision token validated at execution; explicit overwrite decisions | | mobile_totp / otp | discovery/progress/cancel, cached search, detail/code/copy | storage-owned catalog, Unix-time inputs and expiry | | mobile_onboarding / config | server discovery, clone and local setup, token replacement | typed transport/phase errors, controlled cancellation | | mobile_key_transfer / presentation | export/import, frames, progress, key selection | storage-owned QR envelopes and bounded decoding | | mobile_watch | **Apple opt-in only:** preferences, selection, snapshots/status/receipts | no Watch enum, field, method or control in the Android contract | `MobileIcon` expresses intent, never an asset name. UIKit maps it to SF Symbols in `MobileIcons.swift`. Android's core maps it exhaustively to its presentation enum; Compose maps navigation intents to vector drawables. Asset names and selected variants belong to the native UI. Actions already use typed enums. ## OS boundaries - `MobilePlatformPaths` accepts absolute private data and cache roots, rejects traversal and overlapping roots, and lets storage derive config/vault/key paths. Validation does not create or migrate files. #90 owns initialization, permissions, relocation, backup and cache policy. - `MobileSecretBackend` performs create/retrieve/replace/delete/lock/unlock on a typed locator and protection supplied by storage. Values are opaque encoded storage records. OS errors are enums. It cannot select accounts, protection, enrollment recovery, cache lifetime or authentication policy. Its adapter is usable by `mobileAuthenticationWithPlatform`; #93 implements Android Keystore. Preferences use that same injected backend, never a hidden native fallback. - `MobileClock` returns elapsed monotonic milliseconds from a stable origin. It cannot mint or refresh leases. Rust uses the existing authentication clock and generation checks. Unix timestamps required by TOTP/Git remain explicit inputs, not values parsed from rendered countdowns. - `MobileLifecycleEvent` reports foreground, background, device lock, real user activity, or cancellation. Storage revokes leases/editors on background/lock, checks expiry on foreground, and extends activity only for an active lease. - `MobileClipboard` reads/writes/clears OS text and waits for a requested timeout or cancellation. Rust's existing manager restores the prior text or clears only if the copied value is still current; newer clipboard content survives. Call off the UI thread; the facade serializes access to one OS clipboard. - `MobileCameraFrame` supplies luminance bytes, dimensions and row/pixel stride. Storage validates bounds (4096 pixels per dimension, 32 MiB plane, pixel stride 1–4) and decodes QR payloads. Native UI never interprets envelopes. - `MobileCredentialRequest` carries an opaque request ID, OS-asserted package, signing-certificate SHA-256 and web origins. Rust validates/canonicalizes the facts. A validated request is **not** an authorization grant. #109 implements matching, selected-candidate authentication and scoped grants in storage. ## Ownership, cancellation and diagnostics UniFFI objects are reference-counted Rust owners. Operation objects retain their authentication owner while work runs. Kotlin `close()` / Swift deinitialization release native handles; the final storage authentication owner destroys the lease and locks its store. Frontends cancel the exact operation on navigation or coroutine cancellation before releasing it. `MobileAuthentication.cancel` revokes its generation and discards editor state; it is not interchangeable with cancelling an individual discovery/Git operation. Late results are discarded by facade revision IDs. Polling, repaint and progress never count as user activity. Editor IDs and field IDs are stable within their owning session; revision tokens are opaque and must be returned unchanged to storage. Secrets and payloads are owned values, not persisted view state. Rust secret buffers zeroize on drop. Swift/Kotlin cannot promise complete managed-memory erasure, so callers release revealed/copy/OTP/frame values when their operation ends and never log or save them. Generated records, associated-value enums and errors have redacted diagnostic rendering; typed fields remain available for deliberate UI presentation. Unexpected native/callback errors redact their payloads. Mechanical generator hardening is version-checked and preserves wire layouts and every UniFFI contract/API checksum. ## Contract gates Every Android binding generation generates both Swift and Kotlin from the compiled phone component. The build tool verifies all records/fields, types, functions and operation checksums against metadata, writes `inventory.tsv`, and rejects any Watch item in the phone surface. The Apple opt-in build generates both languages too. `compare-contract` compares complete shared signatures; only Watch items and the opt-in Watch inputs to `preferences` differ. ```sh cargo build -p ironstorage-apple --lib cargo run -p ironstorage-android-build -- contract-bindings \ target/debug/libironstorage_apple.dylib android/uniffi.toml /tmp/ironstorage-watch-contract watch cargo run -p ironstorage-android-build -- compare-contract \ android/rust-bridge/build/generated/bindings-raw/debug/inventory.tsv \ /tmp/ironstorage-watch-contract/inventory.tsv ``` On non-macOS build hosts, use the host shared-library extension. Apple generation copies the verified/hardened Swift source and header into `apple/Generated`. Contract tests exercise typed errors, checksum failure, callback ownership, cancellation/concurrency, camera bounds, large foreign payloads and redaction. All app validation uses persistent emulator devices; no physical-device gate is part of this milestone's testing scope. The native Swift probe in `tools/android-build/tests/mobile_contract.swift` runs against the generated source and the host Apple library: ```sh xcrun swiftc -swift-version 5 -module-cache-path /tmp/ironstorage-swift-cache \ -I apple/Generated \ -Xcc -fmodule-map-file=apple/Generated/ironstorage_mobileFFI.modulemap \ -L target/debug -lironstorage_apple \ -Xlinker -rpath -Xlinker "$PWD/target/debug" \ apple/Generated/ironstorage_mobile.swift \ tools/android-build/tests/mobile_contract.swift -o /tmp/ironstorage-swift-contract /tmp/ironstorage-swift-contract ``` The Android native probe is `MobileContractTest`: run it with the existing `NativeRuntimeTest` on persistent 4 KB and strict 16 KB ARM64 emulators. UI and native instrumentation suites run sequentially to preserve input focus. The storage integration test exercises a real encrypted fixture with the injected secret backend/clock, activity renewal, background revocation, editor invalidation and final-owner cleanup without global configuration.