refactor(core): remove experimental saved device contract

This commit is contained in:
2026-08-12 17:00:28 +02:00
parent 5b57917f52
commit 2c941dc623
20 changed files with 46 additions and 162 deletions

View File

@@ -161,6 +161,9 @@ final class SavedDeviceCoreContractTests: XCTestCase {
"generated bindings must not expose \(needle)" "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("initializeWithLimitsAndNetworkConfig"))
XCTAssertTrue(source.contains("public struct SavedDeviceCapabilities")) XCTAssertTrue(source.contains("public struct SavedDeviceCapabilities"))
XCTAssertTrue(source.contains("public func savedDeviceCapabilities()")) XCTAssertTrue(source.contains("public func savedDeviceCapabilities()"))

View File

@@ -54,7 +54,7 @@ src/
receive.rs # receive, download, export, OutputSinkFile receive.rs # receive, download, export, OutputSinkFile
lifecycle.rs # cancel share, delete, status, access mode, shutdown lifecycle.rs # cancel share, delete, status, access mode, shutdown
provider.rs # provider messages, per-peer transfer progress 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 targeted.rs # saved-device targeted transfers
persistence.rs # AppDataStores / persistence open (domain stores) persistence.rs # AppDataStores / persistence open (domain stores)
invitation/ # invitation-transfer domain store (type name: Repository) invitation/ # invitation-transfer domain store (type name: Repository)

View File

@@ -42,7 +42,7 @@ bytes through Kotlin memory.
`stopped`. `stopped`.
- Main event phases: `endpoint`, `import`, `ticket`, `handshake`, `approval`, - Main event phases: `endpoint`, `import`, `ticket`, `handshake`, `approval`,
`access`, `transfer`, `download`, `export`, `delivery`, `lifecycle`, `error`, `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 - Events are sent to `CoreEventSink` immediately and persisted through the event
hub. `list_events` flushes queued persistence before reading SQLite. hub. `list_events` flushes queued persistence before reading SQLite.
- `shutdown()` is idempotent and flushes events before stopping the router. - `shutdown()` is idempotent and flushes events before stopping the router.

View File

@@ -1,5 +1,3 @@
#![allow(deprecated)]
use anyhow::Context; use anyhow::Context;
use iroh::RelayUrl; use iroh::RelayUrl;
use iroh_blobs::Hash; 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. /// Public view of a single-use pairing window after a completed transfer.
/// ///
/// The eligibility capability itself never crosses this boundary. /// The eligibility capability itself never crosses this boundary.

View File

@@ -1,4 +1,4 @@
//! Experimental saved-device mutual-consent relationships. //! Saved-device mutual-consent relationships.
//! //!
//! Pending outgoing/incoming states, directional grants bound to relationship //! Pending outgoing/incoming states, directional grants bound to relationship
//! generation, and Saved only after mutual acknowledgement. //! generation, and Saved only after mutual acknowledgement.

View File

@@ -14,8 +14,6 @@ mod logging;
mod pairing_eligibility; mod pairing_eligibility;
mod persistence; mod persistence;
mod runtime; mod runtime;
#[cfg(test)]
mod secret;
#[allow( #[allow(
dead_code, dead_code,
reason = "the private custody seam is activated by platform credential adapters" reason = "the private custody seam is activated by platform credential adapters"
@@ -26,15 +24,13 @@ mod ticket;
mod transfer_state; mod transfer_state;
mod util; mod util;
#[allow(deprecated)]
pub use api::{ pub use api::{
clear_inactive_transfer_cache, default_core_limits, default_core_network_config, clear_inactive_transfer_cache, default_core_limits, default_core_network_config,
experimental_saved_device_capabilities, saved_device_capabilities, CoreEvent, CoreEventSink, saved_device_capabilities, CoreEvent, CoreEventSink, CoreLimits, CoreNetworkConfig,
CoreLimits, CoreNetworkConfig, CoreRelayMode, CoreStorageUsage, DeviceRelationship, CoreRelayMode, CoreStorageUsage, DeviceRelationship, DeviceRelationshipState,
DeviceRelationshipState, ExperimentalSavedDeviceCapabilities, PairingEligibilitySummary, PairingEligibilitySummary, PendingTargetedOffer, PublishedOutput, ReceiveOutputSink,
PendingTargetedOffer, PublishedOutput, ReceiveOutputSink, ReceiveOutputSinkV2, ReceiveOutputSinkV2, ReceivedArtifact, ReceivedLocatorKind, ReceiverRequest, RuntimeStatus,
ReceivedArtifact, ReceivedLocatorKind, ReceiverRequest, RuntimeStatus, SavedDevice, SavedDevice, SavedDeviceCapabilities, ShareMetadataInput, ShareResult, ShareSource, SourceKind,
SavedDeviceCapabilities, ShareMetadataInput, ShareResult, ShareSource, SourceKind,
StoredTransfer, TargetedOfferResponse, TargetedTransfer, TargetedTransferState, StoredTransfer, TargetedOfferResponse, TargetedTransfer, TargetedTransferState,
TicketInspection, TransferAccessMode, TransferMetadata, TicketInspection, TransferAccessMode, TransferMetadata,
}; };

View File

@@ -1,5 +1,3 @@
#![allow(deprecated)]
use std::{future::Future, path::PathBuf, sync::Arc}; use std::{future::Future, path::PathBuf, sync::Arc};
use anyhow::Context; use anyhow::Context;
@@ -317,7 +315,6 @@ impl VnidropCore {
} }
} }
#[allow(deprecated)]
#[uniffi::export] #[uniffi::export]
impl VnidropCore { impl VnidropCore {
#[uniffi::constructor] #[uniffi::constructor]
@@ -371,18 +368,6 @@ impl VnidropCore {
Self::initialize_protected(app_data_dir, event_sink, limits, network_config) 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 { pub fn status(&self) -> RuntimeStatus {
self.block_on(self.inner.status()) self.block_on(self.inner.status())
} }

View File

@@ -6,7 +6,7 @@
//! - [`receive`] — ticket receive, download, export //! - [`receive`] — ticket receive, download, export
//! - [`lifecycle`] — cancel/delete/shutdown/status/access //! - [`lifecycle`] — cancel/delete/shutdown/status/access
//! - [`provider`] — blob provider events and per-connection send progress //! - [`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 //! - [`targeted`] — saved-device targeted transfers
mod delivery; mod delivery;

View File

@@ -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; use std::sync::Arc;

View File

@@ -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(())
}

View File

@@ -37,8 +37,6 @@ mod platform_contract_windows_tests;
mod repository_tests; mod repository_tests;
#[path = "tests/runtime.rs"] #[path = "tests/runtime.rs"]
mod runtime_tests; mod runtime_tests;
#[path = "tests/secret.rs"]
mod secret_tests;
#[path = "tests/secure_secret_android.rs"] #[path = "tests/secure_secret_android.rs"]
mod secure_secret_android_tests; mod secure_secret_android_tests;
#[cfg(any(target_os = "macos", target_os = "ios"))] #[cfg(any(target_os = "macos", target_os = "ios"))]

View File

@@ -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")); let lib = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib.rs"));
for forbidden in [ for forbidden in [
"initialize_with_experimental_saved_devices",
"experimental_saved_device_capabilities",
"ExperimentalSavedDeviceCapabilities",
"fn list_contacts(", "fn list_contacts(",
"fn send_to_contact(", "fn send_to_contact(",
"fn poll_contacts_for_offers(", "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 forget_saved_device(",
"fn share_files(", "fn share_files(",
"fn receive(", "fn receive(",
"experimental_saved_device_capabilities",
"saved_device_capabilities", "saved_device_capabilities",
] { ] {
assert!( assert!(

View File

@@ -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 //! These tests drive the public UniFFI surface through an
//! [`AndroidSecureSecretStore`] backed by an in-process Keystore fake so the //! [`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() path.display()
); );
} }
assert!(!kotlin.contains("initializeWithExperimentalSavedDevices"));
assert!(!kotlin.contains("ExperimentalSavedDeviceCapabilities"));
assert!(!kotlin.contains("experimentalSavedDeviceCapabilities"));
assert!( assert!(
kotlin.contains("initializeWithLimitsAndNetworkConfig"), kotlin.contains("initializeWithLimitsAndNetworkConfig"),
"production protected Android init must remain on the public binding surface" "production protected Android init must remain on the public binding surface"

View File

@@ -53,7 +53,7 @@ impl RecordingSink {
/// `AppleKeychainSecretStore` + profile scoping. CLI unit tests lack the app /// `AppleKeychainSecretStore` + profile scoping. CLI unit tests lack the app
/// Keychain entitlement, so the system Keychain returns Unavailable; the /// Keychain entitlement, so the system Keychain returns Unavailable; the
/// injectable API exercises the identical adapter path. Swift XCTest covers /// 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 { struct KeychainNode {
data_dir: tempfile::TempDir, data_dir: tempfile::TempDir,
api: RecordingKeychain, 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( VnidropCore::initialize_with_limits_and_network_config(
app_data_dir.to_string_lossy().into_owned(), app_data_dir.to_string_lossy().into_owned(),
Arc::new(RecordingSink { Arc::new(RecordingSink {
@@ -593,7 +593,7 @@ fn recover_authoritative_state(
} }
#[test] #[test]
fn experimental_keychain_identity_survives_core_restart() { fn keychain_identity_survives_core_restart() {
let node = KeychainNode::new(); let node = KeychainNode::new();
let endpoint_id = node.core().status().endpoint_id.clone(); let endpoint_id = node.core().status().endpoint_id.clone();
assert!(!endpoint_id.is_empty()); 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 // constructor must also preserve identity. Headless cargo often lacks that
// entitlement and maps it to Unavailable — that path is covered by Swift. // entitlement and maps it to Unavailable — that path is covered by Swift.
let live = tempfile::tempdir().unwrap(); let live = tempfile::tempdir().unwrap();
match try_experimental_keychain_init(live.path()) { match try_keychain_init(live.path()) {
Ok(core) => { Ok(core) => {
let id = core.status().endpoint_id.clone(); let id = core.status().endpoint_id.clone();
core.shutdown(); core.shutdown();
drop(core); 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); assert_eq!(restarted.status().endpoint_id, id);
restarted.shutdown(); restarted.shutdown();
cleanup_scoped_keychain(live.path()); cleanup_scoped_keychain(live.path());
} }
Err(VnidropError::SecureStorageUnavailable { .. }) => {} 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() swift.display()
); );
} }
assert!(!source.contains("initializeWithExperimentalSavedDevices"));
assert!(!source.contains("ExperimentalSavedDeviceCapabilities"));
assert!(!source.contains("experimentalSavedDeviceCapabilities"));
assert!( assert!(
source.contains("initializeWithLimitsAndNetworkConfig"), source.contains("initializeWithLimitsAndNetworkConfig"),
"Swift bindings must expose standard protected initialization" "Swift bindings must expose standard protected initialization"
@@ -865,6 +868,8 @@ fn apple_public_bindings_omit_raw_secrets_and_generic_mutation() {
) )
.expect("api.rs"); .expect("api.rs");
for forbidden in [ for forbidden in [
"ExperimentalSavedDeviceCapabilities",
"experimental_saved_device_capabilities",
"SecretMaterial", "SecretMaterial",
"SecretHandle", "SecretHandle",
"SecureSecretStore", "SecureSecretStore",

View File

@@ -546,7 +546,7 @@ fn secret_service_identity_survives_core_restart() {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[test] #[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 // Regression: protected init used to call blocking Secret Service on the
// Tokio worker that drives `CoreInner::start`, which nested `block_on` and // Tokio worker that drives `CoreInner::start`, which nested `block_on` and
// aborted desktop startup with "Cannot start a runtime from within a runtime". // 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(), CoreLimits::default(),
CoreNetworkConfig::default(), CoreNetworkConfig::default(),
) )
.expect("experimental Linux Secret Service core"); .expect("Linux Secret Service core");
let endpoint_id = core.status().endpoint_id.clone(); let endpoint_id = core.status().endpoint_id.clone();
assert!(!endpoint_id.is_empty()); assert!(!endpoint_id.is_empty());
assert!(!data_dir.path().join("iroh.secret").exists()); 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)); 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); assert_eq!(restarted.status().endpoint_id, endpoint_id);
@@ -842,7 +842,7 @@ fn linux_public_bindings_omit_raw_secrets_and_generic_mutation() {
} }
assert!( assert!(
public_facade.contains("initialize_with_limits_and_network_config"), public_facade.contains("initialize_with_limits_and_network_config"),
"facade must expose experimental saved-device init" "facade must expose standard protected initialization"
); );
assert!( assert!(
!public_facade.contains("fn set_state(") && !public_facade.contains("fn mutate_state("), !public_facade.contains("fn set_state(") && !public_facade.contains("fn mutate_state("),

View File

@@ -315,7 +315,7 @@ fn windows_dpapi_identity_survives_core_restart() {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[test] #[test]
fn real_windows_dpapi_experimental_init_preserves_identity() { fn real_windows_dpapi_init_preserves_identity() {
let data_dir = tempfile::tempdir().unwrap(); let data_dir = tempfile::tempdir().unwrap();
let path = data_dir.path().to_string_lossy().into_owned(); let path = data_dir.path().to_string_lossy().into_owned();
let sink = Arc::new(RecordingSink { let sink = Arc::new(RecordingSink {
@@ -327,7 +327,7 @@ fn real_windows_dpapi_experimental_init_preserves_identity() {
CoreLimits::default(), CoreLimits::default(),
CoreNetworkConfig::default(), CoreNetworkConfig::default(),
) )
.expect("experimental windows core"); .expect("Windows core");
let first = core.status().endpoint_id.clone(); let first = core.status().endpoint_id.clone();
core.shutdown(); core.shutdown();
drop(core); drop(core);
@@ -341,7 +341,7 @@ fn real_windows_dpapi_experimental_init_preserves_identity() {
CoreLimits::default(), CoreLimits::default(),
CoreNetworkConfig::default(), CoreNetworkConfig::default(),
) )
.expect("restarted experimental windows core"); .expect("restarted Windows core");
assert_eq!(restarted.status().endpoint_id, first); assert_eq!(restarted.status().endpoint_id, first);
restarted.shutdown(); restarted.shutdown();
} }

View File

@@ -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
);
}

View File

@@ -8,7 +8,7 @@ use vnidrop::{
}; };
#[test] #[test]
fn saved_device_protocols_are_explicitly_experimental_and_versioned() { fn saved_device_protocols_are_explicitly_versioned() {
assert_eq!( assert_eq!(
saved_device_capabilities(), saved_device_capabilities(),
SavedDeviceCapabilities { SavedDeviceCapabilities {
@@ -97,7 +97,7 @@ fn targeted_transfer_transitions_are_validated_by_the_domain() {
} }
#[test] #[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 source_dir = tempfile::tempdir().unwrap();
let first_output = tempfile::tempdir().unwrap(); let first_output = tempfile::tempdir().unwrap();
let second_output = tempfile::tempdir().unwrap(); let second_output = tempfile::tempdir().unwrap();

View File

@@ -32,6 +32,9 @@ class SavedDeviceCoreContractBindingHygieneTest {
"generated binding ${binding.path} must not expose `$token`", "generated binding ${binding.path} must not expose `$token`",
) )
} }
assertFalse(source.contains("initializeWithExperimentalSavedDevices"))
assertFalse(source.contains("ExperimentalSavedDeviceCapabilities"))
assertFalse(source.contains("experimentalSavedDeviceCapabilities"))
assertTrue( assertTrue(
source.contains("initializeWithLimitsAndNetworkConfig"), source.contains("initializeWithLimitsAndNetworkConfig"),

View File

@@ -32,6 +32,9 @@ class SavedDeviceCoreContractBindingHygieneTest {
"generated binding ${binding.path} must not expose `$token`", "generated binding ${binding.path} must not expose `$token`",
) )
} }
assertFalse(source.contains("initializeWithExperimentalSavedDevices"))
assertFalse(source.contains("ExperimentalSavedDeviceCapabilities"))
assertFalse(source.contains("experimentalSavedDeviceCapabilities"))
assertTrue( assertTrue(
source.contains("initializeWithLimitsAndNetworkConfig"), source.contains("initializeWithLimitsAndNetworkConfig"),