From 7682fc6b572669461d52283f839d794ae24bf201 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sat, 5 Sep 2026 07:32:03 +0400 Subject: [PATCH] refactor: cleanup --- src-tauri/src/browser_runner.rs | 54 ++-- src-tauri/src/fingerprint_consistency.rs | 41 ++- src-tauri/src/lib.rs | 4 +- src-tauri/src/mcp_server.rs | 3 + src-tauri/src/profile/manager.rs | 57 +++- src-tauri/src/profile_importer.rs | 11 +- src-tauri/src/wayfern_manager.rs | 314 +++++++++++++++-------- src/components/cookie-bot-shared.tsx | 5 +- src/components/sync-follower-dialog.tsx | 6 +- src/components/wayfern-config-dialog.tsx | 5 +- src/components/wayfern-config-form.tsx | 38 ++- src/types.ts | 4 +- 12 files changed, 367 insertions(+), 175 deletions(-) diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index e609818..e22abf9 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -474,8 +474,21 @@ impl BrowserRunner { // tested immediately below - which is a deliberate per-profile setting // and not a consequence of the version. let mut updated_profile = profile.clone(); + // ONE-TIME MIGRATION: a profile that still stores a whole device beside + // its identity moves to identity-only storage here, before the launch + // reads it, and the migrated shape is what gets persisted below. + if crate::wayfern_manager::WayfernManager::migrate_identity_config(&mut wayfern_config) { + let mut cfg = updated_profile.wayfern_config.clone().unwrap_or_default(); + crate::wayfern_manager::WayfernManager::migrate_identity_config(&mut cfg); + updated_profile.wayfern_config = Some(cfg); + log::info!( + "Migrated Wayfern profile {} to identity-only storage", + profile.name + ); + } let randomize_requested = wayfern_config.randomize_fingerprint_on_launch == Some(true); - let needs_device = wayfern_config.fingerprint.is_none(); + let needs_device = + wayfern_config.fingerprint.is_none() && wayfern_config.identity_id.is_none(); if randomize_requested || needs_device { if needs_device && !randomize_requested { log::info!( @@ -530,16 +543,29 @@ impl BrowserRunner { generated.identity_id ); - // Update the config with the new fingerprint for launching - wayfern_config.fingerprint = Some(generated.fingerprint.clone()); + // Update the config with the new device for launching. An identity + // stores the id and the location only; a legacy browser stores the + // whole payload. + let is_identity = generated.identity_id.is_some(); wayfern_config.identity_id = generated.identity_id.clone(); - wayfern_config.identity_baseline = generated.identity_baseline.clone(); + wayfern_config.location = generated.location.clone(); + wayfern_config.identity_baseline = None; + wayfern_config.fingerprint = if is_identity { + None + } else { + Some(generated.fingerprint.clone()) + }; - // Save the updated fingerprint to the profile so it persists. + // Save the updated device to the profile so it persists. let mut updated_wayfern_config = updated_profile.wayfern_config.clone().unwrap_or_default(); - updated_wayfern_config.fingerprint = Some(generated.fingerprint); updated_wayfern_config.identity_id = generated.identity_id; - updated_wayfern_config.identity_baseline = generated.identity_baseline; + updated_wayfern_config.location = generated.location; + updated_wayfern_config.identity_baseline = None; + updated_wayfern_config.fingerprint = if is_identity { + None + } else { + Some(generated.fingerprint) + }; // Preserve the randomize flag so it persists across launches updated_wayfern_config.randomize_fingerprint_on_launch = wayfern_config.randomize_fingerprint_on_launch; @@ -714,23 +740,19 @@ impl BrowserRunner { // which may differ from the stored one. Persist it so the next launch // starts from that value — saved below via // save_process_info(&updated_profile). + // LEGACY profiles only: an identity-backed profile never persists the + // device (the manager returns no echo for it), so this block is reached + // only by a whole-payload profile applied with setFingerprint. if let Some(used_fp) = wayfern_result.used_fingerprint.clone() { let mut cfg = updated_profile.wayfern_config.clone().unwrap_or_default(); - let baseline_changed = wayfern_result.used_identity_baseline.is_some() - && cfg.identity_baseline != wayfern_result.used_identity_baseline; - if cfg.fingerprint.as_deref() != Some(used_fp.as_str()) || baseline_changed { + if cfg.identity_id.is_none() && cfg.fingerprint.as_deref() != Some(used_fp.as_str()) { log::info!( "Persisting applied fingerprint echoed by Wayfern for profile: {} (len {})", profile.name, used_fp.len() ); cfg.fingerprint = Some(used_fp); - // The baseline must move with the fingerprint it was computed - // against, or the next launch diffs the two apart and invents - // overrides the user never asked for. - if let Some(baseline) = wayfern_result.used_identity_baseline.clone() { - cfg.identity_baseline = Some(baseline); - } + cfg.identity_baseline = None; updated_profile.wayfern_config = Some(cfg); } } diff --git a/src-tauri/src/fingerprint_consistency.rs b/src-tauri/src/fingerprint_consistency.rs index 3090781..68cb76e 100644 --- a/src-tauri/src/fingerprint_consistency.rs +++ b/src-tauri/src/fingerprint_consistency.rs @@ -146,12 +146,13 @@ fn language_matches_country(cc: &str, language: &str) -> Option { crate::geolocation::locale_selector()?.region_speaks(cc, language) } -/// Extract (timezone, language) from a profile's stored fingerprint JSON. +/// Extract (timezone, language) from a profile's stored location, or from its +/// legacy fingerprint payload when it still stores one. fn fingerprint_locale(profile: &BrowserProfile) -> (Option, Option) { let Some(config) = &profile.wayfern_config else { return (None, None); }; - let Some(fp_str) = &config.fingerprint else { + let Some(fp_str) = config.location.as_ref().or(config.fingerprint.as_ref()) else { return (None, None); }; let Ok(fp) = serde_json::from_str::(fp_str) else { @@ -363,20 +364,34 @@ pub async fn match_profile_fingerprint_to_exit( let mut config = profile .wayfern_config .clone() - .filter(|c| c.fingerprint.is_some()) + .filter(|c| c.fingerprint.is_some() || c.identity_id.is_some()) .ok_or_else(|| serde_json::json!({ "code": "FINGERPRINT_MATCH_FAILED" }).to_string())?; - let fingerprint = config.fingerprint.clone().unwrap(); let geoip_override = serde_json::Value::String(exit_ip); - let refreshed = crate::wayfern_manager::WayfernManager::refresh_fingerprint_geolocation( - &fingerprint, - None, - Some(&geoip_override), - ) - .await - .ok_or_else(|| serde_json::json!({ "code": "FINGERPRINT_MATCH_FAILED" }).to_string())?; - - config.fingerprint = Some(refreshed); + if let Some(fingerprint) = config.fingerprint.clone() { + // Legacy payload: the location lives inside the stored device. + let refreshed = crate::wayfern_manager::WayfernManager::refresh_fingerprint_geolocation( + &fingerprint, + None, + Some(&geoip_override), + ) + .await + .ok_or_else(|| serde_json::json!({ "code": "FINGERPRINT_MATCH_FAILED" }).to_string())?; + config.fingerprint = Some(refreshed); + } else { + // Identity-backed: only the location object moves; the device stays + // whatever the identity derives. + let location = config.location.clone().unwrap_or_else(|| "{}".to_string()); + let refreshed = crate::wayfern_manager::WayfernManager::refresh_fingerprint_geolocation( + &location, + None, + Some(&geoip_override), + ) + .await + .ok_or_else(|| serde_json::json!({ "code": "FINGERPRINT_MATCH_FAILED" }).to_string())?; + config.location = crate::wayfern_manager::WayfernManager::fingerprint_object(&refreshed) + .and_then(|object| crate::wayfern_manager::WayfernManager::location_of(&object)); + } profile.wayfern_config = Some(config); manager.save_profile(&profile).map_err(|e| { serde_json::json!({ "code": "INTERNAL_ERROR", "params": { "detail": e.to_string() } }) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fe1a45e..44ebf78 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1304,7 +1304,7 @@ async fn list_active_vpn_connections() -> Result, String> { struct SampleFingerprint { fingerprint: String, identity_id: Option, - identity_baseline: Option, + location: Option, } #[tauri::command] @@ -1356,7 +1356,7 @@ async fn generate_sample_fingerprint( .map(|generated| SampleFingerprint { fingerprint: generated.fingerprint, identity_id: generated.identity_id, - identity_baseline: generated.identity_baseline, + location: generated.location, }) .map_err(|e| format!("Failed to generate fingerprint: {e}")) } else { diff --git a/src-tauri/src/mcp_server.rs b/src-tauri/src/mcp_server.rs index 4ebd76a..7ca8da4 100644 --- a/src-tauri/src/mcp_server.rs +++ b/src-tauri/src/mcp_server.rs @@ -4224,6 +4224,9 @@ impl McpServer { serde_json::json!({ "browser": "wayfern", "fingerprint": config.fingerprint, + "identity_id": config.identity_id, + "identity_overrides": config.identity_overrides, + "location": config.location, "os": config.os, "randomize_fingerprint_on_launch": config.randomize_fingerprint_on_launch, "screen_max_width": config.screen_max_width, diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index 1e60f86..9094f34 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -181,8 +181,9 @@ impl ProfileManager { // behavior; for generated ones this comes from the geolocation lookup. let mut geolocation_applied = true; - // Generate fingerprint if not already provided - if config.fingerprint.is_none() { + // Generate a device if the profile has neither a legacy payload nor an + // identity. + if config.fingerprint.is_none() && config.identity_id.is_none() { log::info!("Generating fingerprint for Wayfern profile: {name}"); // Create a temporary profile for fingerprint generation @@ -224,12 +225,16 @@ impl ProfileManager { .await { Ok(generated) => { - config.fingerprint = Some(generated.fingerprint); - // Set together with the fingerprint they describe. A profile that - // stored one without the other would either lose reproducibility or - // diff its whole device into overrides on the next launch. + // An identity-backed profile stores the id and the location and + // never the device; a legacy browser stores the whole payload. config.identity_id = generated.identity_id; - config.identity_baseline = generated.identity_baseline; + config.location = generated.location; + config.identity_baseline = None; + config.fingerprint = if config.identity_id.is_some() { + None + } else { + Some(generated.fingerprint) + }; geolocation_applied = generated.geolocation_applied; log::info!("Successfully generated fingerprint for Wayfern profile: {name}"); } @@ -1193,12 +1198,42 @@ impl ProfileManager { // re-mint the device on the next launch and throw the edit away with it, // which is the opposite of what an override is for. Carry it forward unless // the caller either supplied its own or cleared the fingerprint outright. - if config.identity_id.is_none() && config.fingerprint.is_some() { - if let Some(stored) = profile.wayfern_config.as_ref() { + if let Some(stored) = profile.wayfern_config.as_ref() { + if config.identity_id.is_none() + && (config.fingerprint.is_some() || config.identity_overrides.is_some()) + { config.identity_id = stored.identity_id.clone(); - config.identity_baseline = stored.identity_baseline.clone(); + } + if config.identity_id.is_some() { + if config.location.is_none() { + config.location = stored.location.clone(); + } + if config.identity_overrides.is_none() { + config.identity_overrides = stored.identity_overrides.clone(); + } + // A WHOLE fingerprint sent for an identity-backed profile (an older UI + // or an API/MCP caller) is an explicit set of fields: it becomes the + // override map and is never stored as a device. + if let Some(fingerprint) = config.fingerprint.take() { + if let Some(object) = + crate::wayfern_manager::WayfernManager::fingerprint_object(&fingerprint) + { + let overrides = + crate::wayfern_manager::WayfernManager::overrides_from_explicit_fingerprint(&object); + config.identity_overrides = if overrides.is_empty() { + None + } else { + serde_json::to_string(&overrides).ok() + }; + if config.location.is_none() { + config.location = crate::wayfern_manager::WayfernManager::location_of(&object); + } + } + } } } + // The baseline is a legacy field; nothing writes it any more. + config.identity_baseline = None; // Update the Wayfern configuration profile.wayfern_config = Some(config); @@ -2036,7 +2071,7 @@ pub async fn update_wayfern_config( profile_id: String, config: WayfernConfig, ) -> Result<(), String> { - if config.fingerprint.is_some() + if (config.fingerprint.is_some() || config.identity_overrides.is_some()) && !crate::cloud_auth::CLOUD_AUTH .can_use_cross_os_fingerprints() .await diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 1df67ad..49b39e9 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -948,7 +948,7 @@ impl ProfileImporter { } } - if config.fingerprint.is_none() { + if config.fingerprint.is_none() && config.identity_id.is_none() { let temp_profile = BrowserProfile { id: uuid::Uuid::new_v4(), name: new_profile_name.to_string(), @@ -989,9 +989,14 @@ impl ProfileImporter { // geo_proxy_signature is intentionally left unset here: the first // launch's signature-mismatch refresh verifies the location either way. Ok(generated) => { - config.fingerprint = Some(generated.fingerprint); config.identity_id = generated.identity_id; - config.identity_baseline = generated.identity_baseline; + config.location = generated.location; + config.identity_baseline = None; + config.fingerprint = if config.identity_id.is_some() { + None + } else { + Some(generated.fingerprint) + }; } Err(e) => { let _ = fs::remove_dir_all(&new_profile_uuid_dir); diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs index befa372..d6a0b9a 100644 --- a/src-tauri/src/wayfern_manager.rs +++ b/src-tauri/src/wayfern_manager.rs @@ -45,15 +45,30 @@ pub struct WayfernConfig { /// location can be refreshed instead of showing stale data. #[serde(default)] pub geo_proxy_signature: Option, - /// Identity handle for this profile, when it has one. `None` means the - /// profile stores a whole fingerprint payload instead. + /// Identity handle for this profile, when it has one. An identity-backed + /// profile stores the id, its `location` and its `identity_overrides` and + /// NOTHING else: the device is rebuilt from the id by the browser on every + /// launch, so no fingerprint payload ever sits on disk to be copied. + /// `None` means a legacy profile that still stores a whole payload in + /// `fingerprint` and is applied with `Wayfern.setFingerprint`. #[serde(default)] pub identity_id: Option, - /// The fingerprint as first received for `identity_id`, before geolocation - /// and before any user edit. Diffed against `fingerprint` on launch to - /// recover the user's own edits. + /// LEGACY, read only by `migrate_identity_config`: the derived device an + /// older build snapshotted so the user's edits could be diffed out of the + /// stored payload. Cleared by the migration; never written again. #[serde(default)] pub identity_baseline: Option, + /// The user's own edits to an identity-backed device, as a JSON object of + /// fingerprint fields. Sent verbatim as `setIdentity` overrides; everything + /// not listed here comes from the identity. `None` means no edits. + #[serde(default)] + pub identity_overrides: Option, + /// The location the profile's exit resolves to (timezone, timezoneOffset, + /// language, languages, latitude, longitude, accuracy) as a JSON object. + /// It depends on the proxy, not on the identity, which is why it is the one + /// piece of device state an identity-backed profile persists. + #[serde(default)] + pub location: Option, } /// First Wayfern version that ships `createIdentity`/`setIdentity`/ @@ -119,12 +134,15 @@ const LOCALE_CARRY_OVER_KEYS: [&str; 7] = [ /// A freshly generated device, plus its identity handle when the browser /// supports identities. pub struct GeneratedFingerprint { - /// The fingerprint JSON to store in `WayfernConfig::fingerprint`. Both paths - /// produce a flat camelCase object, so everything that reads the stored - /// fingerprint keeps working either way. + /// The device the browser produced, as a flat camelCase JSON object. For a + /// LEGACY browser this is what `WayfernConfig::fingerprint` stores. For an + /// identity-backed profile it is a VIEW for the caller to show once and + /// discard: only `identity_id` and `location` are persisted. pub fingerprint: String, pub identity_id: Option, - pub identity_baseline: Option, + /// `WayfernConfig::location` for the exit this device was generated + /// against, or `None` when no location field was resolved. + pub location: Option, /// Whether fresh geolocation was resolved and applied. Callers must only /// stamp `geo_proxy_signature` when this is true. pub geolocation_applied: bool, @@ -277,7 +295,7 @@ impl WayfernManager { /// Parse a stored fingerprint JSON into its object, tolerating the legacy /// `{ "fingerprint": {...} }` wrapper some old profiles carry. - fn fingerprint_object( + pub fn fingerprint_object( fingerprint_json: &str, ) -> Option> { let parsed: serde_json::Value = serde_json::from_str(fingerprint_json).ok()?; @@ -285,6 +303,94 @@ impl WayfernManager { fp.as_object().cloned() } + /// A stored JSON object field (`identity_overrides`, `location`), or an + /// empty map when absent or unparsable. + pub fn stored_object(json: Option<&str>) -> serde_json::Map { + json.and_then(Self::fingerprint_object).unwrap_or_default() + } + + /// The exit-derived location fields a device object carries, in the shape + /// `WayfernConfig::location` stores; `None` when it carries none. + pub fn location_of( + device: &serde_json::Map, + ) -> Option { + let mut location = serde_json::Map::new(); + for key in LOCALE_CARRY_OVER_KEYS { + if let Some(value) = device.get(key) { + if !value.is_null() { + location.insert(key.to_string(), value.clone()); + } + } + } + if location.is_empty() { + None + } else { + serde_json::to_string(&location).ok() + } + } + + /// Overrides from a WHOLE fingerprint an API or MCP caller supplied for an + /// identity-backed profile: every field it names is taken as an explicit + /// edit, except the provenance keys the browser refuses and the location + /// keys, which travel through `location`. + pub fn overrides_from_explicit_fingerprint( + fingerprint: &serde_json::Map, + ) -> serde_json::Map { + let mut overrides = serde_json::Map::new(); + for (key, value) in fingerprint { + if DERIVED_PROVENANCE_KEYS.contains(&key.as_str()) + || GEO_PARAM_KEYS.contains(&key.as_str()) + || LOCALE_CARRY_OVER_KEYS.contains(&key.as_str()) + || value.is_null() + { + continue; + } + overrides.insert(key.clone(), value.clone()); + } + overrides + } + + /// ONE-TIME MIGRATION to identity-only storage. A profile created by an + /// earlier build stored the whole device in `fingerprint` beside its + /// `identity_id`, with `identity_baseline` recording the derived view so the + /// user's edits could be diffed out. This moves those edits into + /// `identity_overrides`, the exit-derived fields into `location`, and drops + /// the payload and the baseline. Returns whether anything changed. + /// + /// Without a baseline nothing can separate an edit from a derived value, so + /// no override is recovered: pinning the whole device would defeat the + /// identity, and the browser rebuilds every field from the id anyway. + pub fn migrate_identity_config(config: &mut WayfernConfig) -> bool { + if config.identity_id.is_none() { + return false; + } + let Some(stored_json) = config.fingerprint.clone() else { + if config.identity_baseline.is_some() { + config.identity_baseline = None; + return true; + } + return false; + }; + let stored = Self::fingerprint_object(&stored_json).unwrap_or_default(); + let overrides = match config + .identity_baseline + .as_deref() + .and_then(Self::fingerprint_object) + { + Some(baseline) => Self::identity_overrides(&stored, &baseline), + None => serde_json::Map::new(), + }; + if config.identity_overrides.is_none() && !overrides.is_empty() { + config.identity_overrides = serde_json::to_string(&overrides).ok(); + } + if config.location.is_none() { + config.location = Self::location_of(&stored); + } + config.fingerprint = None; + config.identity_baseline = None; + true + } + /// The user's edits, recovered as the difference between the fingerprint the /// profile stores and the view the browser derived from the identity. /// @@ -379,35 +485,6 @@ impl WayfernManager { locale.split('-').next().unwrap_or(locale) } - /// The baseline to persist after a successful `setIdentity`. - /// - /// For every key the user did NOT override, adopt whatever the browser just - /// derived, so a value that changes on a newer browser flows through instead - /// of reading as a user edit forever. For overridden keys the applied view - /// holds the override rather than the derived value, so the previous baseline - /// is kept as the diff reference. - fn refreshed_identity_baseline( - applied: &serde_json::Map, - previous_baseline: &serde_json::Map, - overrides: &serde_json::Map, - ) -> serde_json::Map { - let mut baseline = applied.clone(); - for key in overrides.keys() { - match previous_baseline.get(key) { - Some(previous) => { - baseline.insert(key.clone(), previous.clone()); - } - // The user added a field the baseline never carried, so there is - // nothing to fall back to and the key must stay absent from the - // baseline or the edit would diff away on the next launch. - None => { - baseline.remove(key); - } - } - } - baseline - } - /// The `setIdentity` geolocation parameters carried by a stored fingerprint. fn geo_params( fingerprint: &serde_json::Map, @@ -942,7 +1019,7 @@ impl WayfernManager { } }; - let (fingerprint, identity_id, identity_baseline, geolocation_applied) = match generate_result { + let (fingerprint, identity_id, geolocation_applied) = match generate_result { Ok(result) => { // createIdentity returns { identityId, identity }; getFingerprint // returns { fingerprint: {...} }. A bare object is tolerated so a @@ -961,15 +1038,6 @@ impl WayfernManager { // Normalize the fingerprint: convert JSON string fields to proper types let mut normalized = Self::normalize_fingerprint(fp); - // Snapshot the derived view BEFORE geolocation is applied, so the - // location fields donut writes below are not mistaken for user edits - // when the overrides are recovered on launch. - let identity_baseline = if use_identity_api { - serde_json::to_string(&normalized).ok() - } else { - None - }; - // reqwest's SOCKS connector (hyper-util) corrupts its parse buffer // when a proxy splits a handshake reply across TCP segments, so a // socks upstream here can fail even though the proxy is healthy. @@ -1020,12 +1088,7 @@ impl WayfernManager { let _ = crate::proxy_runner::stop_proxy_process(&worker_id).await; } - ( - normalized, - identity_id, - identity_baseline, - geolocation_applied, - ) + (normalized, identity_id, geolocation_applied) } Err(e) => { cleanup().await; @@ -1075,9 +1138,9 @@ impl WayfernManager { } Ok(GeneratedFingerprint { + location: fingerprint.as_object().and_then(Self::location_of), fingerprint: fingerprint_json, identity_id, - identity_baseline, geolocation_applied, }) } @@ -1407,8 +1470,88 @@ impl WayfernManager { // Apply fingerprint if configured let mut used_fingerprint: Option = None; - let mut used_identity_baseline: Option = None; - if let Some(fingerprint_json) = &config.fingerprint { + // Always None: nothing writes a baseline any more. The field stays on the + // result for the one launch that still migrates a legacy identity profile. + let used_identity_baseline: Option = None; + // An identity-backed profile: the id, the user's overrides and the exit's + // location are all the browser needs, and all the profile stores. The + // device comes back in the response and is deliberately NOT persisted. + let identity_only = supports_identity_api(&profile.version) + && config.identity_id.is_some() + && config.fingerprint.is_none(); + if identity_only { + let identity_id = config.identity_id.clone().unwrap_or_default(); + let overrides = Self::stored_object(config.identity_overrides.as_deref()); + let location = Self::stored_object(config.location.as_deref()); + let wayfern_token = crate::cloud_auth::CLOUD_AUTH.get_wayfern_token().await; + + let mut params = serde_json::Map::new(); + params.insert("identityId".to_string(), json!(identity_id)); + // The claimed OS travels explicitly as well as inside the id. A Wayfern + // 152 id carries an epoch and a 16-bit check that a 151 browser's decoder + // does not know; without this parameter 151 would read such an id as + // untagged and rebuild the HOST OS. Both releases let the explicit + // parameter win, so this keeps one stored profile portable across them. + if let Some(os) = config.os.as_deref().filter(|os| !os.is_empty()) { + params.insert("operatingSystem".to_string(), json!(os)); + } + if !overrides.is_empty() { + params.insert( + "overrides".to_string(), + serde_json::Value::Object(overrides.clone()), + ); + } + // Location is a property of the exit, not of the identity, so it travels + // in setIdentity's own parameters rather than as an override. + params.extend(Self::geo_params(&location)); + if let Some(ref token) = wayfern_token { + params.insert("wayfernToken".to_string(), json!(token)); + } + log::info!( + "Applying Wayfern identity {} with {} override(s): {:?}", + identity_id, + overrides.len(), + overrides.keys().collect::>() + ); + + let mut applied_ok = false; + let mut last_apply_error: Option = None; + for target in &page_targets { + if let Some(ws_url) = &target.websocket_debugger_url { + match self + .send_cdp_command( + ws_url, + "Wayfern.setIdentity", + serde_json::Value::Object(params.clone()), + ) + .await + { + Ok(_) => { + applied_ok = true; + log::info!("Successfully applied identity to page target"); + } + Err(e) => { + log::error!("Failed to apply identity to target: {e}"); + last_apply_error = Some(e.to_string()); + } + } + } + } + if !applied_ok { + let detail = last_apply_error + .unwrap_or_else(|| "the browser exposed no page target to apply it to".to_string()); + log::error!( + "Killing Wayfern (pid {process_id:?}) for profile {}: the identity was never applied: {detail}", + profile.name + ); + if let Some(pid) = process_id { + kill_browser_process(pid); + } + return Err( + Self::apply_failure_error(&detail, Self::claimed_operating_system(config, None)).into(), + ); + } + } else if let Some(fingerprint_json) = &config.fingerprint { log::info!( "Applying fingerprint to Wayfern browser, fingerprint length: {} chars", fingerprint_json.len() @@ -1490,7 +1633,7 @@ impl WayfernManager { // On the identity path only the user's own edits are sent; everything // else comes from the identity itself. - let (apply_method, apply_params, previous_baseline, overrides) = + let (apply_method, apply_params, _previous_baseline, overrides) = match config.identity_id.as_deref().filter(|_| apply_by_identity) { Some(identity_id) => { let previous_baseline = config @@ -1577,23 +1720,6 @@ impl WayfernManager { .cloned() .unwrap_or(result); if let Some(applied_obj) = applied.as_object() { - if apply_by_identity { - // The baseline is "what the browser derived", so it is - // computed from the untouched response. Move it forward for - // everything the user did not override: leaving it behind - // would make the next launch read a re-derived value as a - // user edit and pin it. - let baseline = Self::refreshed_identity_baseline( - applied_obj, - &previous_baseline, - &overrides, - ); - match serde_json::to_string(&baseline) { - Ok(s) => used_identity_baseline = Some(s), - Err(e) => log::warn!("Failed to serialize identity baseline: {e}"), - } - } - let mut persisted = applied; if apply_by_identity { // The location travelled as setIdentity parameters rather @@ -2156,42 +2282,6 @@ mod tests { assert!(geo.get("platform").is_none()); } - #[test] - fn baseline_adopts_rederived_values_but_keeps_overridden_ones() { - // The same identity on a newer browser derives 12 cores where it used to - // derive 8, while the user has pinned deviceMemory to 32. - let previous = obj(r#"{"hardwareConcurrency": 8, "deviceMemory": 8}"#); - let overrides = obj(r#"{"deviceMemory": 32}"#); - let applied = obj(r#"{"hardwareConcurrency": 12, "deviceMemory": 32}"#); - - let refreshed = WayfernManager::refreshed_identity_baseline(&applied, &previous, &overrides); - - // Re-derived: adopted, so the next launch does not mistake it for an edit. - assert_eq!(refreshed.get("hardwareConcurrency"), Some(&json!(12))); - // Overridden: the applied view holds the override, so the derived value is - // kept as the diff reference and the override survives. - assert_eq!(refreshed.get("deviceMemory"), Some(&json!(8))); - - let next_overrides = WayfernManager::identity_overrides(&applied, &refreshed); - assert_eq!(next_overrides.len(), 1); - assert_eq!(next_overrides.get("deviceMemory"), Some(&json!(32))); - } - - #[test] - fn baseline_keeps_a_user_added_key_out_so_the_override_survives() { - // The user set a field the derivation never produces. There is no derived - // value to fall back to, so the key must stay absent from the baseline. - let previous = obj(r#"{"platform": "Win32"}"#); - let overrides = obj(r#"{"doNotTrack": "1"}"#); - let applied = obj(r#"{"platform": "Win32", "doNotTrack": "1"}"#); - - let refreshed = WayfernManager::refreshed_identity_baseline(&applied, &previous, &overrides); - assert!(refreshed.get("doNotTrack").is_none()); - - let next_overrides = WayfernManager::identity_overrides(&applied, &refreshed); - assert_eq!(next_overrides.get("doNotTrack"), Some(&json!("1"))); - } - #[test] fn the_launch_echo_only_fills_location_the_browser_left_out() { // setIdentity carries the location in its own parameters, so the applied diff --git a/src/components/cookie-bot-shared.tsx b/src/components/cookie-bot-shared.tsx index b6874ae..00f193d 100644 --- a/src/components/cookie-bot-shared.tsx +++ b/src/components/cookie-bot-shared.tsx @@ -388,7 +388,10 @@ export function enableProfileSync(profileId: string): Promise { * the operator happens to be sitting. Falls back to this machine's zone. */ export function profileTimezone(profile: BrowserProfile): string { - const raw = profile.wayfern_config?.fingerprint; + // Identity-backed profiles keep the exit's location in `location`; legacy + // ones carry it inside the stored payload. + const raw = + profile.wayfern_config?.location ?? profile.wayfern_config?.fingerprint; if (raw) { try { const parsed = JSON.parse(raw) as WayfernFingerprintConfig; diff --git a/src/components/sync-follower-dialog.tsx b/src/components/sync-follower-dialog.tsx index dc34b60..ff0f6f0 100644 --- a/src/components/sync-follower-dialog.tsx +++ b/src/components/sync-follower-dialog.tsx @@ -31,7 +31,11 @@ import { RippleButton } from "./ui/ripple"; function getScreenSize( profile: BrowserProfile, ): { w: number; h: number } | null { - const fp = profile.wayfern_config?.fingerprint; + // An identity-backed profile stores no device, only the user's edits, so a + // screen size is available only when the user pinned one. + const fp = + profile.wayfern_config?.fingerprint ?? + profile.wayfern_config?.identity_overrides; if (!fp) return null; try { const parsed: WayfernFingerprintConfig = JSON.parse(fp); diff --git a/src/components/wayfern-config-dialog.tsx b/src/components/wayfern-config-dialog.tsx index 035e062..15ff3eb 100644 --- a/src/components/wayfern-config-dialog.tsx +++ b/src/components/wayfern-config-dialog.tsx @@ -65,9 +65,10 @@ export function WayfernConfigDialog({ const handleSave = async () => { if (!profile) return; - if (config.fingerprint) { + const storedJson = config.identity_overrides ?? config.fingerprint; + if (storedJson) { try { - JSON.parse(config.fingerprint); + JSON.parse(storedJson); } catch (_error) { const { toast } = await import("sonner"); toast.error(t("wayfernConfigDialog.invalidFingerprint"), { diff --git a/src/components/wayfern-config-form.tsx b/src/components/wayfern-config-form.tsx index fe4c326..dc2b302 100644 --- a/src/components/wayfern-config-form.tsx +++ b/src/components/wayfern-config-form.tsx @@ -55,7 +55,7 @@ const isFingerprintEditingDisabled = (config: WayfernConfig): boolean => { interface GeneratedFingerprint { fingerprint: string; identity_id: string | null; - identity_baseline: string | null; + location: string | null; } const getCurrentOS = (): WayfernOS => { @@ -109,15 +109,18 @@ export function WayfernConfigForm({ configJson, }, ); - onConfigChange("fingerprint", result.fingerprint); - // The identity travels with the fingerprint it produced. Storing one - // without the other leaves a device the launch path cannot reproduce, so - // it would be discarded and re-minted on the next launch. + // An identity-backed profile stores the id, its location and the user's + // edits, never the device: the browser rebuilds the device from the id + // on every launch, so nothing worth copying is ever written to disk. A + // legacy browser without the identity API still stores the payload. onConfigChange("identity_id", result.identity_id ?? undefined); + onConfigChange("location", result.location ?? undefined); onConfigChange( - "identity_baseline", - result.identity_baseline ?? undefined, + "fingerprint", + result.identity_id ? undefined : result.fingerprint, ); + onConfigChange("identity_overrides", undefined); + onConfigChange("identity_baseline", undefined); } catch (error) { console.error("Failed to generate fingerprint:", error); } finally { @@ -164,12 +167,16 @@ export function WayfernConfigForm({ onConfigChange, ]); + // What the form edits: the override map for an identity-backed profile + // (only the user's own edits exist on disk), the whole payload for a legacy + // one. + const editedJson = config.identity_id + ? config.identity_overrides + : config.fingerprint; useEffect(() => { - if (config.fingerprint) { + if (editedJson) { try { - const parsed = JSON.parse( - config.fingerprint, - ) as WayfernFingerprintConfig; + const parsed = JSON.parse(editedJson) as WayfernFingerprintConfig; setFingerprintConfig(parsed); } catch (error) { console.error("Failed to parse fingerprint config:", error); @@ -178,7 +185,7 @@ export function WayfernConfigForm({ } else { setFingerprintConfig({}); } - }, [config.fingerprint]); + }, [editedJson]); const updateFingerprintConfig = ( key: keyof WayfernFingerprintConfig, @@ -200,7 +207,12 @@ export function WayfernConfigForm({ try { const jsonString = JSON.stringify(newConfig); - onConfigChange("fingerprint", jsonString); + onConfigChange( + config.identity_id ? "identity_overrides" : "fingerprint", + Object.keys(newConfig).length === 0 && config.identity_id + ? undefined + : jsonString, + ); } catch (error) { console.error("Failed to serialize fingerprint config:", error); } diff --git a/src/types.ts b/src/types.ts index 040bcce..41e0238 100644 --- a/src/types.ts +++ b/src/types.ts @@ -414,7 +414,9 @@ export interface WayfernConfig { os?: WayfernOS; // Operating system for fingerprint generation geo_proxy_signature?: string; // Internal: routing the fingerprint's location was computed for identity_id?: string; // Internal: UUID the device is derived from on browsers with the identity API - identity_baseline?: string; // Internal: derived fingerprint before edits, diffed to recover overrides + identity_baseline?: string; // Legacy: read once by the migration to identity-only storage, never written + identity_overrides?: string; // JSON object of the user's own edits to an identity-backed device + location?: string; // JSON object of the exit-derived location fields (timezone, language, coordinates) } // Wayfern fingerprint config - matches the C++ FingerprintData structure