app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit b19555ab50f80899c51bd3ed9ca5bdc2b5d8227d
parent 4eb02491b931713ba07784cb51e239006396ed4f
Author: triesap <tyson@radroots.org>
Date:   Wed, 26 Aug 2026 03:19:37 +0000

feat(storage): add governed backup restore

- wrap Lib capture and verification with sealed Harvest capabilities
- stage and finalize identity-bound restores through the recovery protocol
- preserve legacy state and reopen verified restored state
- freeze the public boundary and round-trip qualification

Diffstat:
MAGENTS.md | 4++++
MREADME.md | 7+++++++
Mcore/compatibility/harvestcircle-storage-api-v1.txt | 7+++++++
Acore/crates/harvestcircle_storage/src/backup.rs | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/harvestcircle_storage/src/db.rs | 50+++++++++++++++++++++++++++++---------------------
Mcore/crates/harvestcircle_storage/src/lib.rs | 2++
Acore/crates/harvestcircle_storage/tests/backup_restore.rs | 275+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/harvestcircle_storage/tests/package_boundary.rs | 10++++++++++
Mtools/xtask/src/lib.rs | 9+++++++++
9 files changed, 491 insertions(+), 21 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -99,6 +99,10 @@ substitute. - Recovery and backup operations must be explicit, user-initiated, bounded, fail closed, and keep sensitive material out of logs, crash text, analytics, filenames, and long-lived UI state. +- Backup and restore must use the sealed HarvestCircle wrappers over Lib's + capture, verify, stage, finalize, and marker-recovery protocol. Verification + requires a trusted manifest digest, current database identity, and positive + caller-supplied member limit; never accept an arbitrary replacement path. - Clipboard writes of sensitive output require explicit user action, bounded lifetime, ownership-aware clearing, and tests for cancellation and replacement. Never clear unrelated clipboard content. diff --git a/README.md b/README.md @@ -55,6 +55,13 @@ close, backup, and restore mechanics. The historical `harvestcircle.sqlite3` file is legacy evidence only and is never imported, repaired, deleted, or treated as current state. +Online backup capture returns the canonical manifest in memory and writes only +the governed `state.sqlite` member into a caller-selected new directory. +Restore accepts only a digest-bound, identity-bound, size-bounded verified +backup capability, closes the live host, uses the governed marker protocol, +and reopens recovered state before returning. There is no arbitrary database +repair or pathname-only restore authority. + ## Project documentation The consuming Radroots monorepo owns normative HarvestCircle specifications, diff --git a/core/compatibility/harvestcircle-storage-api-v1.txt b/core/compatibility/harvestcircle-storage-api-v1.txt @@ -9,6 +9,9 @@ impl core::fmt::Display for harvestcircle_storage::HarvestCircleStorageContractE pub fn harvestcircle_storage::HarvestCircleStorageContractError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct harvestcircle_storage::Database impl harvestcircle_storage::Database +pub async fn harvestcircle_storage::Database::capture_online_backup(&self, &std::path::Path, radroots_service_sqlite::backup::manifest::BackupCreatedAtUnixMs) -> core::result::Result<radroots_service_sqlite::backup::manifest::ServiceBackupManifest, harvestcircle_domain::error::SafeError> +pub async fn harvestcircle_storage::Database::restore_verified_backup(&mut self, &radroots_runtime_paths::context::RuntimeContext, harvestcircle_storage::VerifiedHarvestCircleBackup, u64, &radroots_service_sqlite::migration::MigrationBuildIdentity) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +impl harvestcircle_storage::Database pub async fn harvestcircle_storage::Database::close(&self) -> core::result::Result<(), harvestcircle_domain::error::SafeError> pub const fn harvestcircle_storage::Database::metadata(&self) -> &radroots_service_sqlite::metadata::ServiceDatabaseMetadata pub async fn harvestcircle_storage::Database::open(&radroots_runtime_paths::context::RuntimeContext, u64, u64, &radroots_service_sqlite::migration::MigrationBuildIdentity) -> core::result::Result<Self, harvestcircle_domain::error::SafeError> @@ -55,6 +58,9 @@ pub fn harvestcircle_storage::OsKeyringSecretStore::contains(&self, harvestcircl pub fn harvestcircle_storage::OsKeyringSecretStore::delete(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<(), harvestcircle_domain::error::SafeError> pub fn harvestcircle_storage::OsKeyringSecretStore::load(&self, harvestcircle_domain::key::PublicKey) -> core::result::Result<harvestcircle_domain::key::SecretKeyInput, harvestcircle_domain::error::SafeError> pub fn harvestcircle_storage::OsKeyringSecretStore::put(&self, harvestcircle_domain::key::PublicKey, harvestcircle_domain::key::SecretKeyInput) -> core::result::Result<(), harvestcircle_domain::error::SafeError> +pub struct harvestcircle_storage::VerifiedHarvestCircleBackup +impl core::fmt::Debug for harvestcircle_storage::VerifiedHarvestCircleBackup +pub fn harvestcircle_storage::VerifiedHarvestCircleBackup::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub const harvestcircle_storage::CREDENTIAL_SERVICE: &str pub const harvestcircle_storage::CURRENT_SCHEMA_VERSION: u32 pub const harvestcircle_storage::HARVESTCIRCLE_ACTOR_MAILBOX_CAPACITY: usize @@ -74,3 +80,4 @@ pub const harvestcircle_storage::HARVESTCIRCLE_STATE_SCHEMA_VERSION: u32 pub const harvestcircle_storage::HARVESTCIRCLE_UNFINISHED_DURABLE_OPERATION_CAPACITY: usize pub fn harvestcircle_storage::harvestcircle_migration_catalog() -> core::result::Result<radroots_service_sqlite::migration::MigrationCatalog, harvestcircle_storage::HarvestCircleStorageContractError> pub fn harvestcircle_storage::harvestcircle_schema_catalog() -> core::result::Result<radroots_service_sqlite::integrity::catalog::SchemaCatalog, harvestcircle_storage::HarvestCircleStorageContractError> +pub fn harvestcircle_storage::verify_harvestcircle_backup(&[u8], radroots_service_sqlite::backup::manifest::BackupManifestSha256, &std::path::Path, &radroots_service_sqlite::metadata::ServiceDatabaseIdentity, core::num::nonzero::NonZeroU64) -> core::result::Result<harvestcircle_storage::VerifiedHarvestCircleBackup, harvestcircle_domain::error::SafeError> diff --git a/core/crates/harvestcircle_storage/src/backup.rs b/core/crates/harvestcircle_storage/src/backup.rs @@ -0,0 +1,148 @@ +use core::{fmt, num::NonZeroU64}; +use std::path::Path; + +use radroots_runtime_paths::RuntimeContext; +use radroots_service_sqlite::{ + BackupCreatedAtUnixMs, BackupManifestSha256, MigrationAppliedAtUnixSeconds, + MigrationBuildIdentity, ServiceBackupManifest, ServiceDatabaseIdentity, VerifiedServiceBackup, + finalize_staged_restore, stage_verified_restore, verify_backup_bundle, +}; + +use crate::db::{invalid_storage_contract, map_service_error}; +use crate::{Database, HarvestCircleStorageContract}; +use harvestcircle_domain::{SafeError, SafeErrorCode, SafeMessage}; + +/// Retained, non-forgeable proof of one identity-bound HarvestCircle backup. +/// +/// This type intentionally exposes no path, descriptor, or raw database handle. +/// It is single-use restore authority and cannot be cloned: +/// +/// ```compile_fail +/// use harvestcircle_storage::VerifiedHarvestCircleBackup; +/// +/// fn require_clone<T: Clone>() {} +/// require_clone::<VerifiedHarvestCircleBackup>(); +/// ``` +pub struct VerifiedHarvestCircleBackup { + inner: VerifiedServiceBackup, +} + +impl fmt::Debug for VerifiedHarvestCircleBackup { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("VerifiedHarvestCircleBackup([redacted])") + } +} + +/// Verifies one untrusted canonical backup bundle without mutating it. +/// +/// The expected digest, database identity, and positive member limit are +/// trusted inputs. This operation is synchronous and performs bounded file and +/// SQLite reads; callers own any supervised worker and deadline. +pub fn verify_harvestcircle_backup( + manifest_bytes: &[u8], + expected_manifest_digest: BackupManifestSha256, + bundle_directory: &Path, + expected_identity: &ServiceDatabaseIdentity, + maximum_state_bytes: NonZeroU64, +) -> Result<VerifiedHarvestCircleBackup, SafeError> { + verify_backup_bundle( + manifest_bytes, + expected_manifest_digest, + bundle_directory, + expected_identity, + maximum_state_bytes, + ) + .map(|inner| VerifiedHarvestCircleBackup { inner }) + .map_err(|_| invalid_backup()) +} + +impl Database { + /// Captures one point-in-time backup into a caller-selected new directory. + pub async fn capture_online_backup( + &self, + staging_directory: &Path, + created_at_unix_ms: BackupCreatedAtUnixMs, + ) -> Result<ServiceBackupManifest, SafeError> { + self.host() + .capture_online_backup(staging_directory, created_at_unix_ms) + .await + .map_err(map_service_error) + } + + /// Closes live state, installs one verified backup, and reopens recovered state. + /// + /// Exclusive mutable access prevents concurrent use of the pre-restore + /// host. Preflight failure leaves that host open and usable. Failure after + /// close may leave exact recovery evidence; a later ordinary + /// `Database::open` reconciles that evidence. + pub async fn restore_verified_backup( + &mut self, + context: &RuntimeContext, + verified: VerifiedHarvestCircleBackup, + applied_at_unix_s: u64, + build: &MigrationBuildIdentity, + ) -> Result<(), SafeError> { + let contract = HarvestCircleStorageContract::from_runtime_context(context) + .map_err(|_| invalid_storage_contract())?; + let expected = self.metadata().identity(); + let backup_metadata = verified.inner.database_metadata(); + if expected.service() != contract.paths().service() + || expected.instance() != contract.paths().instance() + || expected.supported_state_schema_version() != contract.state_schema_version() + || expected.application_id() != contract.application_id() + || backup_metadata.service() != expected.service() + || backup_metadata.instance() != expected.instance() + || backup_metadata.source_generation() != expected.source_generation() + || backup_metadata.application_id() != expected.application_id() + || backup_metadata.state_schema_version() > expected.supported_state_schema_version() + { + return Err(invalid_storage_contract()); + } + let applied_at = MigrationAppliedAtUnixSeconds::new(applied_at_unix_s) + .map_err(|_| invalid_storage_contract())?; + + self.close().await?; + let staged = stage_verified_restore( + contract.paths(), + &expected, + contract.migrations(), + contract.schema(), + verified.inner, + ) + .await + .map_err(map_restore_error)?; + finalize_staged_restore(staged) + .await + .map_err(map_restore_error)?; + let reopened = Self::open_existing(&contract, applied_at, build).await?; + *self = reopened; + Ok(()) + } +} + +const fn invalid_backup() -> SafeError { + SafeError::new( + SafeErrorCode::StorageBackupInvalid, + SafeMessage::new("The selected backup could not be verified."), + ) +} + +fn map_restore_error(error: radroots_service_sqlite::ServiceSqliteError) -> SafeError { + use radroots_service_sqlite::ServiceSqliteErrorKind; + + match error.kind() { + ServiceSqliteErrorKind::Metadata + | ServiceSqliteErrorKind::Migration + | ServiceSqliteErrorKind::Integrity + | ServiceSqliteErrorKind::Backup => invalid_backup(), + ServiceSqliteErrorKind::Recovery => SafeError::new( + SafeErrorCode::StorageQuarantined, + SafeMessage::new("The application state requires recovery."), + ), + ServiceSqliteErrorKind::Authority + | ServiceSqliteErrorKind::Open + | ServiceSqliteErrorKind::Create + | ServiceSqliteErrorKind::Pragma + | ServiceSqliteErrorKind::Restore => map_service_error(error), + } +} diff --git a/core/crates/harvestcircle_storage/src/db.rs b/core/crates/harvestcircle_storage/src/db.rs @@ -52,25 +52,7 @@ impl Database { .try_exists() .map_err(|_| storage_unavailable())? { - let intent = ExistingServiceDatabaseIntent::new( - contract.paths(), - contract.state_schema_version(), - contract.application_id(), - ); - let (opened, _) = ServiceSqliteHost::open_read_write_existing_with_intent( - contract.paths(), - &intent, - contract.migrations(), - contract.schema(), - options, - applied_at, - build, - &[], - ) - .await - .map_err(map_service_error)?; - let (host, metadata) = opened.into_parts(); - return Ok(Self { host, metadata }); + return Self::open_existing(&contract, applied_at, build).await; } let mut generation = [0_u8; 32]; @@ -122,6 +104,32 @@ impl Database { pub(crate) const fn host(&self) -> &ServiceSqliteHost { &self.host } + + pub(crate) async fn open_existing( + contract: &HarvestCircleStorageContract, + applied_at: MigrationAppliedAtUnixSeconds, + build: &MigrationBuildIdentity, + ) -> Result<Self, SafeError> { + let intent = ExistingServiceDatabaseIntent::new( + contract.paths(), + contract.state_schema_version(), + contract.application_id(), + ); + let (opened, _) = ServiceSqliteHost::open_read_write_existing_with_intent( + contract.paths(), + &intent, + contract.migrations(), + contract.schema(), + ServiceSqliteConnectionOptions::reviewed(), + applied_at, + build, + &[], + ) + .await + .map_err(map_service_error)?; + let (host, metadata) = opened.into_parts(); + Ok(Self { host, metadata }) + } } async fn initialize_application_schema(path: std::path::PathBuf) -> Result<(), sqlx::Error> { @@ -166,7 +174,7 @@ pub(crate) fn map_transaction_error(error: ServiceSqliteTransactionError<SafeErr } } -fn map_service_error(error: radroots_service_sqlite::ServiceSqliteError) -> SafeError { +pub(crate) fn map_service_error(error: radroots_service_sqlite::ServiceSqliteError) -> SafeError { match error.kind() { ServiceSqliteErrorKind::Metadata | ServiceSqliteErrorKind::Migration @@ -195,7 +203,7 @@ pub(crate) const fn corrupt_storage() -> SafeError { ) } -const fn invalid_storage_contract() -> SafeError { +pub(crate) const fn invalid_storage_contract() -> SafeError { SafeError::new( SafeErrorCode::InvalidApplicationState, SafeMessage::new("The application storage contract is invalid."), diff --git a/core/crates/harvestcircle_storage/src/lib.rs b/core/crates/harvestcircle_storage/src/lib.rs @@ -1,6 +1,7 @@ #![doc = "HarvestCircle persistence adapters."] #![cfg_attr(coverage_nightly, feature(coverage_attribute))] +mod backup; mod contract; mod db; mod identities; @@ -12,6 +13,7 @@ mod journal; mod os_keyring; mod profiles; +pub use backup::{VerifiedHarvestCircleBackup, verify_harvestcircle_backup}; pub use contract::{ HARVESTCIRCLE_ACTOR_MAILBOX_CAPACITY, HARVESTCIRCLE_APPLICATION_ID, HARVESTCIRCLE_COMMAND_DEADLINE_MAX_MS, HARVESTCIRCLE_COMMAND_DEADLINE_MIN_MS, diff --git a/core/crates/harvestcircle_storage/tests/backup_restore.rs b/core/crates/harvestcircle_storage/tests/backup_restore.rs @@ -0,0 +1,275 @@ +use core::num::NonZeroU64; +use std::fs; + +use harvestcircle_application::{IdentityRepository, KeyMaterialProvider}; +use harvestcircle_domain::{ + IdentityCreatedAt, LocalKeyringBinding, NostrIdentity, NostrIdentityReference, SafeErrorCode, + SignerAvailability, UnixTimestamp, +}; +use harvestcircle_nostr::NostrKeyMaterialProvider; +use harvestcircle_storage::{Database, verify_harvestcircle_backup}; +use radroots_runtime_paths::{ + InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, +}; +use radroots_service_sqlite::{ + BACKUP_STATE_MEMBER_NAME, BackupCreatedAtUnixMs, BackupManifestSha256, MigrationBuildIdentity, +}; +use tempfile::{TempDir, tempdir_in}; + +fn tempdir() -> std::io::Result<TempDir> { + tempdir_in(std::env::temp_dir().canonicalize()?) +} + +fn runtime_context(directory: &TempDir) -> RuntimeContext { + RuntimeContext::resolve( + &RadrootsPathResolver::new( + RadrootsPlatform::current(), + RadrootsHostEnvironment::default(), + ), + RuntimeContextBootstrap::new( + RadrootsPathProfile::RepoLocal, + Some( + directory + .path() + .canonicalize() + .expect("canonical directory"), + ), + RuntimeContextSource::BootstrapCli, + RuntimeContextSource::SafeDefault, + ) + .expect("bootstrap"), + ServiceId::new("harvestcircle").expect("service"), + InstanceId::new("desktop").expect("instance"), + ) + .expect("runtime context") +} + +fn build_identity() -> MigrationBuildIdentity { + MigrationBuildIdentity::new( + "0.1.0-alpha", + "1111111111111111111111111111111111111111", + "2222222222222222222222222222222222222222", + "1.97.1", + "test", + "test", + 1, + 1, + 1, + 1, + 1, + ) + .expect("build identity") +} + +fn identity(index: i64) -> NostrIdentity { + let (public_key, npub, secret, nsec) = NostrKeyMaterialProvider + .generate() + .expect("generated key material") + .into_parts(); + drop((secret, nsec)); + NostrIdentity::new( + NostrIdentityReference::verify(public_key, npub.as_str().to_owned()) + .expect("identity reference"), + LocalKeyringBinding::new(public_key, SignerAvailability::Available), + None, + IdentityCreatedAt::new(UnixTimestamp::from_seconds(index).expect("time")), + None, + ) + .expect("identity") +} + +fn prepare_backup_parent(directory: &TempDir) -> std::path::PathBuf { + let parent = directory.path().join("backup-output"); + fs::create_dir(&parent).expect("backup parent"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&parent, fs::Permissions::from_mode(0o700)) + .expect("backup parent mode"); + } + parent +} + +#[tokio::test] +async fn capture_verify_restore_round_trip_is_identity_bound_and_legacy_safe() { + let directory = tempdir().expect("directory"); + let context = runtime_context(&directory); + let build = build_identity(); + let legacy = directory.path().join("harvestcircle.sqlite3"); + fs::write(&legacy, b"legacy-state-remains-untouched").expect("legacy sentinel"); + + let mut database = Database::open(&context, 1, 1, &build) + .await + .expect("database"); + let retained = identity(1); + database + .insert_identity(&retained) + .await + .expect("retained identity"); + let expected = database.metadata().identity(); + + let backup_parent = prepare_backup_parent(&directory); + let bundle = backup_parent.join("bundle"); + let manifest = database + .capture_online_backup( + &bundle, + BackupCreatedAtUnixMs::new(1_700_000_000_000).expect("capture time"), + ) + .await + .expect("capture"); + assert_eq!( + fs::read_dir(&bundle) + .expect("bundle inventory") + .map(|entry| entry.expect("entry").file_name()) + .collect::<Vec<_>>(), + [std::ffi::OsString::from(BACKUP_STATE_MEMBER_NAME)] + ); + assert!(!manifest.protected_material_included()); + + database + .insert_identity(&identity(2)) + .await + .expect("post-capture identity"); + assert_eq!( + database + .list_identities() + .await + .expect("live identities") + .len(), + 2 + ); + + let member_length = manifest.members()[0].byte_length(); + let verified = verify_harvestcircle_backup( + manifest.canonical_bytes(), + manifest.digest(), + &bundle, + &expected, + NonZeroU64::new(member_length).expect("member length"), + ) + .expect("verified backup"); + assert_eq!( + format!("{verified:?}"), + "VerifiedHarvestCircleBackup([redacted])" + ); + + database + .restore_verified_backup(&context, verified, 2, &build) + .await + .expect("restore and recovery reopen"); + let identities = database + .list_identities() + .await + .expect("restored identities"); + assert_eq!(identities.len(), 1); + assert_eq!(identities[0].public_key(), retained.public_key()); + assert_eq!(database.metadata().identity(), expected); + + let state_directory = context.paths().state(); + for forbidden in [ + "state.restore-staged.sqlite", + "state.restore-backup.sqlite", + "state.restore-marker.v1", + "state.restore-marker.v1.next", + ] { + assert!( + !state_directory.join(forbidden).exists(), + "retained {forbidden}" + ); + } + assert_eq!( + fs::read(&legacy).expect("legacy sentinel"), + b"legacy-state-remains-untouched" + ); + database.close().await.expect("restored close"); +} + +#[tokio::test] +async fn wrong_manifest_digest_fails_before_restore_and_preserves_live_state() { + let directory = tempdir().expect("directory"); + let context = runtime_context(&directory); + let build = build_identity(); + let database = Database::open(&context, 1, 1, &build) + .await + .expect("database"); + let retained = identity(1); + database.insert_identity(&retained).await.expect("identity"); + let expected = database.metadata().identity(); + let bundle = prepare_backup_parent(&directory).join("bundle"); + let manifest = database + .capture_online_backup( + &bundle, + BackupCreatedAtUnixMs::new(1_700_000_000_001).expect("capture time"), + ) + .await + .expect("capture"); + + let error = verify_harvestcircle_backup( + manifest.canonical_bytes(), + BackupManifestSha256::from_bytes([0; 32]), + &bundle, + &expected, + NonZeroU64::new(manifest.members()[0].byte_length()).expect("member length"), + ) + .expect_err("wrong trusted digest"); + assert_eq!(error.code(), SafeErrorCode::StorageBackupInvalid); + assert_eq!( + database.list_identities().await.expect("live identities")[0].public_key(), + retained.public_key() + ); + database.close().await.expect("close"); +} + +#[tokio::test] +async fn mismatched_verified_identity_is_rejected_before_live_host_close() { + let live_directory = tempdir().expect("live directory"); + let source_directory = tempdir().expect("source directory"); + let live_context = runtime_context(&live_directory); + let source_context = runtime_context(&source_directory); + let build = build_identity(); + + let mut live = Database::open(&live_context, 1, 1, &build) + .await + .expect("live database"); + let retained = identity(1); + live.insert_identity(&retained) + .await + .expect("live identity"); + + let source = Database::open(&source_context, 1, 1, &build) + .await + .expect("source database"); + let source_identity = source.metadata().identity(); + let bundle = prepare_backup_parent(&source_directory).join("bundle"); + let manifest = source + .capture_online_backup( + &bundle, + BackupCreatedAtUnixMs::new(1_700_000_000_002).expect("capture time"), + ) + .await + .expect("capture"); + let verified = verify_harvestcircle_backup( + manifest.canonical_bytes(), + manifest.digest(), + &bundle, + &source_identity, + NonZeroU64::new(manifest.members()[0].byte_length()).expect("member length"), + ) + .expect("verified source backup"); + + let error = live + .restore_verified_backup(&live_context, verified, 2, &build) + .await + .expect_err("mismatched source generation"); + assert_eq!(error.code(), SafeErrorCode::InvalidApplicationState); + assert_eq!( + live.list_identities() + .await + .expect("live host remains open")[0] + .public_key(), + retained.public_key() + ); + live.close().await.expect("live close"); + source.close().await.expect("source close"); +} diff --git a/core/crates/harvestcircle_storage/tests/package_boundary.rs b/core/crates/harvestcircle_storage/tests/package_boundary.rs @@ -33,6 +33,7 @@ fn storage_package_keeps_one_sqlite_authority_and_a_sealed_public_surface() { } for module in [ + "backup", "contract", "db", "identities", @@ -54,6 +55,10 @@ fn storage_package_keeps_one_sqlite_authority_and_a_sealed_public_surface() { "pub struct harvestcircle_storage::Database", "pub async fn harvestcircle_storage::Database::open", "pub async fn harvestcircle_storage::Database::close", + "pub async fn harvestcircle_storage::Database::capture_online_backup", + "pub async fn harvestcircle_storage::Database::restore_verified_backup", + "pub struct harvestcircle_storage::VerifiedHarvestCircleBackup", + "pub fn harvestcircle_storage::verify_harvestcircle_backup", "impl harvestcircle_application::ports::DurableOperationRepository for harvestcircle_storage::Database", "harvestcircle_application::ports::BoxFuture", "pub fn harvestcircle_storage::harvestcircle_migration_catalog()", @@ -67,6 +72,11 @@ fn storage_package_keeps_one_sqlite_authority_and_a_sealed_public_surface() { "sqlx::", "OperationJournal", "harvestcircle_initial_schema_sql", + "VerifiedServiceBackup", + "StagedServiceRestore", + "verify_backup_bundle", + "stage_verified_restore", + "finalize_staged_restore", "repair", "preflight", ] { diff --git a/tools/xtask/src/lib.rs b/tools/xtask/src/lib.rs @@ -878,6 +878,10 @@ fn provenance_check(root: &Path, inventory: &Inventory, findings: &mut Vec<Strin "pub struct harvestcircle_storage::Database", "pub async fn harvestcircle_storage::Database::open", "pub async fn harvestcircle_storage::Database::close", + "pub async fn harvestcircle_storage::Database::capture_online_backup", + "pub async fn harvestcircle_storage::Database::restore_verified_backup", + "pub struct harvestcircle_storage::VerifiedHarvestCircleBackup", + "pub fn harvestcircle_storage::verify_harvestcircle_backup", "impl harvestcircle_application::ports::DurableOperationRepository for harvestcircle_storage::Database", "harvestcircle_application::ports::BoxFuture", ] { @@ -891,6 +895,11 @@ fn provenance_check(root: &Path, inventory: &Inventory, findings: &mut Vec<Strin "sqlx::", "OperationJournal", "harvestcircle_initial_schema_sql", + "VerifiedServiceBackup", + "StagedServiceRestore", + "verify_backup_bundle", + "stage_verified_restore", + "finalize_staged_restore", "repair", "preflight", ] {