Kotlin
StableGenerated from capsule-sdk/bindings-surface.json, the dump mise run gen-bindings writes from
the capsule_sdk Kotlin sources uniffi 0.31.1
generates (uniffi/capsule_sdk/capsule_sdk.kt), and checks with --check in the Rust gate. It lists
every declaration the namespace exports; the generated source also carries the
converters and checksums uniffi needs internally, which are not a caller’s surface. To
change a description on this page, change the Rust doc comment it comes from and
regenerate; this file is build output. Which SDK serves which platform is the
SDK overview.
Functions
Section titled “Functions”syncedHistorySources
Section titled “syncedHistorySources”@Throws(FfiException::class) fun syncedHistorySources(manifestCbor: kotlin.ByteArray, history: List<kotlin.ByteArray>): FfiHistorySourcesName what adopting an asset from its history fetches: the live metadata blob and the
original, read off the asset’s latest record manifest_cbor (a feed entry’s) and history,
the records before it that FfiSession::fetch_provenance_history returned.
An album page — the one a writer member’s device reads — serves a delete with no metadata
blob and no blob, so a device that first sees the asset there has only its records. The live
metadata blob is the newest record that binds one, and the delete commits to the original;
the server serves both to the asset’s readers until its retention window closes. Fetch them
with FfiSession::fetch_metadata_blob and FfiSession::fetch_blob_to_path, set the
entry’s metadata_blob to the first, and hand the second to
FfiWorkspace::open_synced_history: the asset is held in this device’s trash and its
trash-restore can be signed. An entry that names its metadata blob (the owner’s own feed)
agrees with this answer.
Pure and offline. Nothing is trusted for being named: the adoption binds the metadata blob and the original to the records, and refuses what does not.
Errors
Section titled “Errors”FfiError::Sync when a record does not decode or none binds a metadata blob.
embeddedPreview
Section titled “embeddedPreview”@Throws(FfiException::class) fun embeddedPreview(originalPath: kotlin.String, ext: kotlin.String): FfiEmbeddedPreview?Read the embedded preview of the original at original_path, whose extension (without the
dot) is ext.
None when the original is not a RAW, carries no preview, or carries one that does not read
within the decode budget: each leaves the rung to the next source, so none is an error. The
reason is logged.
This reads the whole original into memory, because the extraction’s IFD bounds walk and its
stated-length guard run over the full file before the RAW reader does. Call it once per asset
and keep the FfiEmbeddedPreview it returns, rather than once per render.
Errors
Section titled “Errors”FfiError::Workspace when the file cannot be read.
renderStep
Section titled “renderStep”fun renderStep(request: FfiRenderRequest, shown: FfiRendition?, local: FfiLocalHoldings, fetchable: List<FfiRepresentation>): FfiRenderStepOne step of the render ladder (crate::fetch::render_step): what to show now and what to
obtain next. shown is what the surface shows, None when it shows nothing yet or is falling
back below what it showed; fetchable is what the server lists for the asset.
evaluateAlerts
Section titled “evaluateAlerts”@Throws(FfiException::class) fun evaluateAlerts(input: FfiNotifyInput, now: kotlin.String): List<FfiAlert>Every alert that is true at now, in delivery order.
Pure and offline: no network call, no library open, no clock read — now is the caller’s, so
the same input always gives the same answer and an app can drive it from a test clock.
Errors
Section titled “Errors”FfiError::InvalidArgument if now or any instant in input is not RFC 3339, or if
suppressed_until names something that is not an alert class.
nextAlertDeadline
Section titled “nextAlertDeadline”@Throws(FfiException::class) fun nextAlertDeadline(input: FfiNotifyInput, now: kotlin.String): kotlin.String?The earliest instant in pre_arm_deadlines (RFC 3339), or None when there is nothing to
arm.
For a host that can hold only one timer. An app that schedules per class wants
pre_arm_deadlines: the minimum discards the later class’s alarm, and on a device the app
never runs on again that alert is simply lost.
Errors
Section titled “Errors”As evaluate_alerts.
preArmDeadlines
Section titled “preArmDeadlines”@Throws(FfiException::class) fun preArmDeadlines(input: FfiNotifyInput, now: kotlin.String): List<FfiClassDeadline>The instant to arm a local notification for, per class — the call an app schedules its
UNCalendarNotificationTrigger / AlarmManager alarms from.
Reconcile your timers against the result: a class present here should hold exactly one alarm at the returned instant, and a class absent from it should hold none. Recompute after any state change; that is the whole of the arm / re-arm / cancel rule on the client side.
Only the two classes whose deadline a device can compute alone ever appear. The other three depend on server state and surface at next app launch — a real gap the design accepts.
Errors
Section titled “Errors”As evaluate_alerts.
p256HardwareDekRoundTrip
Section titled “p256HardwareDekRoundTrip”@Throws(FfiException::class) fun p256HardwareDekRoundTrip(hardware: FfiHardwareKeyAgreement, keyAlias: kotlin.String, mlSeed: kotlin.ByteArray): kotlin.BooleanSmoke the hardware-bound P-256 device encryption key across the FFI seam:
enroll a key-agreement key in hardware under key_alias, publish the hybrid public key,
encapsulate a fresh shared secret to it in software (the sender side), then
decapsulate through the element’s ECDH. Returns true iff the two secrets match —
proving the element’s ECDH composes into the hybrid KEM without the private scalar leaving
hardware. ml_seed (32 bytes) is the software-sealed ML-KEM-768 half.
The DEK is not yet wired into workspace creation, so this focused round-trip is the
composition’s on-device check — the DEK analogue of
FfiWorkspace::create_with_p256_hardware_signer. Every step is one capsule-core call.
Classes
Section titled “Classes”FfiCapsuleClient
Section titled “FfiCapsuleClient”open class FfiCapsuleClient: Disposable, AutoCloseable, FfiCapsuleClientInterfaceThe unauthenticated entry point over the SDK’s auth + upload endpoints. Turns
credentials into an FfiSession handle; the protocol pin and endpoints are
captured once here and reused by every session this client mints.
constructor
Section titled “constructor”constructor(authBaseUrl: kotlin.String, uploadBaseUrl: kotlin.String, protocolVersion: kotlin.String, cohortHash: kotlin.String?)Build a client.
auth_base_urlis the auth endpoint root (e.g.https://api.example.com/auth).upload_base_urlis the upload endpoint root (POSTtarget; chunk/HEAD hang under{upload_base_url}/{id}).protocol_versionis the pinned protocol date (YYYY-MM-DD).cohort_hashis the optional advisory device-cohort digest;Nonesends nothing (the server behaves identically).
suspend fun login(email: kotlin.String, password: kotlin.String): FfiLoginOutcomeAuthenticate with email + password, returning an authenticated session handle.
register
Section titled “register”suspend fun register(email: kotlin.String, password: kotlin.String): FfiSessionCreate an account and return an authenticated session handle (the server issues tokens on registration). An address and a password, and nothing else: the server takes nothing else. A display name is a fact about a person, and the profile surface that would hold one is owed rather than assumed.
verifySecondFactor
Section titled “verifySecondFactor”suspend fun verifySecondFactor(mfaToken: kotlin.String, totpCode: kotlin.String): FfiSessionComplete a sign-in with the code an authenticator app is showing.
mfa_token is what FfiLoginOutcome::SecondFactorRequired carried. It is good once
and for five minutes.
FfiSession
Section titled “FfiSession”open class FfiSession: Disposable, AutoCloseable, FfiSessionInterfaceAn authenticated session handle. Owns the SDK token store internally (auto
pre-flight refresh, single-flight, 401-retry-once); no token ever crosses
the FFI boundary. Every flow rides this handle.
escrowGet
Section titled “escrowGet”suspend fun escrowGet(apiBaseUrl: kotlin.String): kotlin.ByteArrayFetch this account’s escrow blob (GET /v1/auth/escrow) as opaque canonical CBOR — the
bytes FfiWorkspace::verify_escrow_blob checks and
a recovery flow unwraps.
No escrow enrolled yet is FfiError::Escrow carrying error.escrow.not_stored — the
code that separates “set up a recovery key” from “we could not read the one you have”.
A refused credential is FfiError::Auth.
escrowPut
Section titled “escrowPut”suspend fun escrowPut(apiBaseUrl: kotlin.String, blob: kotlin.ByteArray): FfiEscrowStoredStore or replace this account’s master-key escrow blob (PUT /v1/auth/escrow).
blob is the opaque canonical CBOR
FfiWorkspace::escrow_blob minted — the master key itself
never crosses this boundary in either direction.
Single active escrow: the server replaces any prior blob in the same transaction, so the secret a rotation retired unwraps nothing.
api_base_url is the API root the session authenticates against (the per-call endpoint
convention this surface already uses for sync_pull). A URL operation paths cannot hang
off is FfiError::InvalidArgument; a refused credential is FfiError::Auth, so a
caller re-authenticates rather than retrying; everything else is
FfiError::Escrow with the server’s error.escrow.* code when it sent one.
The answer says what the store did: FfiEscrowStored::replaced separates “recovery
set up” from “recovery key replaced” (the previous secret has stopped working).
fetchBlobToPath
Section titled “fetchBlobToPath”suspend fun fetchBlobToPath(blobBaseUrl: kotlin.String, hash: kotlin.String, size: kotlin.ULong, path: kotlin.String)Fetch the blob at content address hash (size bytes, as the sync feed’s
FfiBlobRef declares) into the file at path, created or truncated.
The blob streams to disk in bounded Range windows, so memory is bounded by one window
whatever the blob’s size, and its content address is checked as it lands. On any error
the file is removed: a path this returns Ok for holds exactly the verified ciphertext.
A session that cannot authorize the fetch is FfiError::Auth, so the app signs in
again; every other failure is FfiError::Fetch.
blob_base_url is the root the blob route hangs off ({blob_base_url}/blob/{hash}),
the server’s /v1 root.
fetchDeviceDirectory
Section titled “fetchDeviceDirectory”suspend fun fetchDeviceDirectory(apiBaseUrl: kotlin.String, userId: kotlin.String, pinnedUserIk: kotlin.ByteArray): kotlin.ByteArrayFetch a user’s signed device directory, verified under pinned_user_ik (the bytes
FfiWorkspace::user_ik_public returns) before it is
handed back. A document that does not verify is an error, never a return value.
fetchMetadataBlob
Section titled “fetchMetadataBlob”suspend fun fetchMetadataBlob(blobBaseUrl: kotlin.String, hash: kotlin.String): kotlin.ByteArrayFetch the metadata blob at content address hash, which a sync feed entry names by
address only (its metadata_blob): the feed lists no size for it, so it is read in
bounded Range windows until the server’s answer ends, and returned whole once it hashes
to hash. This is the bytes FfiSyncHead::metadata_blob carries into
FfiWorkspace::open_synced_original or FfiWorkspace::open_synced_history.
A sealed sidecar is a few KiB; a blob that grows past MAX_METADATA_BLOB_BYTES is
refused rather than buffered without bound. On a delete in trash the server serves the
asset’s live metadata blob to its readers until the retention window closes, so an asset
first seen at its delete can be adopted and restored; where the entry names no metadata
blob (an album page’s tombstone), synced_history_sources names its address.
blob_base_url is as for fetch_blob_to_path. A hash that
is not a lowercase hex SHA-256 is FfiError::InvalidArgument and reaches no network; a
session that cannot authorize the fetch is FfiError::Auth; a blob the server refuses,
that does not hash to its address, or that runs past the bound is FfiError::Fetch.
fetchProvenanceHistory
Section titled “fetchProvenanceHistory”suspend fun fetchProvenanceHistory(blobBaseUrl: kotlin.String, manifestCbor: kotlin.ByteArray): List<kotlin.ByteArray>Fetch the earlier provenance records of the asset whose latest record is
manifest_cbor (a feed entry’s): each record the one after it names by
prior_provenance_hash, back to the asset’s create, from the blob route the server
serves an asset’s superseded records on. Returned oldest first, each the exact bytes the
server holds, checked against its content address; empty for a create.
For an asset this workspace does not hold, first seen past its create — an entry
FfiWorkspace::apply_sync_entry_from_file quarantines as Rejected.ForgedChain when
the app’s catalog holds no head for it, or applies without adopting
(FfiAssetFacts::adopted false) when it does — hand the records to
FfiWorkspace::open_synced_history, which verifies them in order and holds the asset
from its create. Nothing is verified here: the walk only follows links.
An asset first seen at its delete is fetched the same way while it is in trash: the
server serves a trashed asset’s records, and its live metadata blob and original, to
its readers until the retention window closes, so the adopted asset can be restored. An
album page’s tombstone names neither blob; synced_history_sources names both from
these records.
blob_base_url is as for fetch_blob_to_path. A record the
server refuses (a deleted asset’s past its retention window, a purged asset’s, or one the
caller may not read) or that does not hash
to its address is FfiError::Fetch; a session that cannot authorize the fetch is
FfiError::Auth; a record that does not decode, or a chain longer than
MAX_HISTORY_RECORDS, is FfiError::Sync.
isAuthenticated
Section titled “isAuthenticated”suspend fun isAuthenticated(): kotlin.BooleanWhether the session currently holds tokens (false after logout).
logout
Section titled “logout”suspend fun logout()Revoke the session server-side and clear the local store (idempotent).
publishDeviceDirectory
Section titled “publishDeviceDirectory”suspend fun publishDeviceDirectory(apiBaseUrl: kotlin.String, directoryCbor: kotlin.ByteArray, userIk: kotlin.ByteArray): kotlin.ULongPublish this device’s signed device directory, returning the directory_version the
server now stores. directory_cbor is the opaque document
FfiWorkspace::signed_device_directory
produced; it travels verbatim, because re-encoding it would detach it from the signature
it carries. user_ik is the account’s user identity public key the document is
signed under — the bytes FfiWorkspace::user_ik_public
returns — which the server anchors on first publish and verifies every publish against.
Publish when the document changes (a device enrolled or revoked). The version must advance — republishing an unchanged document answers the version-conflict code.
pushAsset
Section titled “pushAsset”suspend fun pushAsset(workspace: FfiWorkspace, assetId: kotlin.String): FfiPushReportPush one sealed asset of workspace — every blob of its bundle, in ladder order
(provenance and metadata, then derivative manifests and derivatives, then the
original) — driving capsule_sdk::push::push_bundle inside the SDK.
The original never crosses the boundary: its ciphertext is re-derived from the plaintext on disk one upload chunk window at a time, so memory is bounded by the transfer window whatever the original’s size. The workspace lock is held only while the bundle is built, never across the network.
Every blob the reported path admits is driven (set_path); the tiers
it holds back are counted in FfiPushReport::deferred and wait for the next push. A
blob the server already holds answers duplicate_blob and comes back
FfiUploadOutcome::AlreadyStored, so pushing twice is safe. The album must already be
provisioned on the server.
A create the server refused is re-sealed and retried once (issue #822). When the
album’s epoch moved past the one the asset was sealed under, the server refuses its
create for good; if this device holds a newer epoch, the asset is re-sealed at it
(Workspace::reseal_create) and pushed again, once. A second refusal fails the push.
When this device holds no newer epoch the push fails with FfiError::Upload carrying
the server’s code and a detail saying there is no newer epoch to re-seal at, and the
asset stays local. That detail names no missing key: the refusal may have had nothing to
do with the epoch (a revoked device, withdrawn write access), or another device rotated
the album and its key has not reached this one yet.
To push several assets, use push_assets: one call here is one
bundle on the server’s lane, so the lane never holds more than one session open.
pushAssets
Section titled “pushAssets”suspend fun pushAssets(workspace: FfiWorkspace, assetIds: List<kotlin.String>): List<FfiPushAssetResult>Push several sealed assets of workspace in one call, returning one
FfiPushAssetResult per id, in the order of asset_ids.
Every asset is pushed exactly as push_asset pushes one — its
original streamed in bounded windows, the tiers the reported path holds back counted in
FfiPushReport::deferred, and a create the server refused re-sealed and retried once —
but the assets share one run of the server’s lane: up to W of them have their sessions
open at once, every range any of them sends reserves against one device-wide byte
budget, and their retries spend from one lane budget. Calling push_asset concurrently
instead gives each call its own budget, so the budgets add up rather than being shared.
The ids are pushed in batches of a bounded size, so a list of any length holds only one
batch’s bundles in memory at a time. One asset’s failure is its own
(FfiPushAssetResult::Failed, carrying the error push_asset would have returned for
it): the others still push, and the call itself never fails. An id repeated in
asset_ids pushes once, at its first occurrence; each later occurrence is
FfiPushAssetResult::Failed with FfiError::InvalidArgument.
The adverse-link detector hears each batch’s outcomes when that batch’s run ends, so a link that degrades mid-batch re-plans the push from the next batch on.
setPath
Section titled “setPath”fun setPath(snapshot: FfiPathSnapshot): kotlin.ULongReport the path as the app’s OS listeners see it now, and return its path generation.
Call it whenever a listener fires. The generation advances only when reachability, the interface or the cost changes; it then drops this session’s pooled connections, forgets the resets and stalls seen on the old path, and cancels — within one scheduling tick — any push work the new path forbids, which re-plans and resumes. Power, thermal and background-deadline changes re-plan without a new generation.
syncPull
Section titled “syncPull”suspend fun syncPull(apiBaseUrl: kotlin.String, cursor: kotlin.ByteArray, pageSize: kotlin.UInt): FfiSyncPagePull one page of the key-free sync feed after cursor from api_base_url
(http[s]://host:port). Pass an empty cursor for the first page; persist and pass
back next_cursor to advance. The bearer rides the Authorization header
(auto-refreshed); it never crosses the FFI boundary.
The parameter is the API base URL now, not a gRPC endpoint: the feed is
GET /v1/sync on the same origin as every other call.
transferEvents
Section titled “transferEvents”fun transferEvents(): FfiTransferSubscriptionSubscribe to the step-by-step progress of every upload, push and fetch this session drives from now on, each event stamped with the milliseconds since this call on a monotonic clock.
Progress is advisory: a transfer never waits on its observer, so a subscriber that
falls behind reads FfiTransferFeedItem::Lagged with the count it missed. The
subscription ends once this session is dropped.
upload
Section titled “upload”suspend fun upload(request: FfiUploadRequest, data: kotlin.ByteArray): FfiUploadOutcomeUpload a full ciphertext blob (data) under request, driving the adaptive,
resumable protocol with the code-driven recovery matrix.
data crosses the boundary whole, so this is for blobs an app already holds in memory.
To push a sealed asset, original included, use push_asset, which
never holds the original whole.
uploadStatus
Section titled “uploadStatus”suspend fun uploadStatus(sessionId: kotlin.String): FfiUploadStatus?Query an upload session’s progress (HEAD /upload/{id}). None means the
session is gone (discarded/expired/never-existent).
applyLifecycleOp
Section titled “applyLifecycleOp”suspend fun applyLifecycleOp(apiBaseUrl: kotlin.String, workspace: FfiWorkspace, record: kotlin.ByteArray, serverHead: kotlin.ByteArray?): FfiLifecycleOpOutcomeApply one signed lifecycle op record — a delete or a trash-restore, as the opaque
canonical CBOR FfiWorkspace::provenance_head,
FfiPurgeOutcome::reissue_record and
FfiWorkspace::purged_record carry — to its album’s ops surface on api_base_url
(http[s]://host:port, the root sync_pull takes).
The record must be one workspace holds: its device signature verifies under the
workspace’s device directory, and it is a record of the workspace’s chain for its asset
(live, trashed, or kept by a purge). The server cannot verify signatures, so a record
that is not this workspace’s own is refused here rather than becoming the asset’s chain
head there.
server_head is the sync feed’s evidence for the asset: the manifest_cbor of the
latest entry listing it (empty for an entry that carries none), or None when the feed
does not list it. With None nothing is sent and the answer is
FfiLifecycleOpOutcome::NotIndexed: the server never sees a lifecycle op for an asset
it has not indexed.
Idempotent: a record the server already applied answers Applied with replayed set.
A refusal, or a server that could not be reached, is FfiLifecycleOpOutcome::Failed;
a session the server did not accept is FfiError::Auth.
Errors
Section titled “Errors”FfiError::InvalidArgument for bytes that are not a lifecycle op record (over the
bound, not canonical, not a delete or trash-restore, not signed by a device of the
workspace’s directory, or not on the workspace’s chain for its asset) or an
api_base_url that is not an http(s) URL, before anything is sent;
FfiError::Auth for a refused session.
deliverLifecycleAtHash
Section titled “deliverLifecycleAtHash”suspend fun deliverLifecycleAtHash(apiBaseUrl: kotlin.String, workspace: FfiWorkspace, assetId: kotlin.String, serverHeadHash: kotlin.ByteArray?, latestChangedAt: kotlin.String?): FfiLifecycleOpOutcomeDeliver the held lifecycle records of an asset workspace still holds, live or in the
trash: every record of its chain after the head the server holds, in chain order, each
once the one before it was applied. That is a delete or trash-restore signed while
the server had not indexed the asset, or whose send failed, together with any it chains
onto that the server never saw. The rule is the CLI’s and a purge’s
(plan_delivery).
server_head_hash is the feed’s evidence for the asset, as
deliver_purge_at_hash takes it: None, an asset the
feed does not list, sends nothing and answers FfiLifecycleOpOutcome::NotIndexed.
latest_changed_at is the latest changed_at (RFC 3339) the caller’s feed drain saw,
on the server’s clock, or None when it saw none. It floors this device’s clock
(judging_clock), and a stretch in which a trash-restore undoes a delete whose
window had closed by that clock is withheld and answers
FfiLifecycleOpOutcome::RestoreTooLate: sent, the server would apply the delete and
refuse the restore.
Everything is read from the workspace’s chain, so a delivery that failed or was held is
retried by calling this again, also after the workspace was reopened. The workspace lock
is held only while the chain is read, never across the network, and every record of
the stretch is judged as apply_lifecycle_op judges a
foreign one before any is sent. A server level with the chain is sent its head alone,
which it answers as a replay; a caller that compares the listed head with
FfiWorkspace::provenance_head first sends nothing for it. The answer is the last
record’s Applied, or the first record that was not applied, whose Failed stops the
stretch.
Errors
Section titled “Errors”FfiError::Trash with FfiTrashRefusal::NotFound
for an asset this workspace does not hold (a purged one goes through
deliver_purge_at_hash); FfiError::InvalidArgument
for a malformed asset_id or api_base_url, a server_head_hash that is neither empty
nor 32 bytes, or a latest_changed_at that is not an RFC 3339 timestamp, before
anything is sent; FfiError::Workspace for a chain whose records do not verify under
the workspace’s device directory; FfiError::Auth for a refused session.
deliverPurge
Section titled “deliverPurge”suspend fun deliverPurge(apiBaseUrl: kotlin.String, workspace: FfiWorkspace, assetId: kotlin.String, serverHead: kotlin.ByteArray?): FfiLifecycleOpOutcomeDeliver a purged asset’s delete to its album’s ops surface: every record of the chain
the purge kept after the head the server holds, in chain order, each once the one
before it was applied. After an early purge that is the shorter-retention re-issue
(FfiPurgeOutcome::reissue_record) together
with any delete it chains onto that the server never saw.
Everything is read from the workspace’s kept chain, so a delivery that failed or was
held is retried by calling this again, also after the workspace was reopened
(FfiWorkspace::purged_asset_ids lists what to walk). The workspace lock is held only
while the chain is read, never across the network. Every record of the stretch is
judged as apply_lifecycle_op judges a foreign one before
any is sent, so a kept chain whose records no longer verify sends nothing.
server_head is as apply_lifecycle_op takes it: None
sends nothing and answers FfiLifecycleOpOutcome::NotIndexed. The answer is the last
record’s Applied — the purge reached the server — or the first record that was not
applied, whose Failed stops the stretch.
The held rules are a live asset’s
(deliver_lifecycle_at_hash), judged by this
device’s clock: a kept chain whose stretch holds a trash-restore undoing a delete the server never saw, whose window has
closed, is withheld and answers FfiLifecycleOpOutcome::RestoreTooLate.
Errors
Section titled “Errors”FfiError::Trash with FfiTrashRefusal::NotFound
for an asset this workspace has not purged; FfiError::InvalidArgument for a malformed
asset_id or api_base_url; FfiError::Workspace for a kept chain whose records do
not verify under the workspace’s device directory; FfiError::Auth for a refused
session.
deliverPurgeAtHash
Section titled “deliverPurgeAtHash”suspend fun deliverPurgeAtHash(apiBaseUrl: kotlin.String, workspace: FfiWorkspace, assetId: kotlin.String, serverHeadHash: kotlin.ByteArray?): FfiLifecycleOpOutcomedeliver_purge with the head the feed lists given by its record
hash rather than its bytes: the SHA-256 of the latest entry’s manifest_cbor, which is
the head record’s hash, so a caller keeps 32 bytes per listed asset instead of the
record. Some of an empty vector is an entry that carries no record, and None an asset
the feed does not list, exactly as deliver_purge reads its server_head.
Errors
Section titled “Errors”As deliver_purge, and FfiError::InvalidArgument for a
server_head_hash that is neither empty nor 32 bytes, before anything is sent.
FfiTransferSubscription
Section titled “FfiTransferSubscription”open class FfiTransferSubscription: Disposable, AutoCloseable, FfiTransferSubscriptionInterfaceA live subscription to a session’s transfer events: every step published after it opened.
Obtained from FfiSession::transfer_events.
Pull with next until it answers None, which it does once the session that
publishes the events is gone.
suspend fun next(): FfiTransferFeedItem?The next event, or how many were missed since the last pull; None once the session
publishing them is gone, and from then on.
Waits for an event when none is pending. Concurrent pulls take turns: each event is delivered to exactly one of them.
FfiWorkspace
Section titled “FfiWorkspace”open class FfiWorkspace: Disposable, AutoCloseable, FfiWorkspaceInterfaceA Capsule workspace, callable from Swift/Kotlin: the durable library at a filesystem root plus everything an app does with it.
Workspace mutates through &mut self while uniffi objects expose only &self; the
Mutex supplies the interior mutability and serializes concurrent foreign calls.
create
Section titled “create”@Throws(FfiException::class) fun create(root: kotlin.String, passphrase: kotlin.ByteArray, tier: FfiDeviceTier, client: FfiClientBuild): FfiWorkspaceEnroll: create a brand-new library at root, guarded by passphrase at the
Argon2id cost for tier. Mints the account, the device key, and the signed device
directory, and seeds the durable album keystore.
client is the app’s own build identity: every manifest this workspace authors then
reports client_id/semver+commit rather than the bare capsule-core default.
createWithHardwareSigner
Section titled “createWithHardwareSigner”@Throws(FfiException::class) fun createWithHardwareSigner(root: kotlin.String, passphrase: kotlin.ByteArray, tier: FfiDeviceTier, hardware: FfiHardwareSigner, keyAlias: kotlin.String, mlSeed: kotlin.ByteArray, client: FfiClientBuild): FfiWorkspaceEnroll with a hardware-bound Ed25519 device key: as create, but
the classical half of the device signing key is produced by hardware under
key_alias over Ed25519 (a 32-byte public key, 64-byte signatures). The
post-quantum ML-DSA-65 half is the software-sealed ml_seed (32 bytes).
No shipping secure element holds Ed25519 keys, so this is the software/TPM reference
composition; phones use
create_with_p256_hardware_signer. It is the
same core call (HardwareBackedSigner::enroll) reached through this namespace’s
FfiHardwareSigner seam.
createWithP256HardwareSigner
Section titled “createWithP256HardwareSigner”@Throws(FfiException::class) fun createWithP256HardwareSigner(root: kotlin.String, passphrase: kotlin.ByteArray, tier: FfiDeviceTier, hardware: FfiHardwareSigner, keyAlias: kotlin.String, mlSeed: kotlin.ByteArray, client: FfiClientBuild): FfiWorkspaceEnroll with a hardware-bound device key: as create, but the
classical half of the device signing key is produced by hardware (a native Secure
Enclave / StrongBox / TPM element) under key_alias, over ECDSA-P256 — the
composition shipping secure elements actually provide, none of which do Ed25519. The
post-quantum ML-DSA-65 half is the software-sealed ml_seed (32 bytes).
The published device key carries the P-256 tag and verify_asset dispatches on it. The
library records the binding (the alias, ml_seed sealed under the account master key,
and the published key), so every later launch reopens it with
open_with_p256_hardware_signer on the same
key; plain open refuses such a library. An element that refuses
enrollment fails this call before anything is written under root.
@Throws(FfiException::class) fun open(root: kotlin.String, passphrase: kotlin.ByteArray, tier: FfiDeviceTier, client: FfiClientBuild): FfiWorkspaceOpen the existing library at root under passphrase, restoring its album keys and
authorities, every managed asset, and the add_id counter from disk. This is
what makes an app relaunch resume rather than re-enroll.
openWithP256HardwareSigner
Section titled “openWithP256HardwareSigner”@Throws(FfiException::class) fun openWithP256HardwareSigner(root: kotlin.String, passphrase: kotlin.ByteArray, tier: FfiDeviceTier, hardware: FfiHardwareSigner, keyAlias: kotlin.String, client: FfiClientBuild): FfiWorkspaceOpen the existing library at root as open does, re-attaching
hardware, the element that holds this device’s P-256 signing key. Required for a
library made by
create_with_p256_hardware_signer: the element
is asked for the key under the alias the library recorded, never to make a new one, and
the rebuilt key must be the one this device published. An element that lost the key or
is unavailable fails the open with FfiError::Workspace rather than letting the
library sign with another key.
The library’s record decides, not the caller: a library created with the software key
opens on it and hardware is not used. key_alias is used only when root holds a
laid-out library with no account yet, whose fresh account is then enrolled under it.
assetsByCull
Section titled “assetsByCull”fun assetsByCull(flag: FfiCullFlag): List<kotlin.String>The managed, non-trashed assets currently carrying flag, sorted. Neutral includes
the never-flagged ones.
cullFlag
Section titled “cullFlag”fun cullFlag(assetId: kotlin.String): FfiCullFlagThe asset’s current flag: Neutral when it was never flagged, and for an asset this
workspace does not hold.
flagStack
Section titled “flagStack”fun flagStack(stackId: kotlin.String, flag: FfiCullFlag): List<kotlin.String>Flag every non-trashed member of a stack, one metadata-update each — flagging a
collapsed stack flags its members, since a stack stores no flag of its own. Returns the
members flagged, sorted; empty for a stack with no members.
rejectSweep
Section titled “rejectSweep”fun rejectSweep(retainDays: kotlin.UInt): List<kotlin.String>The reject sweep: move every rejected, not-yet-trashed asset to the trash with a
retention window of retain_days days, and return the swept ids, sorted. Each is a
signed delete, restorable from the trash until its window closes. A refusal partway
is FfiError::Trash; the assets swept before it stay in the trash, and a retried
sweep moves the rest.
setCull
Section titled “setCull”fun setCull(assetId: kotlin.String, flag: FfiCullFlag)Flag one asset. Neutral clears a prior flag. Signs one metadata-update, stamped with
this device and now, so concurrent flags from two devices converge under LWW.
stackCullState
Section titled “stackCullState”fun stackCullState(stackId: kotlin.String): FfiStackCullState?A stack’s cull state, derived from its members on this read. None for a stack with no
non-trashed members.
stackMemberships
Section titled “stackMemberships”fun stackMemberships(): List<FfiStackMember>Every managed, non-trashed asset that belongs to a stack, with its stack, whether it is
the cover and its member index, sorted by asset id. An asset in no stack is absent, so a
cull pass reads it as a lone asset; a trashed member is absent too, matching the members
flag_stack writes.
purgedAssetIds
Section titled “purgedAssetIds”fun purgedAssetIds(): List<kotlin.String>Every asset this workspace has purged (UUID strings), whose kept chain
purged_record reads: the list a relaunch walks to deliver the
purges whose delivery failed or was held, with FfiSession::deliver_purge. A purge
the server already applied answers as a replay, so walking it again is safe.
addLabel
Section titled “addLabel”fun addLabel(target: FfiLabelTarget, label: kotlin.String): List<kotlin.String>Add label to target and return its labels afterwards.
addRegion
Section titled “addRegion”fun addRegion(assetId: kotlin.String, shape: FfiRegionShape, labels: List<kotlin.String>): FfiRegionMark a region of asset_id with its first labels, and return it.
Every label is checked before the region is written, so a refused label leaves no unlabelled region behind; then the shape, then the asset. A refusal writes nothing.
albums
Section titled “albums”fun albums(): List<FfiAlbum>Every album this workspace holds key material for, in the one album order every app
shows as is: the album with the most recent visible, untrashed asset first, an empty
album by its creation time, ties by ascending id (Workspace::albums).
applySyncEntry
Section titled “applySyncEntry”fun applySyncEntry(entry: FfiSyncEntry): FfiSyncApplyOutcomeSync-apply: verify one remote feed entry through the verify_asset chokepoint,
bind and decrypt its metadata blob, and return either the facts to upsert or a named
quarantine/hold verdict.
Never silently drops and never silently accepts: a refused entry is a
FfiSyncApplyOutcome::Quarantined the app must surface, not an error that aborts the
page. Errors are reserved for a failure of this workspace itself.
An applied record that chains onto an asset this workspace holds is adopted: appended
to the asset’s chain with the metadata it binds, written and indexed, and the facts say
so (FfiAssetFacts::adopted). A create is adopted only by
open_synced_original, which has its original.
applySyncEntryFromFile
Section titled “applySyncEntryFromFile”fun applySyncEntryFromFile(entry: FfiSyncHead, ciphertextPath: kotlin.String): FfiSyncApplyOutcomeapply_sync_entry for an entry whose original ciphertext is
the file at ciphertext_path — the one
FfiSession::fetch_blob_to_path wrote —
read in memory bounded by one STREAM chunk whatever the original’s size.
The same chokepoint and verdicts: the file is hashed, and that digest is what the
content-integrity step checks against the manifest’s content address, so a file that is
not the committed ciphertext is quarantined as Rejected.CiphertextHashMismatch, as
apply_sync_entry quarantines those bytes. The ciphertext file is only read.
An applied record is adopted as apply_sync_entry adopts one:
a delete, trash-restore or edit of an asset this workspace holds is appended to its
chain, written under media/ and indexed, and the facts say so
(FfiAssetFacts::adopted).
A malformed album_id or local_chain_head is refused before the file is opened, so a
bad argument never costs a read of the original. The file is hashed before the workspace
is locked, so other verbs are not held up while a large original is read.
assetIds
Section titled “assetIds”fun assetIds(): List<kotlin.String>Every managed asset id (UUID strings).
createAlbum
Section titled “createAlbum”fun createAlbum(name: kotlin.String): kotlin.StringCreate an album (minting AMK_v1, its write-tier and admin keys, and an attested authority, then persisting the keystore). Returns the album id.
defaultAlbumId
Section titled “defaultAlbumId”fun defaultAlbumId(): kotlin.StringThe default album’s id, derived deterministically from the account master key — so every device of this account computes the same value with nothing to synchronize.
deleteAsset
Section titled “deleteAsset”fun deleteAsset(assetId: kotlin.String, retainDays: kotlin.UInt): FfiTrashEntryMove an asset to the trash: sign a delete whose retention window is retain_days days
from now, and return its trash entry. An asset already in the trash keeps the window it
was deleted with and signs nothing, so a retried delete never extends it.
deleteRegion
Section titled “deleteRegion”fun deleteRegion(assetId: kotlin.String, regionId: kotlin.String)Delete a region. Final: a concurrent or later edit of it on another device does not bring it back once the two merge, and its id is never live again.
deviceId
Section titled “deviceId”fun deviceId(): kotlin.StringThis device’s id (UUID string) — the created_by_device of every manifest it authors,
and the id to report as the session’s device_id.
ensureAlbum
Section titled “ensureAlbum”fun ensureAlbum(albumId: kotlin.String, name: kotlin.String): kotlin.StringResolve album_id to the album this workspace already holds, or create it under name.
This is the verb first-run wiring wants for the default album: it is idempotent across
relaunches, where create_album would refuse.
escrowBlob
Section titled “escrowBlob”fun escrowBlob(recoverySecret: kotlin.ByteArray, tier: FfiDeviceTier): kotlin.ByteArrayMint the master-key escrow blob under recovery_secret, as opaque canonical CBOR —
the bytes FfiSession::escrow_put stores.
The master key never crosses this boundary: it is wrapped inside capsule-core and only
the wrapped blob comes out. recovery_secret is the ≥128-bit secret shown to the user
exactly once at first-device enrollment.
expiredTrash
Section titled “expiredTrash”fun expiredTrash(drain: FfiFeedDrain?): List<FfiTrashEntry>The trash entries whose signed retention window has closed: the retention sweep’s
input, each one ready for purge_asset.
A deadline is judged only after a sync drained the feed to its head, so a restore on a
later page lands first: pass that drain as drain, its server_heads covering the whole
feed as FfiFeedDrain::server_heads requires. The deadline is judged by the
drain’s latest server changed_at alone, never by this device’s clock, and an asset
whose delete the server has not indexed (a held delete) never expires. With no drain
(None: the sync failed, or this device has no server), or a drain that saw no entry,
nothing has expired, and the trash keeps every asset until the user empties it.
Errors
Section titled “Errors”FfiError::InvalidArgument for a latest_changed_at that is not RFC 3339.
labels
Section titled “labels”fun labels(target: FfiLabelTarget): List<kotlin.String>The labels target carries, sorted. An unknown target is FfiError::Label with
FfiLabelRefusal::NotFound.
openSyncedHistory
Section titled “openSyncedHistory”fun openSyncedHistory(entry: FfiSyncHead, history: List<kotlin.ByteArray>, ciphertextPath: kotlin.String, destinationPath: kotlin.String): FfiOpenedOriginalopen_synced_original for an asset this workspace does not
hold and first sees past its create, with history the records before the entry’s,
oldest first from the create — what
FfiSession::fetch_provenance_history
returns for the entry’s manifest_cbor.
The entry is applied against the last record of history, whatever its
local_chain_head says, and its original opened from ciphertext_path as
open_synced_original opens one. Then every record of history is verified through
the chokepoint in order from the create, the sidecar is opened from the entry’s
metadata_blob (the asset’s live one, which the server’s metadata role names: a
superseded metadata blob is collected), and the asset is adopted, held from its
create with the opened original as its library copy. The facts say whether it was
(FfiAssetFacts::adopted); a held asset’s delete, trash-restore or edit then works
on this device as on one it imported.
An entry that names no metadata blob or original — a delete on an album page, the
surface a writer member’s device reads — is opened the same way once they are fetched:
synced_history_sources names both from the entry
and history, the fetched metadata blob goes in the entry’s metadata_blob, and the
fetched original is ciphertext_path. Left empty, the metadata_blob binds to nothing
and the entry is refused.
A history that does not verify, or a live metadata blob that does not bind, is
FfiOpenedOriginal::Refused with the verdict that refused it, and
destination_path is untouched: the original is placed there only once the asset it
belongs to verifies from its create. An empty history is open_synced_original, and so
is an entry of an asset this workspace already holds: its history is not read, so none of
its records can refuse the entry, and the entry is applied against its own
local_chain_head.
Errors
Section titled “Errors”As open_synced_original; an adoption whose write failed
leaves nothing at destination_path.
openSyncedOriginal
Section titled “openSyncedOriginal”fun openSyncedOriginal(entry: FfiSyncHead, ciphertextPath: kotlin.String, destinationPath: kotlin.String): FfiOpenedOriginalOpen a synced original from the ciphertext file at ciphertext_path to its plaintext
at destination_path, in memory bounded by a few STREAM chunks whatever its size.
The entry goes through the chokepoint as
apply_sync_entry_from_file would take it, then the
file is hashed while it is decrypted: it opens only if it is the ciphertext the manifest
commits to, every chunk authenticates, and the plaintext hashes to the signed sidecar’s
digest. The plaintext is written to a hidden staging file beside the destination and
renamed into place only once all of that holds, so destination_path is untouched by
any other verdict and never holds a partial or unverified original. An existing file there
is replaced only by a verified one.
An opened entry is then adopted: a create of an asset this workspace does not hold
makes it held, with its chain, sidecar and sealed metadata written under media/, the
opened original copied there as its own library copy, and its row and user
tags indexed; a later record of a held asset is appended as
apply_sync_entry_from_file appends one. The facts
say whether it was (FfiAssetFacts::adopted). destination_path still holds the
plaintext either way, for the caller to keep or remove.
The workspace stays locked while the original decrypts and is adopted.
Errors
Section titled “Errors”FfiError::Workspace when a file cannot be read or written, or the workspace itself
fails (an adoption whose write failed included, after the original was placed at
destination_path); FfiError::InvalidArgument for a malformed entry or a destination
with no file name. A refused entry is a verdict, never an error.
provenanceHead
Section titled “provenanceHead”fun provenanceHead(assetId: kotlin.String): kotlin.ByteArrayThe asset’s head provenance record as opaque canonical CBOR — the exact bytes the
provenance blob carries, the feed serves back as manifest_cbor, and a receiving
device runs through apply_sync_entry.
The record wraps the head signed manifest with its chain position. That wrapper is not
optional: the server’s chain head is the SHA-256 of these bytes while a client’s next
prior_provenance_hash is the record’s own hash, so the bare manifest is the one
encoding under which no lifecycle op can ever chain. Canonical CBOR is deterministic, so
the manifest inside is byte-identical to its own signed bytes — nothing is re-authored
and the two signatures still verify over exactly what they covered.
Use signed_manifest when the manifest is what is wanted.
purgeAsset
Section titled “purgeAsset”fun purgeAsset(assetId: kotlin.String): FfiPurgeOutcomePermanently delete a trashed asset: its original, every derivative and local cache
entry, its sidecar and its index row. The provenance chain is kept as the record of the
deletion. Before retention_until a signed shorter-retention delete is appended first
and handed back in FfiPurgeOutcome::reissue_record to send to the server.
purgedRecord
Section titled “purgedRecord”fun purgedRecord(assetId: kotlin.String): kotlin.ByteArray?The signed delete that ended a purged asset, the head of its kept provenance chain, as
the opaque canonical CBOR record FfiPurgeOutcome::reissue_record carries. After an
early purge it is that same re-issue, so a delivery to the album’s ops surface that
failed can be resent from here, also after a reopen
(FfiSession::apply_lifecycle_op, or
FfiSession::deliver_purge for the whole stretch).
None for an asset this workspace has not purged.
readPlaintext
Section titled “readPlaintext”fun readPlaintext(assetId: kotlin.String): kotlin.ByteArrayThe plaintext bytes of a managed asset (what a gallery renders for a locally-held original).
regions
Section titled “regions”fun regions(assetId: kotlin.String): List<FfiRegion>The asset’s live regions, oldest first.
removeLabel
Section titled “removeLabel”fun removeLabel(target: FfiLabelTarget, label: kotlin.String): List<kotlin.String>Remove label from target (every add of it this device has observed) and return its
labels afterwards. A label the target does not carry writes nothing and answers the
unchanged list.
restoreAsset
Section titled “restoreAsset”fun restoreAsset(assetId: kotlin.String)Restore an asset from the trash: sign a trash-restore. Refused with
FfiTrashRefusal::RetentionElapsed (and the error.upload.retention_elapsed code) at
or after the signed retention_until, and with FfiTrashRefusal::NotInTrash for an
asset that is not in the trash.
sealAsset
Section titled “sealAsset”fun sealAsset(albumId: kotlin.String, fileName: kotlin.String, bytes: kotlin.ByteArray): kotlin.StringSeal + import: take bytes the app already holds (a PhotoKit / MediaStore export),
derive the file key, STREAM-encrypt, author and sign the create manifest, seal the
signed sidecar into its metadata blob, append the provenance chain, and self-verify
through the verify_asset chokepoint. Returns the asset id.
file_name is consulted for its extension and nothing else. One call, one core call:
the whole sealing order is capsule-core’s.
signedDeviceDirectory
Section titled “signedDeviceDirectory”fun signedDeviceDirectory(): kotlin.ByteArrayThis workspace’s signed device directory as opaque canonical CBOR — the bytes
FfiSession::publish_device_directory
sends. Until this is published, every asset this device signs is an unknown device to
every other device, so first-device enrollment publishes before it uploads.
signedManifest
Section titled “signedManifest”fun signedManifest(assetId: kotlin.String): kotlin.ByteArrayThe asset’s head signed manifest as opaque canonical CBOR.
This is a serialization of an already-signed structure, not a re-authoring: the two signatures the manifest carries are covered by these bytes, which is why they travel verbatim and are never re-modeled on any wire.
Not what the sync feed carries — that is
provenance_head, the record that wraps this manifest with its
chain position. Handing these bytes to
apply_sync_entry is refused as malformed.
fun trash(): List<FfiTrashEntry>Every asset in the trash with its signed retention window, most recently deleted first.
This is the retention data, not the Recently Deleted view: a surface that shows the
trash still opens it behind the SR1 fresh-local-auth gate, as Catalog::query_trash
does.
uploadBlobs
Section titled “uploadBlobs”fun uploadBlobs(assetId: kotlin.String): List<FfiUploadBlob>Everything needed to push one sealed asset, in ladder order: for each blob, the
POST /upload body (manifest envelope projected per the server’s invariant-15 rule)
and the bytes. Feed each straight to
FfiSession::upload.
The index tier (T0) is two blobs — the provenance record then the sealed metadata
blob — because the server publishes an asset to other devices only once it holds both
roles. Push all of them: an app that stopped after the metadata blob would upload an
asset no device, its own included, could ever see.
The original’s ciphertext is re-derived from the manifest’s recorded nonce prefix and gated on the manifest’s own content address, so what reaches the network is what the signed manifest vouches for.
Every blob comes back whole, the original included. To push without holding the
original in memory, use FfiSession::push_asset.
Building the bundle records the original’s upload source guard; once every blob is materialised nothing re-derives the original from this bundle again, so the guard is released before the blobs are returned. A later call re-verifies the plaintext against the signed content address and records it afresh.
userId
Section titled “userId”fun userId(): kotlin.StringThe account owner’s user id (UUID string).
userIkPublic
Section titled “userIkPublic”fun userIkPublic(): kotlin.ByteArrayThis account’s user identity public key — the pin a fetched device directory is verified against.
verifyAsset
Section titled “verifyAsset”fun verifyAsset(assetId: kotlin.String): FfiVerifyOutcomeRun the verify_asset chokepoint over a managed asset, regenerating its ciphertext
deterministically. This is the same single chokepoint sync-apply routes through — it is
exposed here so an app can re-check its own library (an integrity sweep), never as a
second verification path.
verifyEscrowBlob
Section titled “verifyEscrowBlob”fun verifyEscrowBlob(blob: kotlin.ByteArray, recoverySecret: kotlin.ByteArray): kotlin.BooleanLocal, network-free check that recovery_secret still opens blob to this device’s
master key — the recovery verification cadence’s predicate. blob is the canonical CBOR
FfiSession::escrow_get returns.
A false is not necessarily a wrong secret: the cached blob may be stale after a
rotation on another device, so refresh it and re-check once before telling the user
anything.
Interfaces
Section titled “Interfaces”FfiHardwareKeyAgreement
Section titled “FfiHardwareKeyAgreement”public interface FfiHardwareKeyAgreementThe per-platform key-agreement element (Secure Enclave P256.KeyAgreement, StrongBox
ECDH, TPM 2.0 ECDH), implemented by native Swift/Kotlin over the uniffi foreign-trait
boundary — the ECDH analogue of FfiHardwareSigner, and this namespace’s declaration of
capsule-core’s HardwareKeyAgreement contract.
It backs the hardware-bound classical half of the device encryption key:
the element holds a static P-256 private key and performs the ECDH, so the scalar never
leaves hardware, while Rust drives the software-sealed ML-KEM-768 half and the hybrid
combiner. Failures reuse FfiHardwareSignerError, exactly as core’s trait reuses its
signer error.
enroll
Section titled “enroll”fun enroll(keyAlias: kotlin.String): kotlin.ByteArrayGenerate (or bind to) the hardware P-256 key-agreement keypair for key_alias,
returning its public key as uncompressed SEC1 (0x04‖x‖y, 65 bytes). Idempotent per
alias.
publicKey
Section titled “publicKey”fun publicKey(keyAlias: kotlin.String): kotlin.ByteArrayThe uncompressed-SEC1 P-256 public key for an already-enrolled key_alias.
keyAgreement
Section titled “keyAgreement”fun keyAgreement(keyAlias: kotlin.String, peerPublic: kotlin.ByteArray): kotlin.ByteArrayECDH against peer_public (the sender’s ephemeral P-256 public key, uncompressed SEC1)
with the hardware key for key_alias, returning the raw 32-byte shared secret (the
big-endian x-coordinate, before any KDF).
assertNonExportable
Section titled “assertNonExportable”fun assertNonExportable(keyAlias: kotlin.String)Non-exportability assertion: a conforming element MUST refuse to reveal the private
bytes, returning FfiHardwareSignerError::Exportable if they can be read.
FfiHardwareSigner
Section titled “FfiHardwareSigner”public interface FfiHardwareSignerThe per-platform secure element (Secure Enclave, StrongBox, TPM), implemented by native Swift/Kotlin over the uniffi foreign-trait boundary.
The byte shapes are fixed by the constructor the element is handed to, not by the trait:
FfiWorkspace::create_with_p256_hardware_signerandFfiWorkspace::open_with_p256_hardware_signer— the P-256 shape shipping elements actually provide:enroll/classical_public_keyreturn a SEC1 public key andsign_classicala DER-encoded ECDSA signature overSHA-256(msg).FfiWorkspace::create_with_hardware_signer— the Ed25519 shape: a 32-byte public key and a 64-byte signature overmsg. No shipping secure element holds Ed25519 keys, so this is the software/TPM reference path.
Either way Rust composes the classical half with a software-sealed ML-DSA-65 half into the published hybrid device key.
enroll
Section titled “enroll”fun enroll(keyAlias: kotlin.String): kotlin.ByteArrayGenerate (or bind to) the hardware keypair for key_alias, returning its public key in
the constructor’s shape: a SEC1 P-256 key for
FfiWorkspace::create_with_p256_hardware_signer, a 32-byte Ed25519 key for
FfiWorkspace::create_with_hardware_signer. Idempotent per alias.
classicalPublicKey
Section titled “classicalPublicKey”fun classicalPublicKey(keyAlias: kotlin.String): kotlin.ByteArrayThe public key for an already-enrolled key_alias, in the same constructor-dependent
shape enroll returns (SEC1 P-256, or 32-byte Ed25519).
signClassical
Section titled “signClassical”fun signClassical(keyAlias: kotlin.String, msg: kotlin.ByteArray): kotlin.ByteArraySign msg with the hardware key for key_alias, in the constructor’s shape: a
DER-encoded ECDSA signature over SHA-256(msg) for
FfiWorkspace::create_with_p256_hardware_signer, a 64-byte Ed25519 signature over
msg for FfiWorkspace::create_with_hardware_signer.
assertNonExportable
Section titled “assertNonExportable”fun assertNonExportable(keyAlias: kotlin.String)Non-exportability assertion: a conforming element MUST refuse to reveal the private
bytes, returning FfiHardwareSignerError::Exportable if they can be read.
Data classes
Section titled “Data classes”FfiAlbum
Section titled “FfiAlbum”data class FfiAlbumOne album this workspace holds key material for.
| Member | Declaration | Description |
|---|---|---|
albumId | var albumId: kotlin.String | Album id (UUID string). |
name | var name: kotlin.String | Display name, which lives only in the client’s own state — it never crosses the wire. |
FfiAlert
Section titled “FfiAlert”data class FfiAlertOne alert that is true at an instant.
Never a localized string: class selects the app’s own notification.* catalog key and
params are interpolated into it, so a server can neither supply nor influence the words a
user reads.
| Member | Declaration | Description |
|---|---|---|
class | var class: FfiAlertClass | Which alert this is. |
severity | var severity: FfiAlertSeverity | How prominently to present it. |
deadline | var deadline: kotlin.String? | The instant whose passing made this alert true (RFC 3339), for the two pre-armable classes; None for the three whose condition is server-held. |
params | var params: Map<kotlin.String, kotlin.String> | Catalog parameters — count, days_behind, grace, and the pair recovery_check_due always carries: snooze_budget (available / spent) and recovery (check / rewrap). |
FfiAssetFacts
Section titled “FfiAssetFacts”data class FfiAssetFactsThe verified facts one applied entry contributes, flattened for a catalog upsert.
| Member | Declaration | Description |
|---|---|---|
assetId | var assetId: kotlin.String | The asset id (UUID string). |
albumId | var albumId: kotlin.String | The album id (UUID string). |
amkVersion | var amkVersion: kotlin.UInt | The AMK epoch the asset is sealed under. |
ciphertextHash | var ciphertextHash: kotlin.String | The original’s ciphertext content address (lowercase hex). |
plaintextSize | var plaintextSize: kotlin.ULong | The original’s plaintext length in bytes. |
action | var action: kotlin.String | The head manifest’s lifecycle action (create, metadata-update, delete, …). |
timestamp | var timestamp: kotlin.String | RFC3339 authoring timestamp of the head manifest. |
createdByDevice | var createdByDevice: kotlin.String | The device that authored it (UUID string), resolved in the signed device directory. |
provenanceHead | var provenanceHead: kotlin.ByteArray | The 32-byte provenance head this entry establishes — persist it and pass it back as [FfiSyncEntry::local_chain_head] next time. |
metadata | var metadata: FfiAssetMetadata? | The decrypted, signature-checked metadata. None exactly when the action mints no metadata blob — a delete tombstone or a trash-restore. Those are fully-verified entries that simply carry no metadata, so an app applies the lifecycle change and leaves the row’s metadata as it stands. |
adopted | var adopted: kotlin.Boolean = false | Whether this workspace now holds the asset at this entry’s record: it adopted the entry, writing the asset’s chain, sidecar, sealed metadata and original under media/ and its index row and user tags into the library catalog, so the trash verbs and tag edits work on it. When true the catalog already describes the asset and an app writes nothing of its own for it; when false (an asset first seen past its create, a fork, or a verb that applies without adopting) the facts are the app’s to upsert. An asset first seen past its create is adopted from its earlier records: FfiSession::fetch_provenance_history, then [FfiWorkspace::open_synced_history]. |
FfiAssetMetadata
Section titled “FfiAssetMetadata”data class FfiAssetMetadataThe decrypted sidecar’s facts, flattened for a catalog upsert.
| Member | Declaration | Description |
|---|---|---|
sidecarSchema | var sidecarSchema: kotlin.UShort | Sidecar schema version. |
contentType | var contentType: kotlin.String | The original’s MIME type. |
captureTimestamp | var captureTimestamp: kotlin.String | RFC3339 capture time the asset is dated by: the user’s correction when one is recorded, else the as-imported value. |
width | var width: kotlin.UInt? | Pixel width, when the sidecar records dimensions. |
height | var height: kotlin.UInt? | Pixel height, when the sidecar records dimensions. |
lqipChromahash | var lqipChromahash: kotlin.ByteArray? | LQIP chromahash bytes — renderable the instant metadata syncs, at zero extra request. |
lqipDominantColor | var lqipDominantColor: kotlin.ByteArray? | LQIP dominant colour, as three RGB bytes. |
caption | var caption: kotlin.String? | The caption LWW register’s current value, if any. |
tags | var tags: List<kotlin.String> | The user-tag OR-set’s current members, sorted. |
rating | var rating: kotlin.UByte? | The rating LWW register’s current value, if any. |
hidden | var hidden: kotlin.Boolean | Whether the asset is hidden from default views. |
FfiBlobManifest
Section titled “FfiBlobManifest”data class FfiBlobManifestFFI mirror of BlobManifest — an asset’s blobs by role.
| Member | Declaration | Description |
|---|---|---|
original | var original: FfiBlobRef? | The original ciphertext blob, when this entry carries one. |
derivatives | var derivatives: List<FfiBlobRef> | Derivative / metadata / provenance blobs. |
FfiBlobRef
Section titled “FfiBlobRef”data class FfiBlobRefFFI mirror of BlobRef — a blob’s content address and role.
| Member | Declaration | Description |
|---|---|---|
ciphertextHash | var ciphertextHash: kotlin.String | Ciphertext content address (lowercase hex). |
role | var role: kotlin.String | original | metadata | derivative | provenance. |
size | var size: kotlin.ULong | Ciphertext size in bytes. |
FfiClassDeadline
Section titled “FfiClassDeadline”data class FfiClassDeadlineOne class and the instant its local notification should be armed for.
A class absent from pre_arm_deadlines’s result has no timer to hold: cancel whatever it
has.
| Member | Declaration | Description |
|---|---|---|
class | var class: FfiAlertClass | The class whose timer this is — also the notification.* catalog key the app renders when it fires. |
deadline | var deadline: kotlin.String | When to fire it (RFC 3339). |
FfiClientBuild
Section titled “FfiClientBuild”data class FfiClientBuildA foreign client’s self-reported build identity: its product id and own semver.
The git commit + dirty flag come from the core this app links, together composing the
client_id/semver+commit client_version every manifest carries — so a defect in a shipped
app build is traceable across the assets it produced.
| Member | Declaration | Description |
|---|---|---|
clientId | var clientId: kotlin.String | The app’s product id, e.g. capsule-ios. |
semver | var semver: kotlin.String | The app’s own semantic version, e.g. 1.4.2. |
FfiEmbeddedPreview
Section titled “FfiEmbeddedPreview”data class FfiEmbeddedPreviewA RAW original’s embedded JPEG, as embedded_preview reads it.
| Member | Declaration | Description |
|---|---|---|
jpeg | var jpeg: kotlin.ByteArray | The JPEG exactly as the RAW file carries it. |
width | var width: kotlin.UInt | Upright width in pixels, once orientation is applied. |
height | var height: kotlin.UInt | Upright height in pixels. |
orientation | var orientation: kotlin.UShort | The EXIF orientation (1..=8) to apply to jpeg’s stored pixels, in place of any tag the JPEG itself carries. |
FfiEscrowStored
Section titled “FfiEscrowStored”data class FfiEscrowStoredFFI mirror of EscrowStored — what FfiSession::escrow_put did.
| Member | Declaration | Description |
|---|---|---|
storedAt | var storedAt: kotlin.String | When the server accepted the blob, RFC 3339, on the server’s clock. |
replaced | var replaced: kotlin.Boolean | Whether this displaced an earlier escrow: false is a first enrollment (recovery set up), true a replacement (the previous recovery secret has stopped working). |
FfiFeedDrain
Section titled “FfiFeedDrain”data class FfiFeedDrainA sync that drained the feed to its head, the evidence FfiWorkspace::expired_trash
needs before it judges a retention deadline.
| Member | Declaration | Description |
|---|---|---|
latestChangedAt | var latestChangedAt: kotlin.String? | The latest server changed_at the drain saw (RFC 3339), or None for a drain that saw no entry. Deadlines are judged at it alone, never by this device’s clock, so with None nothing has expired. |
serverHeads | var serverHeads: Map<kotlin.String, kotlin.ByteArray> | Every asset the server’s feed lists, keyed by its id as the feed names it, with the manifest_cbor of its latest entry (empty for an entry that carries no record). An asset whose latest delete is not that head or before it is a held delete, which never expires. Ids that are not UUIDs name no asset of this workspace and are ignored. The map must cover the whole feed: a drain from the feed’s start (no cursor), or the heads this app kept, entry by entry, across every cursored drain since one. A drain resumed from a cursor lists only the assets that changed after it, so passing it alone leaves out every asset whose delete an earlier sync indexed: each is judged held and never expires, and the trash grows until the user empties it. That loses nothing, but the sweep stops working. |
FfiFeedEntry
Section titled “FfiFeedEntry”data class FfiFeedEntryFFI mirror of FeedEntry — one decoded sync feed entry. Ids/manifest/metadata
cross as opaque bytes.
| Member | Declaration | Description |
|---|---|---|
albumId | var albumId: kotlin.ByteArray | The album this entry belongs to. |
syncSeq | var syncSeq: kotlin.ULong | Per-album, strictly-increasing anti-rewind high-water mark. |
protocolVersion | var protocolVersion: kotlin.String | The album protocol pin (YYYY-MM-DD). |
kind | var kind: FfiChangeKind | What changed. |
assetId | var assetId: kotlin.ByteArray | The asset id. |
manifestCbor | var manifestCbor: kotlin.ByteArray | The signed AssetManifest as opaque canonical CBOR. |
metadataBlob | var metadataBlob: kotlin.ByteArray | The encrypted metadata blob’s content address, as UTF-8 bytes. On a delete in trash, the asset’s live metadata blob, which a restore needs; empty for a purged delete. |
blobs | var blobs: FfiBlobManifest | Content addresses of the asset’s blobs by role. On a delete in trash, only its original; none for a purged delete. |
originalHeld | var originalHeld: kotlin.Boolean | Whether the original blob is finalized server-side. |
changedAt | var changedAt: kotlin.String | When the change happened, RFC 3339, on the server’s clock. |
FfiHistorySources
Section titled “FfiHistorySources”data class FfiHistorySourcesFFI mirror of HistorySources: the blobs adopting an asset
from its fetched history needs beyond its records.
| Member | Declaration | Description |
|---|---|---|
metadataBlobHash | var metadataBlobHash: kotlin.String | The asset’s live metadata blob’s content address (lowercase hex), for [FfiSession::fetch_metadata_blob]; its bytes are the [FfiSyncHead::metadata_blob] [FfiWorkspace::open_synced_history] takes. |
original | var original: FfiBlobRef | The original the head commits to, role original, sized for [FfiSession::fetch_blob_to_path]; that file is the ciphertext_path [FfiWorkspace::open_synced_history] takes. |
FfiLocalHoldings
Section titled “FfiLocalHoldings”data class FfiLocalHoldingsWhat this device holds for the asset. Flat, like FfiNotifyInput: an embedded preview
is present when both of its dimensions are.
| Member | Declaration | Description |
|---|---|---|
held | var held: List<FfiRepresentation> | The encoded tiers and the original held locally; the LQIP is implied. |
embeddedPreviewWidth | var embeddedPreviewWidth: kotlin.UInt? | The embedded preview’s upright width, from [embedded_preview]. |
embeddedPreviewHeight | var embeddedPreviewHeight: kotlin.UInt? | The embedded preview’s upright height. |
FfiManifestEnvelope
Section titled “FfiManifestEnvelope”data class FfiManifestEnvelopeFFI mirror of ManifestEnvelope — the server-visible envelope fields the
import pipeline builds and signs; opaque to this layer, carried verbatim.
| Member | Declaration | Description |
|---|---|---|
cryptoSuiteId | var cryptoSuiteId: kotlin.UShort | Crypto suite the blob is sealed under. |
protocolVersion | var protocolVersion: kotlin.String | Protocol date (YYYY-MM-DD). |
albumId | var albumId: kotlin.String? | Optional album this blob belongs to. |
fileId | var fileId: kotlin.String | The asset id this blob belongs to (UUIDv7). |
amkVersion | var amkVersion: kotlin.UInt | AMK epoch the blob is sealed under. |
ciphertextHash | var ciphertextHash: kotlin.String | Ciphertext content hash, lowercase hex. |
plaintextSize | var plaintextSize: kotlin.ULong | Plaintext size in bytes. |
chunkSize | var chunkSize: kotlin.UInt | STREAM plaintext chunk size. |
keyMode | var keyMode: kotlin.String | derived | wrapped. |
metadataBlobHash | var metadataBlobHash: kotlin.String? | Hash of the bundle’s metadata blob, when known. |
derivativeRole | var derivativeRole: kotlin.String? | A derivative blob’s role (thumbnail | preview | embedding); None for every other blob. |
derivativeFormat | var derivativeFormat: kotlin.String? | A derivative blob’s format, e.g. image/jxl; set exactly when derivative_role is. |
createdByUser | var createdByUser: kotlin.String | The authoring user. |
createdByDevice | var createdByDevice: kotlin.String | The authoring device. |
clientVersion | var clientVersion: kotlin.String | The exact client build. |
timestamp | var timestamp: kotlin.String | RFC 3339 authoring timestamp. |
action | var action: kotlin.String | Lifecycle action (create for a fresh bundle). |
priorProvenanceHash | var priorProvenanceHash: kotlin.String? | Prior provenance hash, for append-only chains. |
retentionUntil | var retentionUntil: kotlin.String? | Optional retention floor. |
FfiNotifyInput
Section titled “FfiNotifyInput”data class FfiNotifyInputThe device-held state the predicate decides from, flattened for the bindings.
Counts and instants only — no album id, no title, no asset id. An all-default value (every
Option None, every count 0, an empty map) is the “just installed, learned nothing”
input, and yields no alerts and no deadline.
| Member | Declaration | Description |
|---|---|---|
lastCompletedSync | var lastCompletedSync: kotlin.String? = null | When the last completed sync finished (RFC 3339). None on a device that has never completed one — which raises no sync_stale, because the alert is about a stale sync and not a missing one. When None, unsynced_changes is ignored. |
unsyncedChanges | var unsyncedChanges: kotlin.ULong = 0uL | Changes still waiting to reach the server, including originals still pending under a staged upload policy. |
recoveryNextDue | var recoveryNextDue: kotlin.String? = null | When the next recovery-verification prompt becomes due (RFC 3339). Project it from the scheduler with RecoveryCadence::notify_facts rather than computing it here. None before recovery is set up, which ignores the other three recovery_* fields. |
recoverySnoozedUntil | var recoverySnoozedUntil: kotlin.String? = null | When an active snooze on the recovery prompt expires (RFC 3339), if one is active. A snooze ending before recovery_next_due does not make the check due earlier. This is the canonical place to snooze the recovery check — not suppressed_until, which is for the other five classes. |
recoverySnoozeBudgetSpent | var recoverySnoozeBudgetSpent: kotlin.Boolean = false | Whether the consecutive-snooze budget is spent — the class has degraded to a persistent, non-blocking badge: still reported, no longer pre-armed. |
recoveryRewrapDue | var recoveryRewrapDue: kotlin.Boolean = false | Whether the scheduler has escalated to the guided re-wrap (repeated failures, or the user declaring the secret lost). Surfaces as the alert’s recovery parameter, so the app routes into the re-wrap flow instead of rendering a routine verification prompt. |
quotaState | var quotaState: FfiQuotaAdvisory? = null | The state from the last GET /v1/quota. None before the first one. |
quarantinePending | var quarantinePending: kotlin.ULong = 0uL | How many items sit on the client’s quarantine surfaces awaiting a human. Excludes pending drops — they have their own class, so counting them here raises both. |
dropsPending | var dropsPending: kotlin.ULong = 0uL | How many guest drops are awaiting review and adoption. |
suppressedUntil | var suppressedUntil: Map<kotlin.String, kotlin.String> = mapOf() | Per-class snooze: class wire name (sync_stale, …) to the RFC 3339 instant the snooze runs until, exclusive. A class snoozed past now reports nothing, and its alarm is deferred to the snooze end rather than cancelled — a class snoozed after it fired must fire again when the snooze expires. Use disabled to turn a class off; do not encode that as a far-future instant here. An unrecognized class name is an [FfiError::InvalidArgument]. This map is for the other five classes. Recovery snoozing belongs in recovery_snoozed_until, which the cadence scheduler owns together with the budget that bounds it. An entry here for recovery_check_due is honoured as a fallback (the later of the two wins) but is not the canonical channel. |
disabled | var disabled: List<kotlin.String> = listOf() | Per-class disable: the wire names of the classes the user turned off. They report nothing and hold no alarm, at any instant. Disabling suppresses the warning and never the behavior. An unrecognized class name is an [FfiError::InvalidArgument]. |
FfiPathSnapshot
Section titled “FfiPathSnapshot”data class FfiPathSnapshotOne sample of the path as the app’s OS listeners report it.
Flat, like FfiNotifyInput: background_deadline_ms present
means the app runs in a background task the OS ends that many milliseconds from now; absent
means the foreground.
| Member | Declaration | Description |
|---|---|---|
cost | var cost: FfiPathCost | What the path costs. |
reachable | var reachable: kotlin.Boolean | Whether the OS reports a usable path. |
interface | var interface: FfiPathInterface | The interface the path runs over. |
power | var power: FfiPowerState | The device’s power source. |
thermal | var thermal: FfiThermalState | The device’s thermal state. |
backgroundDeadlineMs | var backgroundDeadlineMs: kotlin.ULong? | Milliseconds before the OS ends the background task the app runs in; None in the foreground. |
FfiPurgeOutcome
Section titled “FfiPurgeOutcome”data class FfiPurgeOutcomeWhat a FfiWorkspace::purge_asset did.
| Member | Declaration | Description |
|---|---|---|
assetId | var assetId: kotlin.String | The purged asset (UUID string). |
reissueRecord | var reissueRecord: kotlin.ByteArray? | The signed shorter-retention delete an early purge appended, as the opaque canonical CBOR provenance record [FfiWorkspace::provenance_head] would have returned. It goes to the album’s ops surface so the server may purge too, through FfiSession::deliver_purge, which sends it with any delete it chains onto that the server never saw; when that delivery fails, [FfiWorkspace::purged_record] hands the same record back from the kept chain. None when the window had closed. |
FfiPushReport
Section titled “FfiPushReport”data class FfiPushReportWhat FfiSession::push_asset did — FFI mirror of AssetPushReport.
| Member | Declaration | Description |
|---|---|---|
assetId | var assetId: kotlin.String | The asset id (UUID string). |
pushed | var pushed: List<FfiPushedBlob> | Every blob whose session was opened, in ladder order. |
bytes | var bytes: kotlin.ULong | Total bytes handed to the upload client, the original included. |
deferred | var deferred: kotlin.UInt | Blobs the reported path held back ([FfiSession::set_path]): push the asset again once the path allows them. Zero when the whole ladder ran. |
FfiPushedBlob
Section titled “FfiPushedBlob”data class FfiPushedBlobHow one blob of a pushed asset resolved — FFI mirror of PushedBlob.
| Member | Declaration | Description |
|---|---|---|
tier | var tier: kotlin.String | The ladder tier the blob occupies (index / preview / original), the vocabulary [FfiUploadBlob::tier] uses. |
hash | var hash: kotlin.String | The blob’s content address (lowercase hex). |
outcome | var outcome: FfiUploadOutcome | Whether it transferred or merged onto a blob the server already held. |
FfiRegion
Section titled “FfiRegion”data class FfiRegionOne live region of an asset and its labels.
| Member | Declaration | Description |
|---|---|---|
regionId | var regionId: kotlin.String | The region id (UUIDv7, minted fresh per create, never reused). |
shape | var shape: FfiRegionShape | The outline. |
labels | var labels: List<kotlin.String> | The region’s labels, sorted. |
FfiRenderRequest
Section titled “FfiRenderRequest”data class FfiRenderRequestA surface’s request: its intent and its size in device pixels when the view opened.
| Member | Declaration | Description |
|---|---|---|
intent | var intent: FfiRepresentation | The rung the surface’s intent needs: Thumbnail for the grid, Preview for the single-asset view, Original for a full-fidelity view. |
surfaceWidth | var surfaceWidth: kotlin.UInt | The surface’s width when the view opened. A view that grows keeps passing this. |
surfaceHeight | var surfaceHeight: kotlin.UInt | The surface’s height when the view opened. |
FfiRenderStep
Section titled “FfiRenderStep”data class FfiRenderStepThe answer to one render_step.
| Member | Declaration | Description |
|---|---|---|
show | var show: FfiRendition | What to show now. |
changed | var changed: kotlin.Boolean | Whether show differs from the shown passed in. |
next | var next: FfiNextStep? | What to obtain next, or None when the intent is met or nothing can raise it. |
FfiRendition
Section titled “FfiRendition”data class FfiRenditionWhat a surface shows: a rung and its source. The caller keeps the last one and passes it
back as shown.
| Member | Declaration | Description |
|---|---|---|
rung | var rung: FfiRepresentation | The rung shown. |
source | var source: FfiRungSource | What fills it. |
FfiStackMember
Section titled “FfiStackMember”data class FfiStackMemberWhere one live asset sits in its stack, read from its signed stack_membership register.
| Member | Declaration | Description |
|---|---|---|
assetId | var assetId: kotlin.String | The asset (UUID string). |
stackId | var stackId: kotlin.String | The stack it belongs to (UUID string). |
isCover | var isCover: kotlin.Boolean | Whether it is the stack’s cover: its primary member, the “best photo”. |
memberIndex | var memberIndex: kotlin.UInt? | Its position within the stack (burst sequence, chapter index), when one was recorded. |
FfiStampedTransferEvent
Section titled “FfiStampedTransferEvent”data class FfiStampedTransferEventA transfer event and when it arrived: milliseconds since the subscription opened, on a monotonic clock.
| Member | Declaration | Description |
|---|---|---|
atMs | var atMs: kotlin.ULong | Milliseconds since the subscription opened. |
event | var event: FfiTransferEvent | The event. |
FfiSyncEntry
Section titled “FfiSyncEntry”data class FfiSyncEntryOne remote feed entry to apply, as a sync feed delivers it.
album_id and the two id-bearing fields carry the feed’s own 16-byte UUID form (the
bytes album_id of capsule.sync.v1.SyncEntry), so an entry from
FfiSession::sync_pull is passed through unchanged.
| Member | Declaration | Description |
|---|---|---|
albumId | var albumId: kotlin.ByteArray | The album the feed claims this entry belongs to — 16 raw UUID bytes. Cross-checked against the manifest by the chokepoint itself, never trusted on its own. |
manifestCbor | var manifestCbor: kotlin.ByteArray | The signed AssetManifest as opaque canonical CBOR, carried verbatim. |
metadataBlob | var metadataBlob: kotlin.ByteArray | The sealed metadata blob the manifest commits to. |
originalCiphertext | var originalCiphertext: kotlin.ByteArray | The asset’s original ciphertext. Required: the chokepoint’s content-integrity step hashes it against the manifest’s declared content address, and there is no variant of that check which skips it — so fetch the original blob before applying. It crosses whole, so this record costs the original’s full size in memory. An original of any size is received with FfiSession::fetch_blob_to_path, and applied and opened from that file with an [FfiSyncHead] through [FfiWorkspace::apply_sync_entry_from_file] and [FfiWorkspace::open_synced_original], in bounded memory. |
localChainHead | var localChainHead: kotlin.ByteArray? | The 32-byte provenance head your catalog already holds for this asset, or None if it has never seen it. This is what decides replay and fork; feed back [FfiAssetFacts::provenance_head] after a successful apply. |
FfiSyncHead
Section titled “FfiSyncHead”data class FfiSyncHeadOne remote feed entry whose original is on disk rather than in hand: an
FfiSyncEntry without its original_ciphertext, which
FfiWorkspace::apply_sync_entry_from_file and FfiWorkspace::open_synced_original read
from the file FfiSession::fetch_blob_to_path
produced, in memory bounded by one STREAM chunk whatever the original’s size.
| Member | Declaration | Description |
|---|---|---|
albumId | var albumId: kotlin.ByteArray | As [FfiSyncEntry::album_id]. |
manifestCbor | var manifestCbor: kotlin.ByteArray | As [FfiSyncEntry::manifest_cbor]. |
metadataBlob | var metadataBlob: kotlin.ByteArray | As [FfiSyncEntry::metadata_blob]. |
localChainHead | var localChainHead: kotlin.ByteArray? | As [FfiSyncEntry::local_chain_head]. |
FfiSyncPage
Section titled “FfiSyncPage”data class FfiSyncPageFFI mirror of SyncPage — a decoded page plus the opaque cursor for the next
pull (the client persists next_cursor and passes it back to advance).
| Member | Declaration | Description |
|---|---|---|
entries | var entries: List<FfiFeedEntry> | The changes in this page, in strictly-increasing per-album sync_seq order. |
nextCursor | var nextCursor: kotlin.ByteArray | The opaque cursor to pass to the next [FfiSession::sync_pull]. |
hasMore | var hasMore: kotlin.Boolean | Whether the server holds changes beyond this page. |
FfiTrashEntry
Section titled “FfiTrashEntry”data class FfiTrashEntryOne asset in the trash, as its latest signed delete record describes it.
| Member | Declaration | Description |
|---|---|---|
assetId | var assetId: kotlin.String | The trashed asset (UUID string). |
deletedAt | var deletedAt: kotlin.String | When the delete was signed (RFC 3339, UTC). |
retentionUntil | var retentionUntil: kotlin.String? | The signed retention floor (RFC 3339, UTC): a restore is refused at or after it. None for a delete that carries no readable window, which never expires. |
restorable | var restorable: kotlin.Boolean | Whether a restore is accepted now, by this device’s clock. Computed by the workspace so no app re-implements the window; the server judges the same floor by its own clock. |
FfiUploadBlob
Section titled “FfiUploadBlob”data class FfiUploadBlobOne transferable blob of a sealed asset, paired with the POST /upload body that declares
it. Hand request and bytes straight to
FfiSession::upload — this record exists so an app never
has to assemble a manifest envelope itself.
The blobs come back in ladder order: the metadata blob (T0 — the index tier that makes the asset visible), then derivatives (T1), then the original (T2).
bytes holds each blob whole, the original included, so an original costs
its full size in memory here. FfiSession::push_asset
pushes the same blobs with the original streamed, and is the path for an original of any
size.
| Member | Declaration | Description |
|---|---|---|
tier | var tier: kotlin.String | The ladder tier this blob occupies (index / preview / original). |
hash | var hash: kotlin.String | The blob’s content address (lowercase hex) — the value the server dedups on. |
request | var request: FfiUploadRequest | The POST /upload body for this blob, with the manifest envelope already projected. |
bytes | var bytes: kotlin.ByteArray | The bytes to transfer. |
FfiUploadRequest
Section titled “FfiUploadRequest”data class FfiUploadRequestFFI mirror of CreateUploadRequest — the POST /upload body.
| Member | Declaration | Description |
|---|---|---|
size | var size: kotlin.ULong | Ciphertext size in bytes. |
hash | var hash: kotlin.String | Ciphertext content hash, lowercase hex. |
contentType | var contentType: kotlin.String | MIME type (closed enum per protocol version). |
cryptoSuiteId | var cryptoSuiteId: kotlin.UShort | Crypto suite the blob is sealed under. |
protocolVersion | var protocolVersion: kotlin.String | Protocol date (YYYY-MM-DD) this client speaks. |
blobRole | var blobRole: FfiBlobRole | This blob’s role in its bundle. |
manifestEnvelope | var manifestEnvelope: FfiManifestEnvelope | The unencrypted manifest fields the server validates. |
albumId | var albumId: kotlin.String? | Optional album to add the asset to. |
ownerId | var ownerId: kotlin.String? | Optional owner id (defaults to the authenticated uploader). |
intentId | var intentId: kotlin.String? | Album-upgrade intent id (album upgrade ceremony only). |
FfiUploadStatus
Section titled “FfiUploadStatus”data class FfiUploadStatusFFI mirror of HeadInfo — the HEAD /upload/{id} resumption/status primitive.
| Member | Declaration | Description |
|---|---|---|
offset | var offset: kotlin.ULong | Next expected byte (authoritative received-byte count). |
totalSize | var totalSize: kotlin.ULong? | Declared total size, when known. |
status | var status: kotlin.String | Session state (X-Capsule-Upload-Status). |
Enums and sealed classes
Section titled “Enums and sealed classes”FfiAlertClass
Section titled “FfiAlertClass”enum class FfiAlertClassEvery alert class, flattened for the bindings. A closed enum on both sides of the boundary.
| Member | Declaration | Description |
|---|---|---|
SYNC_STALE | SYNC_STALE, | Two weeks without a completed sync while changes remain un-synced. |
RECOVERY_CHECK_DUE | RECOVERY_CHECK_DUE, | A recovery-secret verification check is due. |
QUOTA_SOFT | QUOTA_SOFT, | Storage crossed the soft limit and is below the hard limit. |
QUOTA_GRACE_EXPIRING | QUOTA_GRACE_EXPIRING, | Storage is over the hard limit — the grace window is counting, or has closed. |
QUARANTINE_PENDING | QUARANTINE_PENDING, | Items are sitting on a quarantine surface awaiting a human. |
DROP_PENDING | DROP_PENDING; | Guest drops are awaiting review and adoption. |
FfiAlertSeverity
Section titled “FfiAlertSeverity”enum class FfiAlertSeverityHow prominently a client presents an alert. Neither variant gates anything — no alert blocks sync, unlock, upload, or any critical flow.
| Member | Declaration | Description |
|---|---|---|
ADVISORY | ADVISORY, | Informational: nothing is failing, and nothing is about to. |
WARNING | WARNING; | Something is already degraded or is being refused. |
FfiBlobRole
Section titled “FfiBlobRole”enum class FfiBlobRoleFFI mirror of BlobRole (snake_case on the wire, closed enum).
| Member | Declaration | Description |
|---|---|---|
ORIGINAL | ORIGINAL, | The source asset. |
DERIVATIVE | DERIVATIVE, | A client-generated thumbnail/preview. |
METADATA | METADATA, | The CBOR metadata document. |
PROVENANCE | PROVENANCE, | Append-only provenance. |
BACKUP | BACKUP, | A library backup artifact. |
DERIVATIVE_MANIFEST | DERIVATIVE_MANIFEST; | A derivative tier’s signed manifest. |
FfiChangeKind
Section titled “FfiChangeKind”enum class FfiChangeKindFFI mirror of ChangeKind.
| Member | Declaration | Description |
|---|---|---|
CREATED | CREATED, | A new asset became visible in the album. |
UPDATED | UPDATED, | An existing asset advanced — new metadata, new bytes, or a restore. |
DELETED | DELETED; | The asset was tombstoned. |
FfiCullFlag
Section titled “FfiCullFlag”enum class FfiCullFlagThe trinary culling flag, mirrored from core’s CullFlag. Neutral is the never-flagged
state and what an unwritten register reads as.
| Member | Declaration | Description |
|---|---|---|
PICK | PICK, | A keeper. |
NEUTRAL | NEUTRAL, | Not yet culled either way. |
REJECT | REJECT; | Marked for rejection: a candidate for the reject sweep. |
FfiDeviceTier
Section titled “FfiDeviceTier”enum class FfiDeviceTierDevice hardware tier, selecting the Argon2id cost at wrap time. Mirrors core’s DeviceTier
in this namespace (see the module docs on why it is not shared).
| Member | Declaration | Description |
|---|---|---|
LOW_RAM | LOW_RAM, | ≤ 2 GiB total RAM (entry-level Android / embedded). |
NORMAL | NORMAL, | Default for phones and laptops. |
DESKTOP | DESKTOP; | ≥ 8 GiB; used when wrapping new escrow blobs from a desktop. |
FfiException
Section titled “FfiException”sealed class FfiException: kotlin.Exception()Every failure the FFI flows can surface, flattened by originating layer. Each
variant carries the stable error.* catalog code (when one applies — clients
localize it) and the English detail (stays English), mirroring the
SDK’s { error, code } contract so foreign apps switch on the code, never a
bare HTTP status.
| Member | Declaration | Description |
|---|---|---|
Auth | class Auth | An authentication flow (login/register/refresh/logout) failed. |
Upload | class Upload | An upload flow (create/transfer/status) failed. |
Sync | class Sync | A sync flow (pull/reconcile) failed. |
Workspace | class Workspace | A local workspace operation failed — opening the library, sealing an asset, reading a managed asset. This is the workspace failing, never an entry being refused: a refused sync entry is a [FfiSyncApplyOutcome::Quarantined] verdict, not an error. |
Label | class Label | A label or region edit was refused before anything was written: its target does not exist, the label rule refused the label, or the shape broke the normalized-coordinate contract. refusal says which, so an editor shows the reason rather than parsing detail. |
Trash | class Trash | A trash verb (delete, restore, purge) was refused before anything was written: the asset is unknown, not in the trash, or past its signed retention window. refusal says which. |
Escrow | class Escrow | A master-key escrow flow (store/fetch) failed. |
Directory | class Directory | A device-directory flow (publish/fetch) failed. |
Fetch | class Fetch | A blob fetch ([FfiSession::fetch_blob_to_path]) failed: the blob is gone, still awaiting its original, refused, did not hash to its content address, or could not be written locally. Nothing is left at the destination path. |
InvalidArgument | class InvalidArgument | A foreign argument was structurally invalid (e.g. an un-parseable URL) before any network call. |
FfiHardwareSignerException
Section titled “FfiHardwareSignerException”sealed class FfiHardwareSignerException: kotlin.Exception()Failure surfaced by a foreign FfiHardwareSigner. Mirrors capsule-core’s
HardwareSignerError variant for variant — it is the same contract, declared in the
capsule_sdk namespace because core’s own type carries no uniffi derive.
The error is thrown by foreign code — it is the declared error of the
FfiHardwareSigner and FfiHardwareKeyAgreement foreign traits — so Rust must lift
it, and uniffi lifts only a non-flat error. Every variant therefore carries a named
detail field (the element’s own English diagnostic), which also gives the Kotlin
exceptions and Swift cases the same (detail) shape. Core’s error keeps detail only on
Backend; for the other variants it is logged at the seam and dropped.
| Member | Declaration | Description |
|---|---|---|
AuthCancelled | class AuthCancelled | The user cancelled the biometric / the element refused authentication. |
Unavailable | class Unavailable | No secure element is available on this device. |
NotFound | class NotFound | No key exists for the requested alias. |
Exportable | class Exportable | The private key was readable — non-exportability is violated (a failure). |
Backend | class Backend | Any other backend error. |
FfiLabelRefusal
Section titled “FfiLabelRefusal”sealed class FfiLabelRefusalWhy a label or region edit was refused (FfiError::Label). Nothing was written.
| Member | Declaration | Description |
|---|---|---|
NotFound | object NotFound : FfiLabelRefusal() | The asset, region, stack or album does not exist, or no longer does. |
LabelEmpty | object LabelEmpty : FfiLabelRefusal() | The label is empty once trimmed. |
LabelTooLong | data class LabelTooLong | The normalized label is this many UTF-8 bytes, past the 256-byte bound. |
RegionOutOfRange | object RegionOutOfRange : FfiLabelRefusal() | A coordinate is not a finite number in [0, 1]. |
RegionEmpty | object RegionEmpty : FfiLabelRefusal() | A radius, width or height is zero. |
RegionPastEdge | object RegionPastEdge : FfiLabelRefusal() | The shape extends past an edge of the image. |
FfiLabelTarget
Section titled “FfiLabelTarget”sealed class FfiLabelTargetWhat a label attaches to beyond the asset itself, whose own labels are its user tags. The label rule is the same at every level.
| Member | Declaration | Description |
|---|---|---|
Region | data class Region | A region of an asset. |
Stack | data class Stack | A stack (UUID string). |
Album | data class Album | A container album (UUID string). |
FfiLifecycleOpOutcome
Section titled “FfiLifecycleOpOutcome”sealed class FfiLifecycleOpOutcomeWhat became of one lifecycle op.
| Member | Declaration | Description |
|---|---|---|
Applied | data class Applied | The server applied the op, or had applied it already (replayed). |
NotIndexed | object NotIndexed : FfiLifecycleOpOutcome() | Nothing was sent: the feed does not list the asset, so its create is not indexed on the server, and the server never sees a delete for an asset it has not indexed. The record stays on the asset’s chain; deliver it once a sync lists the asset. |
RestoreTooLate | data class RestoreTooLate | Nothing was sent: a trash-restore in the stretch undoes a delete the server has not seen, and that delete’s window closed at retention_until. Sent, the server would apply the delete, refuse the restore and purge an asset the user restored in time. Withheld for good: the server keeps the asset live at the head it holds, the workspace keeps every record, and nothing after the pair can be sent either, since the chain is hash-linked. A later delivery judges it the same way, so it never needs retrying. |
Failed | data class Failed | The server refused the op, or could not be reached. permanent says whether sending the same bytes again can only be refused again. |
FfiLoginOutcome
Section titled “FfiLoginOutcome”sealed class FfiLoginOutcome: DisposableWhat a password login answered with, across the FFI.
An enum rather than an optional session handle: “signed in” and “needs a code” are different
states an app renders differently, and a null handle beside a mfaToken string would let a
caller read one while acting on the other.
| Member | Declaration | Description |
|---|---|---|
Session | data class Session | The account has no second factor, and this is its session. |
SecondFactorRequired | data class SecondFactorRequired | The password verified and a code is still needed. |
FfiNextStep
Section titled “FfiNextStep”sealed class FfiNextStepHow to obtain the next rung.
| Member | Declaration | Description |
|---|---|---|
Fetch | data class Fetch | Fetch it from the server. |
Generate | data class Generate | Produce it on the device from the original it holds. |
FfiOpenedOriginal
Section titled “FfiOpenedOriginal”sealed class FfiOpenedOriginalHow FfiWorkspace::open_synced_original resolved. Only Opened leaves anything at the
destination path.
| Member | Declaration | Description |
|---|---|---|
Opened | data class Opened | Verified end to end and decrypted: the destination now holds the original’s plaintext, which hashes to the signed sidecar’s digest. |
Refused | data class Refused | The entry did not apply, exactly as apply_sync_entry would say: held pending or quarantined. A ciphertext file that is not the original the manifest commits to is quarantined as Rejected.CiphertextHashMismatch. |
Undecryptable | data class Undecryptable | The entry applied, but its file key could not be recovered or its ciphertext failed authentication under it. |
DigestMismatch | data class DigestMismatch | The ciphertext opened, but the plaintext does not hash to the signed sidecar’s digest. |
FfiPathCost
Section titled “FfiPathCost”enum class FfiPathCostFFI mirror of PathCost.
| Member | Declaration | Description |
|---|---|---|
UNMETERED | UNMETERED, | Bulk transfer is acceptable. |
METERED | METERED, | The link is byte-counted (iOS isExpensive, Android without NOT_METERED). |
CONSTRAINED | CONSTRAINED; | The OS data saver is on (iOS Low Data Mode, Android Data Saver). |
FfiPathInterface
Section titled “FfiPathInterface”enum class FfiPathInterfaceFFI mirror of PathInterface.
| Member | Declaration | Description |
|---|---|---|
WIRED | WIRED, | Ethernet or another wired link. |
WIFI | WIFI, | Wi-Fi. |
CELLULAR | CELLULAR, | A cellular radio. |
OTHER | OTHER; | Anything else, or unknown. |
FfiPowerState
Section titled “FfiPowerState”enum class FfiPowerStateFFI mirror of PowerState.
| Member | Declaration | Description |
|---|---|---|
EXTERNAL | EXTERNAL, | On external power. |
BATTERY | BATTERY, | On battery. |
LOW_POWER | LOW_POWER; | On battery with the OS low-power mode on. |
FfiPushAssetResult
Section titled “FfiPushAssetResult”sealed class FfiPushAssetResultHow one asset of an FfiSession::push_assets call ended.
| Member | Declaration | Description |
|---|---|---|
Pushed | data class Pushed | The asset’s admitted ladder ran: what [FfiSession::push_asset] would have returned. |
Failed | data class Failed | The asset did not push: the error [FfiSession::push_asset] would have failed with. It stays local, and pushing it again is safe. |
FfiQuotaAdvisory
Section titled “FfiQuotaAdvisory”enum class FfiQuotaAdvisoryThe quota state, as GET /v1/quota reports it.
| Member | Declaration | Description |
|---|---|---|
OK | OK, | Below the soft limit. All uploads succeed normally. |
SOFT_WARNING | SOFT_WARNING, | At or above the soft limit and below the hard limit. Uploads succeed; the client warns. |
HARD_EXCEEDED | HARD_EXCEEDED, | At or above the hard limit. New uploads are rejected; the grace window is counting. |
GRACE_EXPIRED | GRACE_EXPIRED, | Over the hard limit for longer than the grace window; metadata-growth writes are refused too. |
SUSPENDED | SUSPENDED; | An admin or billing action rather than a threshold. Raises no quota alert. |
FfiRegionShape
Section titled “FfiRegionShape”sealed class FfiRegionShapeA region’s outline, in coordinates normalized to [0, 1] over the original’s display
orientation. Mirrors capsule_core::sidecar::RegionShape, single precision included, so a
shape that validates on one side validates on the other.
| Member | Declaration | Description |
|---|---|---|
Circle | data class Circle | A circle centred at (cx, cy) (fractions of width and height) with radius r, a fraction of the image’s shorter side. |
Rect | data class Rect | A rectangle with its top-left corner at (x, y). |
FfiRepresentation
Section titled “FfiRepresentation”enum class FfiRepresentationA rung of the ladder, cheapest first.
| Member | Declaration | Description |
|---|---|---|
LQIP | LQIP, | The LQIP, embedded in the metadata blob; always present. |
THUMBNAIL | THUMBNAIL, | The grid thumbnail. |
PREVIEW | PREVIEW, | The single-asset view’s preview. |
ORIGINAL | ORIGINAL; | The original. |
FfiRetryReason
Section titled “FfiRetryReason”sealed class FfiRetryReasonWhy an upload chunk is sent again. Mirrors RetryReason variant for variant.
| Member | Declaration | Description |
|---|---|---|
TransportReset | data class TransportReset | The connection reset or the request was lost; the same chunk is re-sent after the wait. |
OffsetRealign | data class OffsetRealign | The server named a different offset; the transfer continues from it. |
SessionLost | object SessionLost : FfiRetryReason() | The session was gone; a new one is created and the transfer continues from the offset it reports. |
ServerWait | data class ServerWait | The server asked the client to wait; afterwards the transfer resumes from the offset HEAD reports. |
FfiRungSource
Section titled “FfiRungSource”enum class FfiRungSourceWhere a shown rung’s pixels come from.
| Member | Declaration | Description |
|---|---|---|
LQIP | LQIP, | The LQIP (render_lqip), which paints the dominant colour where it does not decode. |
EMBEDDED | EMBEDDED, | The original’s embedded preview ([embedded_preview]). |
ENCODED | ENCODED, | The rung’s encoded tier. |
ORIGINAL | ORIGINAL; | The original, decoded. |
FfiStackCullState
Section titled “FfiStackCullState”enum class FfiStackCullStateA stack’s derived cull state, by the doc’s precedence all-rejected → any-pick → else mixed.
| Member | Declaration | Description |
|---|---|---|
ALL_REJECTED | ALL_REJECTED, | Every member is rejected: the whole stack is a sweep candidate. |
ANY_PICK | ANY_PICK, | At least one member is picked. |
MIXED | MIXED; | Neither: undecided, or rejects mixed with neutrals. |
FfiSyncApplyOutcome
Section titled “FfiSyncApplyOutcome”sealed class FfiSyncApplyOutcomeHow applying one remote entry resolved — the three verdicts the client validation duties permit. There is no “ignored” case: a failure is always a named quarantine or a hold.
| Member | Declaration | Description |
|---|---|---|
Applied | data class Applied | Verified end to end; upsert the facts. |
Pending | data class Pending | Authorization is intact but the epoch’s AMK has not arrived locally yet. Hold and retry — never quarantine. |
Quarantined | data class Quarantined | Refused. reason is a stable, switchable code; detail is English developer text. |
FfiThermalState
Section titled “FfiThermalState”enum class FfiThermalStateFFI mirror of ThermalState.
| Member | Declaration | Description |
|---|---|---|
NOMINAL | NOMINAL, | No thermal pressure. |
FAIR | FAIR, | Slightly elevated. |
SERIOUS | SERIOUS, | High: the OS asks apps to reduce work. |
CRITICAL | CRITICAL; | Critical: the OS asks apps to stop discretionary work. |
FfiTransferEvent
Section titled “FfiTransferEvent”sealed class FfiTransferEventOne step of an upload, a custody check or a ranged fetch. Mirrors TransferEvent
variant for variant and field for field; blob is the content address (lowercase hex) and
offsets and lengths are bytes of ciphertext.
| Member | Declaration | Description |
|---|---|---|
SessionCreated | data class SessionCreated | POST /upload opened a session; resume_offset is non-zero when the server returned an existing session for the same blob. |
SessionResumed | data class SessionResumed | An existing session resumed from the offset HEAD reported. |
ChunkSent | data class ChunkSent | A chunk left the client. |
ChunkAcked | data class ChunkAcked | The server acknowledged a chunk; acked_offset is its new received-byte count. |
ChunkRetried | data class ChunkRetried | A chunk will be sent again, or the transfer moves to another offset or session. |
ChecksumRejected | data class ChecksumRejected | The server refused a chunk whose body did not match its checksum; it is re-sent. |
Finalize | data class Finalize | Every declared byte reached the server; it is finalizing the blob. |
Merged | data class Merged | The server already held the blob: resolved as a merge, nothing sent. |
Verify | data class Verify | POST /storage/verify answered for an asset. |
Receipt | data class Receipt | A custody receipt for one blob verified under the pinned attestation keys. |
RangeRequested | data class RangeRequested | A ranged request left the client; max_len is the bounded window, or None for the open-ended remainder. |
RangeReceived | data class RangeReceived | Bytes of a ranged request landed. |
FetchRetried | data class FetchRetried | A ranged request brought nothing; the fetch backs off and resumes from the same offset. |
FetchVerified | data class FetchVerified | The fetched bytes hashed to the requested content address. |
IntegrityRejected | data class IntegrityRejected | The fetched bytes did not hash to the requested content address and were discarded. |
FfiTransferFeedItem
Section titled “FfiTransferFeedItem”sealed class FfiTransferFeedItemWhat one pull of a subscription yields.
| Member | Declaration | Description |
|---|---|---|
Event | data class Event | The next event. |
Lagged | data class Lagged | The subscriber fell behind and this many events never arrived. The transfers carried on; only their progress went unseen. |
FfiTrashRefusal
Section titled “FfiTrashRefusal”enum class FfiTrashRefusalWhy a trash verb was refused (FfiError::Trash). Nothing was written.
| Member | Declaration | Description |
|---|---|---|
NOT_FOUND | NOT_FOUND, | The workspace holds no such asset. |
NOT_IN_TRASH | NOT_IN_TRASH, | The asset is not in the trash: a restore has nothing to undo, and a purge never skips the trash. |
RETENTION_ELAPSED | RETENTION_ELAPSED; | The signed retention window has closed, so the asset can no longer be restored. It stays in the trash until it is purged. |
FfiUploadOutcome
Section titled “FfiUploadOutcome”sealed class FfiUploadOutcomeFFI mirror of UploadOutcome — how a full upload resolved.
| Member | Declaration | Description |
|---|---|---|
Completed | data class Completed | Every declared byte transferred; the server has begun finalization. |
AlreadyStored | data class AlreadyStored | The blob was already stored server-side (the duplicate_blob → merge path). |
FfiVerifyOutcome
Section titled “FfiVerifyOutcome”sealed class FfiVerifyOutcomeThe verification verdict, flattened for FFI (reasons as stable reason-code strings).
| Member | Declaration | Description |
|---|---|---|
Accept | object Accept : FfiVerifyOutcome() | The asset verified and may enter the trusted set. |
TerminalReject | data class TerminalReject | The asset is permanently rejected (quarantined); reason is the reason code. |
Pending | data class Pending | The asset is recoverable-pending (held + retried); reason is the reason code. |