mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-13 05:49:57 +02:00
refactor(core): remove experimental saved device contract
This commit is contained in:
@@ -161,6 +161,9 @@ final class SavedDeviceCoreContractTests: XCTestCase {
|
||||
"generated bindings must not expose \(needle)"
|
||||
)
|
||||
}
|
||||
XCTAssertFalse(source.contains("initializeWithExperimentalSavedDevices"))
|
||||
XCTAssertFalse(source.contains("ExperimentalSavedDeviceCapabilities"))
|
||||
XCTAssertFalse(source.contains("experimentalSavedDeviceCapabilities"))
|
||||
XCTAssertTrue(source.contains("initializeWithLimitsAndNetworkConfig"))
|
||||
XCTAssertTrue(source.contains("public struct SavedDeviceCapabilities"))
|
||||
XCTAssertTrue(source.contains("public func savedDeviceCapabilities()"))
|
||||
|
||||
@@ -54,7 +54,7 @@ src/
|
||||
receive.rs # receive, download, export, OutputSinkFile
|
||||
lifecycle.rs # cancel share, delete, status, access mode, shutdown
|
||||
provider.rs # provider messages, per-peer transfer progress
|
||||
saved_devices.rs # experimental saved-device pairing, forget, block
|
||||
saved_devices.rs # saved-device pairing, forget, block
|
||||
targeted.rs # saved-device targeted transfers
|
||||
persistence.rs # AppDataStores / persistence open (domain stores)
|
||||
invitation/ # invitation-transfer domain store (type name: Repository)
|
||||
|
||||
@@ -42,7 +42,7 @@ bytes through Kotlin memory.
|
||||
`stopped`.
|
||||
- Main event phases: `endpoint`, `import`, `ticket`, `handshake`, `approval`,
|
||||
`access`, `transfer`, `download`, `export`, `delivery`, `lifecycle`, `error`,
|
||||
plus experimental `pairing` and `targeted_transfer` (see catalog below).
|
||||
`pairing`, and `targeted_transfer` (see catalog below).
|
||||
- Events are sent to `CoreEventSink` immediately and persisted through the event
|
||||
hub. `list_events` flushes queued persistence before reading SQLite.
|
||||
- `shutdown()` is idempotent and flushes events before stopping the router.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use anyhow::Context;
|
||||
use iroh::RelayUrl;
|
||||
use iroh_blobs::Hash;
|
||||
@@ -29,25 +27,6 @@ pub fn saved_device_capabilities() -> SavedDeviceCapabilities {
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(note = "use SavedDeviceCapabilities")]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
|
||||
pub struct ExperimentalSavedDeviceCapabilities {
|
||||
pub domain_contract_version: u16,
|
||||
pub relationship_protocol_version: u16,
|
||||
pub targeted_transfer_protocol_version: u16,
|
||||
}
|
||||
|
||||
#[deprecated(note = "use saved_device_capabilities")]
|
||||
#[uniffi::export]
|
||||
pub fn experimental_saved_device_capabilities() -> ExperimentalSavedDeviceCapabilities {
|
||||
let capabilities = saved_device_capabilities();
|
||||
ExperimentalSavedDeviceCapabilities {
|
||||
domain_contract_version: capabilities.domain_contract_version,
|
||||
relationship_protocol_version: capabilities.relationship_protocol_version,
|
||||
targeted_transfer_protocol_version: capabilities.targeted_transfer_protocol_version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Public view of a single-use pairing window after a completed transfer.
|
||||
///
|
||||
/// The eligibility capability itself never crosses this boundary.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Experimental saved-device mutual-consent relationships.
|
||||
//! Saved-device mutual-consent relationships.
|
||||
//!
|
||||
//! Pending outgoing/incoming states, directional grants bound to relationship
|
||||
//! generation, and Saved only after mutual acknowledgement.
|
||||
|
||||
@@ -14,8 +14,6 @@ mod logging;
|
||||
mod pairing_eligibility;
|
||||
mod persistence;
|
||||
mod runtime;
|
||||
#[cfg(test)]
|
||||
mod secret;
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "the private custody seam is activated by platform credential adapters"
|
||||
@@ -26,15 +24,13 @@ mod ticket;
|
||||
mod transfer_state;
|
||||
mod util;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub use api::{
|
||||
clear_inactive_transfer_cache, default_core_limits, default_core_network_config,
|
||||
experimental_saved_device_capabilities, saved_device_capabilities, CoreEvent, CoreEventSink,
|
||||
CoreLimits, CoreNetworkConfig, CoreRelayMode, CoreStorageUsage, DeviceRelationship,
|
||||
DeviceRelationshipState, ExperimentalSavedDeviceCapabilities, PairingEligibilitySummary,
|
||||
PendingTargetedOffer, PublishedOutput, ReceiveOutputSink, ReceiveOutputSinkV2,
|
||||
ReceivedArtifact, ReceivedLocatorKind, ReceiverRequest, RuntimeStatus, SavedDevice,
|
||||
SavedDeviceCapabilities, ShareMetadataInput, ShareResult, ShareSource, SourceKind,
|
||||
saved_device_capabilities, CoreEvent, CoreEventSink, CoreLimits, CoreNetworkConfig,
|
||||
CoreRelayMode, CoreStorageUsage, DeviceRelationship, DeviceRelationshipState,
|
||||
PairingEligibilitySummary, PendingTargetedOffer, PublishedOutput, ReceiveOutputSink,
|
||||
ReceiveOutputSinkV2, ReceivedArtifact, ReceivedLocatorKind, ReceiverRequest, RuntimeStatus,
|
||||
SavedDevice, SavedDeviceCapabilities, ShareMetadataInput, ShareResult, ShareSource, SourceKind,
|
||||
StoredTransfer, TargetedOfferResponse, TargetedTransfer, TargetedTransferState,
|
||||
TicketInspection, TransferAccessMode, TransferMetadata,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{future::Future, path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -317,7 +315,6 @@ impl VnidropCore {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[uniffi::export]
|
||||
impl VnidropCore {
|
||||
#[uniffi::constructor]
|
||||
@@ -371,18 +368,6 @@ impl VnidropCore {
|
||||
Self::initialize_protected(app_data_dir, event_sink, limits, network_config)
|
||||
}
|
||||
|
||||
/// Compatibility constructor retained during the protected-initialization expand phase.
|
||||
#[deprecated(note = "use initialize_with_limits_and_network_config")]
|
||||
#[uniffi::constructor]
|
||||
pub fn initialize_with_experimental_saved_devices(
|
||||
app_data_dir: String,
|
||||
event_sink: Arc<dyn CoreEventSink>,
|
||||
limits: CoreLimits,
|
||||
network_config: CoreNetworkConfig,
|
||||
) -> Result<Arc<Self>, VnidropError> {
|
||||
Self::initialize_protected(app_data_dir, event_sink, limits, network_config)
|
||||
}
|
||||
|
||||
pub fn status(&self) -> RuntimeStatus {
|
||||
self.block_on(self.inner.status())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! - [`receive`] — ticket receive, download, export
|
||||
//! - [`lifecycle`] — cancel/delete/shutdown/status/access
|
||||
//! - [`provider`] — blob provider events and per-connection send progress
|
||||
//! - [`saved_devices`] — experimental saved-device pairing, forget, block
|
||||
//! - [`saved_devices`] — saved-device pairing, forget, block
|
||||
//! - [`targeted`] — saved-device targeted transfers
|
||||
|
||||
mod delivery;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Runtime operations for experimental saved devices and device relationships.
|
||||
//! Runtime operations for saved devices and device relationships.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use data_encoding::HEXLOWER;
|
||||
use iroh::SecretKey;
|
||||
|
||||
pub(crate) async fn load_or_create_secret(app_data_dir: &Path) -> Result<SecretKey> {
|
||||
if let Ok(secret) = std::env::var("IROH_SECRET") {
|
||||
return SecretKey::from_str(&secret).context("invalid IROH_SECRET");
|
||||
}
|
||||
|
||||
let path = app_data_dir.join("iroh.secret");
|
||||
match tokio::fs::read_to_string(&path).await {
|
||||
Ok(secret) => {
|
||||
let bytes = HEXLOWER
|
||||
.decode(secret.trim().as_bytes())
|
||||
.context("invalid persisted iroh secret encoding")?;
|
||||
let bytes: [u8; 32] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| anyhow::anyhow!("invalid persisted iroh secret length"))?;
|
||||
restrict_permissions(&path).await?;
|
||||
Ok(SecretKey::from_bytes(&bytes))
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {
|
||||
let secret = SecretKey::generate();
|
||||
let encoded = HEXLOWER.encode(&secret.to_bytes());
|
||||
// Create with owner-only mode on Unix so the key is never briefly 0644.
|
||||
write_secret_file(&path, encoded.as_bytes()).await?;
|
||||
restrict_permissions(&path).await?;
|
||||
Ok(secret)
|
||||
}
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn write_secret_file(path: &Path, bytes: &[u8]) -> Result<()> {
|
||||
let path = path.to_path_buf();
|
||||
let bytes = bytes.to_vec();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut options = OpenOptions::new();
|
||||
options.write(true).create_new(true);
|
||||
#[cfg(unix)]
|
||||
options.mode(0o600);
|
||||
let mut file = options
|
||||
.open(&path)
|
||||
.with_context(|| format!("failed to create {}", path.display()))?;
|
||||
file.write_all(&bytes)
|
||||
.with_context(|| format!("failed to write {}", path.display()))?;
|
||||
file.sync_all()
|
||||
.with_context(|| format!("failed to sync {}", path.display()))?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn restrict_permissions(path: &Path) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).await?;
|
||||
// Windows: file lives under the user profile app-data dir with default ACLs
|
||||
// limited to the current user. No portable owner-only API in std.
|
||||
let _ = path;
|
||||
Ok(())
|
||||
}
|
||||
@@ -37,8 +37,6 @@ mod platform_contract_windows_tests;
|
||||
mod repository_tests;
|
||||
#[path = "tests/runtime.rs"]
|
||||
mod runtime_tests;
|
||||
#[path = "tests/secret.rs"]
|
||||
mod secret_tests;
|
||||
#[path = "tests/secure_secret_android.rs"]
|
||||
mod secure_secret_android_tests;
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
|
||||
@@ -10,6 +10,9 @@ fn public_api_exposes_saved_device_surface_without_prototype_contact_entry_point
|
||||
let lib = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib.rs"));
|
||||
|
||||
for forbidden in [
|
||||
"initialize_with_experimental_saved_devices",
|
||||
"experimental_saved_device_capabilities",
|
||||
"ExperimentalSavedDeviceCapabilities",
|
||||
"fn list_contacts(",
|
||||
"fn send_to_contact(",
|
||||
"fn poll_contacts_for_offers(",
|
||||
@@ -58,7 +61,6 @@ fn public_api_exposes_saved_device_surface_without_prototype_contact_entry_point
|
||||
"fn forget_saved_device(",
|
||||
"fn share_files(",
|
||||
"fn receive(",
|
||||
"experimental_saved_device_capabilities",
|
||||
"saved_device_capabilities",
|
||||
] {
|
||||
assert!(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Android platform contract for the experimental saved-device foundation.
|
||||
//! Android platform contract for the saved-device foundation.
|
||||
//!
|
||||
//! These tests drive the public UniFFI surface through an
|
||||
//! [`AndroidSecureSecretStore`] backed by an in-process Keystore fake so the
|
||||
@@ -929,6 +929,9 @@ fn android_public_surface_omits_raw_secrets_and_generic_mutation() {
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
assert!(!kotlin.contains("initializeWithExperimentalSavedDevices"));
|
||||
assert!(!kotlin.contains("ExperimentalSavedDeviceCapabilities"));
|
||||
assert!(!kotlin.contains("experimentalSavedDeviceCapabilities"));
|
||||
assert!(
|
||||
kotlin.contains("initializeWithLimitsAndNetworkConfig"),
|
||||
"production protected Android init must remain on the public binding surface"
|
||||
|
||||
@@ -53,7 +53,7 @@ impl RecordingSink {
|
||||
/// `AppleKeychainSecretStore` + profile scoping. CLI unit tests lack the app
|
||||
/// Keychain entitlement, so the system Keychain returns Unavailable; the
|
||||
/// injectable API exercises the identical adapter path. Swift XCTest covers
|
||||
/// the real experimental constructor under the app entitlements.
|
||||
/// the standard protected constructor under the app entitlements.
|
||||
struct KeychainNode {
|
||||
data_dir: tempfile::TempDir,
|
||||
api: RecordingKeychain,
|
||||
@@ -208,7 +208,7 @@ impl Drop for KeychainNode {
|
||||
}
|
||||
}
|
||||
|
||||
fn try_experimental_keychain_init(app_data_dir: &Path) -> Result<Arc<VnidropCore>, VnidropError> {
|
||||
fn try_keychain_init(app_data_dir: &Path) -> Result<Arc<VnidropCore>, VnidropError> {
|
||||
VnidropCore::initialize_with_limits_and_network_config(
|
||||
app_data_dir.to_string_lossy().into_owned(),
|
||||
Arc::new(RecordingSink {
|
||||
@@ -593,7 +593,7 @@ fn recover_authoritative_state(
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn experimental_keychain_identity_survives_core_restart() {
|
||||
fn keychain_identity_survives_core_restart() {
|
||||
let node = KeychainNode::new();
|
||||
let endpoint_id = node.core().status().endpoint_id.clone();
|
||||
assert!(!endpoint_id.is_empty());
|
||||
@@ -610,18 +610,18 @@ fn experimental_keychain_identity_survives_core_restart() {
|
||||
// constructor must also preserve identity. Headless cargo often lacks that
|
||||
// entitlement and maps it to Unavailable — that path is covered by Swift.
|
||||
let live = tempfile::tempdir().unwrap();
|
||||
match try_experimental_keychain_init(live.path()) {
|
||||
match try_keychain_init(live.path()) {
|
||||
Ok(core) => {
|
||||
let id = core.status().endpoint_id.clone();
|
||||
core.shutdown();
|
||||
drop(core);
|
||||
let restarted = try_experimental_keychain_init(live.path()).expect("restart");
|
||||
let restarted = try_keychain_init(live.path()).expect("restart");
|
||||
assert_eq!(restarted.status().endpoint_id, id);
|
||||
restarted.shutdown();
|
||||
cleanup_scoped_keychain(live.path());
|
||||
}
|
||||
Err(VnidropError::SecureStorageUnavailable { .. }) => {}
|
||||
Err(error) => panic!("unexpected experimental init failure: {error:?}"),
|
||||
Err(error) => panic!("unexpected protected init failure: {error:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,6 +840,9 @@ fn apple_public_bindings_omit_raw_secrets_and_generic_mutation() {
|
||||
swift.display()
|
||||
);
|
||||
}
|
||||
assert!(!source.contains("initializeWithExperimentalSavedDevices"));
|
||||
assert!(!source.contains("ExperimentalSavedDeviceCapabilities"));
|
||||
assert!(!source.contains("experimentalSavedDeviceCapabilities"));
|
||||
assert!(
|
||||
source.contains("initializeWithLimitsAndNetworkConfig"),
|
||||
"Swift bindings must expose standard protected initialization"
|
||||
@@ -865,6 +868,8 @@ fn apple_public_bindings_omit_raw_secrets_and_generic_mutation() {
|
||||
)
|
||||
.expect("api.rs");
|
||||
for forbidden in [
|
||||
"ExperimentalSavedDeviceCapabilities",
|
||||
"experimental_saved_device_capabilities",
|
||||
"SecretMaterial",
|
||||
"SecretHandle",
|
||||
"SecureSecretStore",
|
||||
|
||||
@@ -546,7 +546,7 @@ fn secret_service_identity_survives_core_restart() {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn experimental_secret_service_identity_survives_core_restart_on_linux() {
|
||||
fn secret_service_identity_survives_core_restart_on_linux() {
|
||||
// Regression: protected init used to call blocking Secret Service on the
|
||||
// Tokio worker that drives `CoreInner::start`, which nested `block_on` and
|
||||
// aborted desktop startup with "Cannot start a runtime from within a runtime".
|
||||
@@ -560,7 +560,7 @@ fn experimental_secret_service_identity_survives_core_restart_on_linux() {
|
||||
CoreLimits::default(),
|
||||
CoreNetworkConfig::default(),
|
||||
)
|
||||
.expect("experimental Linux Secret Service core");
|
||||
.expect("Linux Secret Service core");
|
||||
let endpoint_id = core.status().endpoint_id.clone();
|
||||
assert!(!endpoint_id.is_empty());
|
||||
assert!(!data_dir.path().join("iroh.secret").exists());
|
||||
@@ -585,7 +585,7 @@ fn experimental_secret_service_identity_survives_core_restart_on_linux() {
|
||||
{
|
||||
std::thread::sleep(Duration::from_millis(25));
|
||||
}
|
||||
Err(error) => panic!("restart experimental Linux core: {error:?}"),
|
||||
Err(error) => panic!("restart Linux core: {error:?}"),
|
||||
}
|
||||
};
|
||||
assert_eq!(restarted.status().endpoint_id, endpoint_id);
|
||||
@@ -842,7 +842,7 @@ fn linux_public_bindings_omit_raw_secrets_and_generic_mutation() {
|
||||
}
|
||||
assert!(
|
||||
public_facade.contains("initialize_with_limits_and_network_config"),
|
||||
"facade must expose experimental saved-device init"
|
||||
"facade must expose standard protected initialization"
|
||||
);
|
||||
assert!(
|
||||
!public_facade.contains("fn set_state(") && !public_facade.contains("fn mutate_state("),
|
||||
|
||||
@@ -315,7 +315,7 @@ fn windows_dpapi_identity_survives_core_restart() {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn real_windows_dpapi_experimental_init_preserves_identity() {
|
||||
fn real_windows_dpapi_init_preserves_identity() {
|
||||
let data_dir = tempfile::tempdir().unwrap();
|
||||
let path = data_dir.path().to_string_lossy().into_owned();
|
||||
let sink = Arc::new(RecordingSink {
|
||||
@@ -327,7 +327,7 @@ fn real_windows_dpapi_experimental_init_preserves_identity() {
|
||||
CoreLimits::default(),
|
||||
CoreNetworkConfig::default(),
|
||||
)
|
||||
.expect("experimental windows core");
|
||||
.expect("Windows core");
|
||||
let first = core.status().endpoint_id.clone();
|
||||
core.shutdown();
|
||||
drop(core);
|
||||
@@ -341,7 +341,7 @@ fn real_windows_dpapi_experimental_init_preserves_identity() {
|
||||
CoreLimits::default(),
|
||||
CoreNetworkConfig::default(),
|
||||
)
|
||||
.expect("restarted experimental windows core");
|
||||
.expect("restarted Windows core");
|
||||
assert_eq!(restarted.status().endpoint_id, first);
|
||||
restarted.shutdown();
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use crate::secret::load_or_create_secret;
|
||||
|
||||
#[tokio::test]
|
||||
async fn persists_with_restricted_permissions() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let first = load_or_create_secret(temp.path()).await.unwrap();
|
||||
let second = load_or_create_secret(temp.path()).await.unwrap();
|
||||
assert_eq!(first.to_bytes(), second.to_bytes());
|
||||
|
||||
#[cfg(unix)]
|
||||
assert_eq!(
|
||||
std::fs::metadata(temp.path().join("iroh.secret"))
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o777,
|
||||
0o600
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use vnidrop::{
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn saved_device_protocols_are_explicitly_experimental_and_versioned() {
|
||||
fn saved_device_protocols_are_explicitly_versioned() {
|
||||
assert_eq!(
|
||||
saved_device_capabilities(),
|
||||
SavedDeviceCapabilities {
|
||||
@@ -97,7 +97,7 @@ fn targeted_transfer_transitions_are_validated_by_the_domain() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn experimental_domain_seam_does_not_change_multi_receiver_shares() {
|
||||
fn saved_device_domain_seam_does_not_change_multi_receiver_shares() {
|
||||
let source_dir = tempfile::tempdir().unwrap();
|
||||
let first_output = tempfile::tempdir().unwrap();
|
||||
let second_output = tempfile::tempdir().unwrap();
|
||||
@@ -32,6 +32,9 @@ class SavedDeviceCoreContractBindingHygieneTest {
|
||||
"generated binding ${binding.path} must not expose `$token`",
|
||||
)
|
||||
}
|
||||
assertFalse(source.contains("initializeWithExperimentalSavedDevices"))
|
||||
assertFalse(source.contains("ExperimentalSavedDeviceCapabilities"))
|
||||
assertFalse(source.contains("experimentalSavedDeviceCapabilities"))
|
||||
|
||||
assertTrue(
|
||||
source.contains("initializeWithLimitsAndNetworkConfig"),
|
||||
|
||||
@@ -32,6 +32,9 @@ class SavedDeviceCoreContractBindingHygieneTest {
|
||||
"generated binding ${binding.path} must not expose `$token`",
|
||||
)
|
||||
}
|
||||
assertFalse(source.contains("initializeWithExperimentalSavedDevices"))
|
||||
assertFalse(source.contains("ExperimentalSavedDeviceCapabilities"))
|
||||
assertFalse(source.contains("experimentalSavedDeviceCapabilities"))
|
||||
|
||||
assertTrue(
|
||||
source.contains("initializeWithLimitsAndNetworkConfig"),
|
||||
|
||||
Reference in New Issue
Block a user