field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit a5777fe23da4315f5b9125c10d61d0755e6631f7
parent fd5b81fd2b4f2e3a9264f628f8a1178824a20f5a
Author: triesap <tyson@radroots.org>
Date:   Sun, 20 Sep 2026 12:02:24 +0000

recovery: classify cross-boundary receipt states

- Model exact frozen native and Rust transfer associations
- Distinguish lookup, pause, completion, settlement and isolated repair
- Test commit windows, repeated evidence and identity mismatches
- Verify standalone Rust and native consumers with unchanged public APIs

Diffstat:
MTeraFFI/provenance.json | 54+++++++++++++++++++++++++++---------------------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 26+++++++++++++++++++-------
MTeraFFI/source/aarch64-apple-ios-sim.json | 26+++++++++++++++++++-------
MTeraFFI/source/aarch64-apple-ios.json | 26+++++++++++++++++++-------
Mcore/crates/tera_core/src/runtime/product_surface.rs | 2++
Acore/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrelease/provenance.json | 4++--
Mtest-fixtures/legacy-identifiers.v1.json | 16++++++++++++++++
10 files changed, 496 insertions(+), 52 deletions(-)

diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76774144, + "bytes": 76698520, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "c99b2a456361cff0c24d4af802452aea3c7200cf26e930d76972ed3c8c4d3b18" + "sha256": "52728a4e4c61f7d3811e1a358aa71908d3e925450f8ec9079221b22d9d6b03cb" }, { "bytes": 83913, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76853480, + "bytes": 76777928, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "11bf5251738f4f16035417f53648b3ad5ccbdce3d7b59b236e0b6182d126f0df" + "sha256": "f9125fe01e221380b9084a2ad8336611880c901bd20c45207efb1f3d3a4035c0" }, { "bytes": 51571, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 22091840, + "bytes": 22051024, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "d497f0eb46f56965d1fac69aa3f7acc9b3131985c74e59c1ac654e9ec5690e82" + "sha256": "59c25f4639fca3424166b808a0d05bf0fb8cc7e722593b2b09acfafe7cb4729c" }, { - "bytes": 76774144, + "bytes": 76698520, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "c99b2a456361cff0c24d4af802452aea3c7200cf26e930d76972ed3c8c4d3b18" + "sha256": "52728a4e4c61f7d3811e1a358aa71908d3e925450f8ec9079221b22d9d6b03cb" }, { - "bytes": 76853480, + "bytes": 76777928, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "11bf5251738f4f16035417f53648b3ad5ccbdce3d7b59b236e0b6182d126f0df" + "sha256": "f9125fe01e221380b9084a2ad8336611880c901bd20c45207efb1f3d3a4035c0" }, { - "bytes": 106759, + "bytes": 107355, "path": "source/aarch64-apple-darwin.json", - "sha256": "ed5daaa64f4b240c1660f05f06cd1c79d4b9111cc00adb483dac58b3c43b7766" + "sha256": "633259fd62c85519cc0779ef722b7bfabe3aefa57d17a1c4d336bdcb44298844" }, { - "bytes": 106603, + "bytes": 107199, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "f9444a7c2ad81cc0c02439b66ac4c603590cee3d821663e364d519442e591f9d" + "sha256": "c569017d267490cb59f7aff25109dd85810ef848a4d4c748905deddd809b84b3" }, { - "bytes": 106599, + "bytes": 107195, "path": "source/aarch64-apple-ios.json", - "sha256": "78186683a1294f9bbb714a879e060186e3a1e9788c349cb89973d20c79e0ea0d" + "sha256": "93ff0284eeff92eb40fd42e7db205d979a3ba76c0d9f8e735bd96f0c3682ad08" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "207953b663237a6397cdd564c488e1491dbfde76" + "tree": "3e612b493a705f30a2e9ad8ae12a8e320b5f884c" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76774144, + "bytes": 76698520, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "c99b2a456361cff0c24d4af802452aea3c7200cf26e930d76972ed3c8c4d3b18" + "sha256": "52728a4e4c61f7d3811e1a358aa71908d3e925450f8ec9079221b22d9d6b03cb" }, { "bytes": 83913, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76853480, + "bytes": 76777928, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "11bf5251738f4f16035417f53648b3ad5ccbdce3d7b59b236e0b6182d126f0df" + "sha256": "f9125fe01e221380b9084a2ad8336611880c901bd20c45207efb1f3d3a4035c0" }, { "bytes": 663554, @@ -174,19 +174,19 @@ "sha256": "12c54595ac3ebcb6aa22c7e968b019f67e1bc0606782ef4b71c601df6a54e764" }, { - "bytes": 106759, + "bytes": 107355, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "ed5daaa64f4b240c1660f05f06cd1c79d4b9111cc00adb483dac58b3c43b7766" + "sha256": "633259fd62c85519cc0779ef722b7bfabe3aefa57d17a1c4d336bdcb44298844" }, { - "bytes": 106603, + "bytes": 107199, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "f9444a7c2ad81cc0c02439b66ac4c603590cee3d821663e364d519442e591f9d" + "sha256": "c569017d267490cb59f7aff25109dd85810ef848a4d4c748905deddd809b84b3" }, { - "bytes": 106599, + "bytes": 107195, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "78186683a1294f9bbb714a879e060186e3a1e9788c349cb89973d20c79e0ea0d" + "sha256": "93ff0284eeff92eb40fd42e7db205d979a3ba76c0d9f8e735bd96f0c3682ad08" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "207953b663237a6397cdd564c488e1491dbfde76" -manifest_sha256 = "6a1a360e7a13afc89008544a964c4d747bc563c6af28f19fb2ddeea301416d0c" +source_tree = "3e612b493a705f30a2e9ad8ae12a8e320b5f884c" +manifest_sha256 = "e581ed9a2a599bdeaae142f5786090be6a4c0073e9297bfc056268c5f7dd47fa" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -645,10 +645,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 8364, - "git_blob": "76c094ffe07eb95958c3559e13a6e8569ed51985", + "bytes": 8431, + "git_blob": "7fe0757ff6467b31fa087c25485e46b280b29ac6", "mode": "100644", - "sha256": "adcde8696fd8144f13213388f68f1c3d6cc19c31f08121f024281a99e9e05ec8" + "sha256": "67351a02a9d45f216212aa05e52ac0d5c36b74d7787731fb1bcfaecc69b1cec2" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -908,6 +908,18 @@ "mode": "100644", "sha256": "8e0c1fedaa2ae6ceab717b5d44918be49683ac23a75a6c931fbca15013a36322" }, + "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs": { + "bytes": 6212, + "git_blob": "63647bd304a3fd9dd51bc0930b7826eb96efe62e", + "mode": "100644", + "sha256": "655b9e8405a9a53c0e1657648a5d9d7a6a223a9d99c788c21afe5b6a8fc8242b" + }, + "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs": { + "bytes": 6878, + "git_blob": "6d0d08c446de5fc16598de6475020d270de77aa5", + "mode": "100644", + "sha256": "dab08b5c62c89ab3f178b859c6229aef16d4cf844f6990f5c087481b773dfa85" + }, "core/crates/tera_core/src/runtime/product_surface/settings.rs": { "bytes": 57049, "git_blob": "5ffe3e45ee9d48382f5453b066eaeb166b00b407", @@ -2271,13 +2283,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 151542, - "git_blob": "854f586ecad54a22665f64d44d4843b39c66a50c", + "bytes": 152102, + "git_blob": "9801e3a668e27cf4b8e934599ae2d89b2535a9e4", "mode": "100644", - "sha256": "66dec6eb2296cee4c29333a58645ab3686442c34577c36bd2b8344b0bf5c32f2" + "sha256": "b39f1ef08ee92d472f63327675b46de673f3386d77ce93c1afe23c2f1214ee47" } }, "policy": "staged_inputs", - "tree": "207953b663237a6397cdd564c488e1491dbfde76" + "tree": "3e612b493a705f30a2e9ad8ae12a8e320b5f884c" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -641,10 +641,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 8364, - "git_blob": "76c094ffe07eb95958c3559e13a6e8569ed51985", + "bytes": 8431, + "git_blob": "7fe0757ff6467b31fa087c25485e46b280b29ac6", "mode": "100644", - "sha256": "adcde8696fd8144f13213388f68f1c3d6cc19c31f08121f024281a99e9e05ec8" + "sha256": "67351a02a9d45f216212aa05e52ac0d5c36b74d7787731fb1bcfaecc69b1cec2" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -904,6 +904,18 @@ "mode": "100644", "sha256": "8e0c1fedaa2ae6ceab717b5d44918be49683ac23a75a6c931fbca15013a36322" }, + "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs": { + "bytes": 6212, + "git_blob": "63647bd304a3fd9dd51bc0930b7826eb96efe62e", + "mode": "100644", + "sha256": "655b9e8405a9a53c0e1657648a5d9d7a6a223a9d99c788c21afe5b6a8fc8242b" + }, + "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs": { + "bytes": 6878, + "git_blob": "6d0d08c446de5fc16598de6475020d270de77aa5", + "mode": "100644", + "sha256": "dab08b5c62c89ab3f178b859c6229aef16d4cf844f6990f5c087481b773dfa85" + }, "core/crates/tera_core/src/runtime/product_surface/settings.rs": { "bytes": 57049, "git_blob": "5ffe3e45ee9d48382f5453b066eaeb166b00b407", @@ -2267,13 +2279,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 151542, - "git_blob": "854f586ecad54a22665f64d44d4843b39c66a50c", + "bytes": 152102, + "git_blob": "9801e3a668e27cf4b8e934599ae2d89b2535a9e4", "mode": "100644", - "sha256": "66dec6eb2296cee4c29333a58645ab3686442c34577c36bd2b8344b0bf5c32f2" + "sha256": "b39f1ef08ee92d472f63327675b46de673f3386d77ce93c1afe23c2f1214ee47" } }, "policy": "staged_inputs", - "tree": "207953b663237a6397cdd564c488e1491dbfde76" + "tree": "3e612b493a705f30a2e9ad8ae12a8e320b5f884c" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -641,10 +641,10 @@ "sha256": "dff55e46521c26f5f0ece024453b55c15f132fa94f78e2cba3d141f123d4eca0" }, "core/crates/tera_core/src/runtime/product_surface.rs": { - "bytes": 8364, - "git_blob": "76c094ffe07eb95958c3559e13a6e8569ed51985", + "bytes": 8431, + "git_blob": "7fe0757ff6467b31fa087c25485e46b280b29ac6", "mode": "100644", - "sha256": "adcde8696fd8144f13213388f68f1c3d6cc19c31f08121f024281a99e9e05ec8" + "sha256": "67351a02a9d45f216212aa05e52ac0d5c36b74d7787731fb1bcfaecc69b1cec2" }, "core/crates/tera_core/src/runtime/product_surface/authoring.rs": { "bytes": 11282, @@ -904,6 +904,18 @@ "mode": "100644", "sha256": "8e0c1fedaa2ae6ceab717b5d44918be49683ac23a75a6c931fbca15013a36322" }, + "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs": { + "bytes": 6212, + "git_blob": "63647bd304a3fd9dd51bc0930b7826eb96efe62e", + "mode": "100644", + "sha256": "655b9e8405a9a53c0e1657648a5d9d7a6a223a9d99c788c21afe5b6a8fc8242b" + }, + "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs": { + "bytes": 6878, + "git_blob": "6d0d08c446de5fc16598de6475020d270de77aa5", + "mode": "100644", + "sha256": "dab08b5c62c89ab3f178b859c6229aef16d4cf844f6990f5c087481b773dfa85" + }, "core/crates/tera_core/src/runtime/product_surface/settings.rs": { "bytes": 57049, "git_blob": "5ffe3e45ee9d48382f5453b066eaeb166b00b407", @@ -2267,13 +2279,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 151542, - "git_blob": "854f586ecad54a22665f64d44d4843b39c66a50c", + "bytes": 152102, + "git_blob": "9801e3a668e27cf4b8e934599ae2d89b2535a9e4", "mode": "100644", - "sha256": "66dec6eb2296cee4c29333a58645ab3686442c34577c36bd2b8344b0bf5c32f2" + "sha256": "b39f1ef08ee92d472f63327675b46de673f3386d77ce93c1afe23c2f1214ee47" } }, "policy": "staged_inputs", - "tree": "207953b663237a6397cdd564c488e1491dbfde76" + "tree": "3e612b493a705f30a2e9ad8ae12a8e320b5f884c" } } diff --git a/core/crates/tera_core/src/runtime/product_surface.rs b/core/crates/tera_core/src/runtime/product_surface.rs @@ -25,6 +25,8 @@ mod ranking; #[cfg(feature = "mobile-social")] pub mod recovery_inventory; #[cfg(feature = "mobile-social")] +pub mod recovery_reconciliation; +#[cfg(feature = "mobile-social")] mod settings; #[cfg(feature = "mobile-social")] mod submission; diff --git a/core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs b/core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs @@ -0,0 +1,182 @@ +//! Pure decisions over one transfer's authoritative facts. An action is a +//! request to the existing owner, never evidence that an effect has happened. +//! Callers re-read facts and validate the stored response/owned bytes before +//! completion, and require durable Rust success before native settlement. + +use radroots_blossom::{BlobUrl, MediaType}; +use radroots_identity::PublicKey; +use radroots_signing::SigningOperationId; +use radroots_storage::authored_draft::AuthoredDraftId; + +use super::Phase1DraftError; + +// Match the native destination and individual header admission ceilings. +// Use byte bounds so retained canonical text has an explicit memory ceiling. +pub const RECOVERY_URL_MAX_BYTES: usize = 4096; +pub const RECOVERY_MEDIA_TYPE_MAX_BYTES: usize = 8192; + +/// Frozen association, not the current editable draft or current settings. +/// The canonical blob URL includes the exact hash, origin and path. The attempt +/// is the persisted signing operation, not a newly minted recovery identifier. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RecoveryAssociation { + author: PublicKey, + parent: AuthoredDraftId, + attempt: SigningOperationId, + canonical_url: BlobUrl, + media_type: MediaType, + byte_size: u64, +} + +impl RecoveryAssociation { + pub fn new( + author: PublicKey, + parent: AuthoredDraftId, + attempt: SigningOperationId, + canonical_url: BlobUrl, + media_type: MediaType, + byte_size: u64, + ) -> Result<Self, Phase1DraftError> { + if byte_size == 0 + || canonical_url.as_str().len() > RECOVERY_URL_MAX_BYTES + || media_type.as_str().len() > RECOVERY_MEDIA_TYPE_MAX_BYTES + { + return Err(Phase1DraftError::InvalidMedia); + } + Ok(Self { + author, + parent, + attempt, + canonical_url, + media_type, + byte_size, + }) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RustCompletion { + Pending, + /// Only a validated durable journal result establishes this fact. + Verified, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RecoveryParent { + /// Includes absence from a displayed page. Exact lookup has not run yet. + Unqueried, + ProtectedDataUnavailable, + StorageUnavailable, + /// Only an authoritative exact lookup may establish absence. + Missing, + InvalidRecord, + /// Historical records without an exact attempt must retain their evidence. + AssociationUnconfirmed, + Known { + association: Box<RecoveryAssociation>, + completion: RustCompletion, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum NativeRecoveryState { + Active, + /// The stored response is available for validation, not already trusted. + ReceiptAvailable, + Settled, + /// Requires authoritative native reconciliation, not a timeout or an + /// isolated failed callback while an OS task might still be running. + DefinitivelyInactive, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum NativeRecoveryEvidence { + Unknown, + /// Conflicting identity/body evidence is retained for isolated repair. + Conflicting, + Known { + association: Box<RecoveryAssociation>, + state: NativeRecoveryState, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RecoveryPause { + ProtectedData, + StorageUnavailable, + NativeActive, + RetryAuthorityRequired, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RecoveryRepair { + ParentMissing, + InvalidParent, + AssociationUnconfirmed, + ConflictingEvidence, + AssociationMismatch, + SettlementWithoutCompletion, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RecoveryDecision { + LookupParent, + QueryNative, + Pause(RecoveryPause), + Complete, + Settle, + Reconciled, + Quarantine(RecoveryRepair), +} + +/// Constant work and memory for one exact association. No clock, mutable head +/// revision, bearer token, upload, signing, storage write or native effect. +pub fn reconcile(parent: &RecoveryParent, native: &NativeRecoveryEvidence) -> RecoveryDecision { + use RecoveryDecision as D; + let (expected, completion) = match parent { + RecoveryParent::Unqueried => return D::LookupParent, + RecoveryParent::ProtectedDataUnavailable => { + return D::Pause(RecoveryPause::ProtectedData); + } + RecoveryParent::StorageUnavailable => { + return D::Pause(RecoveryPause::StorageUnavailable); + } + RecoveryParent::Missing => return D::Quarantine(RecoveryRepair::ParentMissing), + RecoveryParent::InvalidRecord => return D::Quarantine(RecoveryRepair::InvalidParent), + RecoveryParent::AssociationUnconfirmed => { + return D::Quarantine(RecoveryRepair::AssociationUnconfirmed); + } + RecoveryParent::Known { + association, + completion, + } => (association, completion), + }; + let (observed, state) = match native { + NativeRecoveryEvidence::Unknown => return D::QueryNative, + NativeRecoveryEvidence::Conflicting => { + return D::Quarantine(RecoveryRepair::ConflictingEvidence); + } + NativeRecoveryEvidence::Known { association, state } => (association, state), + }; + if expected != observed { + return D::Quarantine(RecoveryRepair::AssociationMismatch); + } + match (completion, state) { + (RustCompletion::Pending, NativeRecoveryState::ReceiptAvailable) => D::Complete, + (RustCompletion::Verified, NativeRecoveryState::ReceiptAvailable) => D::Settle, + (RustCompletion::Verified, NativeRecoveryState::Settled) => D::Reconciled, + (RustCompletion::Pending, NativeRecoveryState::Settled) => { + D::Quarantine(RecoveryRepair::SettlementWithoutCompletion) + } + (RustCompletion::Pending, NativeRecoveryState::Active) => { + D::Pause(RecoveryPause::NativeActive) + } + (RustCompletion::Pending, NativeRecoveryState::DefinitivelyInactive) => { + D::Pause(RecoveryPause::RetryAuthorityRequired) + } + (RustCompletion::Verified, _) => D::QueryNative, + } +} + +#[cfg(test)] +mod tests; diff --git a/core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs b/core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation/tests.rs @@ -0,0 +1,208 @@ +use super::*; + +fn association() -> RecoveryAssociation { + RecoveryAssociation::new( + PublicKey::from_hex("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798") + .unwrap(), + AuthoredDraftId::new([1; 16]).unwrap(), + SigningOperationId::new([2; 16]).unwrap(), + BlobUrl::parse(&format!("https://media.example/{}", "03".repeat(32))).unwrap(), + MediaType::parse("image/png").unwrap(), + 4096, + ) + .unwrap() +} + +fn parent(completion: RustCompletion) -> RecoveryParent { + RecoveryParent::Known { + association: Box::new(association()), + completion, + } +} + +fn native(state: NativeRecoveryState) -> NativeRecoveryEvidence { + NativeRecoveryEvidence::Known { + association: Box::new(association()), + state, + } +} + +#[test] +fn structurally_valid_text_is_bounded_before_retention_and_comparison() { + let prefix = format!("https://media.example/{}.", "03".repeat(32)); + let url = + |length| BlobUrl::parse(&format!("{prefix}{}", "a".repeat(length - prefix.len()))).unwrap(); + let media_type = |length| { + let prefix = "image/png; x="; + let value = + MediaType::parse(&format!("{prefix}{}", "a".repeat(length - prefix.len()))).unwrap(); + assert_eq!(value.as_str().len(), length); + value + }; + let create = |url, media_type| { + let a = association(); + RecoveryAssociation::new(a.author, a.parent, a.attempt, url, media_type, a.byte_size) + }; + assert!( + create( + url(RECOVERY_URL_MAX_BYTES), + media_type(RECOVERY_MEDIA_TYPE_MAX_BYTES) + ) + .is_ok() + ); + assert!( + create( + url(RECOVERY_URL_MAX_BYTES + 1), + media_type(RECOVERY_MEDIA_TYPE_MAX_BYTES) + ) + .is_err() + ); + assert!( + create( + url(RECOVERY_URL_MAX_BYTES), + media_type(RECOVERY_MEDIA_TYPE_MAX_BYTES + 1) + ) + .is_err() + ); +} + +#[test] +fn both_commit_windows_and_every_native_state_have_explicit_repeatable_actions() { + use NativeRecoveryState as N; + use RecoveryDecision as D; + use RustCompletion as R; + let cases = [ + (R::Pending, N::ReceiptAvailable, D::Complete), + (R::Verified, N::ReceiptAvailable, D::Settle), + (R::Verified, N::Settled, D::Reconciled), + ( + R::Pending, + N::Settled, + D::Quarantine(RecoveryRepair::SettlementWithoutCompletion), + ), + (R::Pending, N::Active, D::Pause(RecoveryPause::NativeActive)), + (R::Verified, N::Active, D::QueryNative), + ( + R::Pending, + N::DefinitivelyInactive, + D::Pause(RecoveryPause::RetryAuthorityRequired), + ), + (R::Verified, N::DefinitivelyInactive, D::QueryNative), + ]; + for (rust, state, expected) in cases { + let parent = parent(rust); + let native = native(state); + let original = (parent.clone(), native.clone()); + for _ in 0..3 { + assert_eq!(reconcile(&parent, &native), expected); + assert_eq!((&parent, &native), (&original.0, &original.1)); + } + } +} + +#[test] +fn page_absence_requires_exact_lookup_and_unavailable_storage_is_never_corruption() { + use RecoveryDecision as D; + let cases = [ + (RecoveryParent::Unqueried, D::LookupParent), + ( + RecoveryParent::Missing, + D::Quarantine(RecoveryRepair::ParentMissing), + ), + ( + RecoveryParent::InvalidRecord, + D::Quarantine(RecoveryRepair::InvalidParent), + ), + ( + RecoveryParent::AssociationUnconfirmed, + D::Quarantine(RecoveryRepair::AssociationUnconfirmed), + ), + ( + RecoveryParent::ProtectedDataUnavailable, + D::Pause(RecoveryPause::ProtectedData), + ), + ( + RecoveryParent::StorageUnavailable, + D::Pause(RecoveryPause::StorageUnavailable), + ), + ]; + for (parent, expected) in cases { + for native in [ + NativeRecoveryEvidence::Unknown, + NativeRecoveryEvidence::Conflicting, + native(NativeRecoveryState::ReceiptAvailable), + native(NativeRecoveryState::Settled), + ] { + assert_eq!(reconcile(&parent, &native), expected); + } + } + for completion in [RustCompletion::Pending, RustCompletion::Verified] { + assert_eq!( + reconcile(&parent(completion), &NativeRecoveryEvidence::Unknown), + D::QueryNative + ); + assert_eq!( + reconcile(&parent(completion), &NativeRecoveryEvidence::Conflicting), + D::Quarantine(RecoveryRepair::ConflictingEvidence) + ); + } +} + +#[test] +fn every_frozen_identity_component_must_match_before_complete_or_settle() { + let original = association(); + let mut mismatches = Vec::new(); + let mut value = original.clone(); + value.author = + PublicKey::from_hex("c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5") + .unwrap(); + mismatches.push(value); + let mut value = original.clone(); + value.parent = AuthoredDraftId::new([4; 16]).unwrap(); + mismatches.push(value); + let mut value = original.clone(); + value.attempt = SigningOperationId::new([5; 16]).unwrap(); + mismatches.push(value); + let mut value = original.clone(); + value.canonical_url = + BlobUrl::parse(&format!("https://media.example/{}", "06".repeat(32))).unwrap(); + mismatches.push(value); + let mut value = original.clone(); + value.canonical_url = + BlobUrl::parse(&format!("https://other.example/{}", "03".repeat(32))).unwrap(); + mismatches.push(value); + let mut value = original.clone(); + value.media_type = MediaType::parse("image/jpeg").unwrap(); + mismatches.push(value); + let mut value = original.clone(); + value.byte_size += 1; + mismatches.push(value); + for association in mismatches { + for completion in [RustCompletion::Pending, RustCompletion::Verified] { + for state in [ + NativeRecoveryState::ReceiptAvailable, + NativeRecoveryState::Settled, + ] { + let native = NativeRecoveryEvidence::Known { + association: Box::new(association.clone()), + state, + }; + assert_eq!( + reconcile(&parent(completion), &native), + RecoveryDecision::Quarantine(RecoveryRepair::AssociationMismatch) + ); + } + } + } + assert!( + RecoveryAssociation::new( + original.author, + original.parent, + original.attempt, + original.canonical_url, + original.media_type, + 0 + ) + .is_err() + ); +} diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "12c54595ac3ebcb6aa22c7e968b019f67e1bc0606782ef4b71c601df6a54e764", - "ffi_provenance_sha256": "6a1a360e7a13afc89008544a964c4d747bc563c6af28f19fb2ddeea301416d0c", + "ffi_provenance_sha256": "e581ed9a2a599bdeaae142f5786090be6a4c0073e9297bfc056268c5f7dd47fa", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "3670c97ee6ffc2c2abd3249885c89bd7c20ec43fedcb66442501a2f42d74fc2a", @@ -22,7 +22,7 @@ "lib_revision": "91006304a47ff335c6f36fbbcfcb68ecfc76f6c7", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "29d68f2242a7725bc38381520ec196803893bf689c8e6b848647e664d6262316", - "tera_ffi_source_tree": "207953b663237a6397cdd564c488e1491dbfde76", + "tera_ffi_source_tree": "3e612b493a705f30a2e9ad8ae12a8e320b5f884c", "xcode_package_lock_sha256": "c468f2b0469438387c519e1b2de424a118dfa9145ea07dcfd2bc5cc714ec8559" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -4489,6 +4489,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/submission/capture/media.rs", "count": 1 }, @@ -4849,6 +4853,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/settings.rs", "count": 1 }, @@ -5305,6 +5313,10 @@ "count": 6 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/submission/intent.rs", "count": 1 }, @@ -5419,6 +5431,10 @@ "count": 1 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/recovery_reconciliation.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/settings.rs", "count": 3 },