refactor: cleanup

This commit is contained in:
zhom
2026-09-05 07:32:03 +04:00
parent 4f655e2173
commit 7682fc6b57
12 changed files with 367 additions and 175 deletions
+38 -16
View File
@@ -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);
}
}
+28 -13
View File
@@ -146,12 +146,13 @@ fn language_matches_country(cc: &str, language: &str) -> Option<bool> {
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<String>, Option<String>) {
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::<serde_json::Value>(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() } })
+2 -2
View File
@@ -1304,7 +1304,7 @@ async fn list_active_vpn_connections() -> Result<Vec<vpn::VpnStatus>, String> {
struct SampleFingerprint {
fingerprint: String,
identity_id: Option<String>,
identity_baseline: Option<String>,
location: Option<String>,
}
#[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 {
+3
View File
@@ -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,
+46 -11
View File
@@ -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
+8 -3
View File
@@ -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);
+202 -112
View File
@@ -45,15 +45,30 @@ pub struct WayfernConfig {
/// location can be refreshed instead of showing stale data.
#[serde(default)]
pub geo_proxy_signature: Option<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
}
/// 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<String>,
pub identity_baseline: Option<String>,
/// `WayfernConfig::location` for the exit this device was generated
/// against, or `None` when no location field was resolved.
pub location: Option<String>,
/// 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<serde_json::Map<String, serde_json::Value>> {
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<String, serde_json::Value> {
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<String, serde_json::Value>,
) -> Option<String> {
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<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
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<String, serde_json::Value>,
previous_baseline: &serde_json::Map<String, serde_json::Value>,
overrides: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
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<String, serde_json::Value>,
@@ -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<String> = None;
let mut used_identity_baseline: Option<String> = 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<String> = 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::<Vec<_>>()
);
let mut applied_ok = false;
let mut last_apply_error: Option<String> = 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
+4 -1
View File
@@ -388,7 +388,10 @@ export function enableProfileSync(profileId: string): Promise<void> {
* 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;
+5 -1
View File
@@ -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);
+3 -2
View File
@@ -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"), {
+25 -13
View File
@@ -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);
}
+3 -1
View File
@@ -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