refactor: cleanup

This commit is contained in:
zhom
2026-09-08 07:04:14 +04:00
parent c417c669c4
commit 598d3bd513
11 changed files with 250 additions and 253 deletions
+20 -16
View File
@@ -223,8 +223,8 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
"Wayfern returned an incomplete fingerprint",
);
// A browser with the identity API must hand back the UUID the device was
// derived from, plus the pre-edit baseline the launch path diffs against.
// Without both, the profile stores a device it cannot reproduce.
// derived from. Without it the profile cannot reproduce the device, since
// it stores none.
const identityCapable =
Number.parseInt(prepared.version.split(".")[0], 10) >= 151;
assert.equal(
@@ -232,28 +232,32 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
identityCapable,
"identity_id must be present exactly on browsers with the identity API",
);
assert.equal(
typeof sample.identity_baseline === "string",
identityCapable,
"identity_baseline must be present exactly on browsers with the identity API",
);
const profile = await createRealProfile(
app,
prepared.version,
`Real Wayfern (${prepared.source})`,
);
assert.ok(profile.wayfern_config.fingerprint);
assert.ok(
Object.keys(JSON.parse(profile.wayfern_config.fingerprint)).length >= 10,
);
// Profile creation stores the identity alongside the device it derived, or
// the launch path would treat the profile as un-migrated and replace it.
// An identity-backed profile stores the identity and never the device: the
// browser rebuilds the device from the id on every launch. A browser
// without the identity API has nowhere to put an id, so there the payload
// is still what gets stored.
assert.equal(
typeof profile.wayfern_config.identity_id === "string",
identityCapable,
"a created profile must carry the identity its device came from",
);
assert.equal(
profile.wayfern_config.fingerprint === undefined,
identityCapable,
"an identity-backed profile must store no device payload",
);
if (!identityCapable) {
assert.ok(
Object.keys(JSON.parse(profile.wayfern_config.fingerprint)).length >=
10,
);
}
assert.equal(await app.invoke("check_missing_geoip_database"), true);
assert.equal(await app.invoke("is_geoip_database_available"), false);
await app.invoke("download_geoip_database");
@@ -281,9 +285,9 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
"the identity must survive update_wayfern_config and an exit re-match",
);
assert.equal(
stored.wayfern_config.identity_baseline,
profile.wayfern_config.identity_baseline,
"the baseline must survive with the identity it describes",
stored.wayfern_config.fingerprint,
undefined,
"neither call may leave a device payload behind",
);
}
// Pre-launch gate: local-only checks that must answer without starting a
+27 -39
View File
@@ -460,23 +460,24 @@ impl BrowserRunner {
// Check if we need to generate a device for this launch.
//
// Two cases share the block: the user asked for a fresh device on every
// launch, or the profile stores none at all. The second is how a clone
// arrives here — cloning clears the fingerprint and the identity so the
// clone gets an independent device instead of the browser's default —
// and it also covers any profile that reached disk without one, which
// used to launch on whatever device the browser drew for itself.
// Three cases share the block: the user asked for a fresh device on
// every launch, the profile stores none at all, or the profile is legacy
// — a whole device payload and no identity — on a browser that speaks
// the identity API. The second is how a clone arrives here, since
// cloning clears both the payload and the identity so the clone gets an
// independent device instead of the browser's default.
//
// A profile that ALREADY stores a device keeps it across a browser
// upgrade: nothing here mints a replacement, and its stored payload is
// what the launch applies. The one thing that does replace a stored
// device is the user asking for it - `randomize_fingerprint_on_launch`,
// tested immediately below - which is a deliberate per-profile setting
// and not a consequence of the version.
// The third is the migration to identity-only storage: donutbrowser
// holds no device on disk, and a payload cannot become an identity
// locally, because only the browser mints an id and the id it mints
// derives its own device. That one-time rotation is the cost of the
// payload leaving disk, and it happens once because the minted id is
// persisted below.
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.
// A profile that stores a whole device BESIDE an identity needs no new
// device, only its payload folded into overrides and location. This runs
// before the launch reads the config, 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);
@@ -487,10 +488,18 @@ impl BrowserRunner {
);
}
let randomize_requested = wayfern_config.randomize_fingerprint_on_launch == Some(true);
let needs_device =
wayfern_config.fingerprint.is_none() && wayfern_config.identity_id.is_none();
let migrating_payload = wayfern_config.identity_id.is_none()
&& wayfern_config.fingerprint.is_some()
&& crate::wayfern_manager::supports_identity_api(&profile.version);
let needs_device = migrating_payload
|| (wayfern_config.fingerprint.is_none() && wayfern_config.identity_id.is_none());
if randomize_requested || needs_device {
if needs_device && !randomize_requested {
if migrating_payload && !randomize_requested {
log::info!(
"Migrating Wayfern profile {} from a stored device to an identity",
profile.name
);
} else if needs_device && !randomize_requested {
log::info!(
"No stored device for Wayfern profile {}; generating one",
profile.name
@@ -736,27 +745,6 @@ impl BrowserRunner {
guard.worker_id = None;
}
// The apply command echoes back the device the browser actually used,
// 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();
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);
cfg.identity_baseline = None;
updated_profile.wayfern_config = Some(cfg);
}
}
// Update profile with the process info
updated_profile.process_id = Some(process_id);
updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs());
+22 -8
View File
@@ -76,17 +76,31 @@ fn update(mutate: impl FnOnce(&mut LaunchGatePrefs)) {
save(&prefs);
}
/// Stable digest of a profile's stored fingerprint, so an acknowledgement stops
/// applying the moment the fingerprint is regenerated or matched to a new exit.
/// Stable digest of the device a profile publishes, so an acknowledgement
/// stops applying the moment that device is regenerated or matched to a new
/// exit.
///
/// An identity-backed profile stores no device at all: the identity, the user's
/// overrides and the exit's location are the whole of it, and the published
/// device moves exactly when one of the three does. The legacy payload is
/// hashed alongside them for a profile that has not been migrated yet.
pub fn fingerprint_hash(profile: &BrowserProfile) -> String {
use sha2::{Digest, Sha256};
let fingerprint = profile
.wayfern_config
.as_ref()
.and_then(|c| c.fingerprint.as_deref())
.unwrap_or("");
let mut hasher = Sha256::new();
hasher.update(fingerprint.as_bytes());
if let Some(config) = profile.wayfern_config.as_ref() {
for field in [
config.identity_id.as_deref(),
config.identity_overrides.as_deref(),
config.location.as_deref(),
config.fingerprint.as_deref(),
] {
// Length-prefixed, so moving a boundary between two fields cannot
// produce the digest of a different pair.
let value = field.unwrap_or("");
hasher.update(value.len().to_le_bytes());
hasher.update(value.as_bytes());
}
}
hasher
.finalize()
.iter()
+29 -1
View File
@@ -181,6 +181,21 @@ impl ProfileManager {
// behavior; for generated ones this comes from the geolocation lookup.
let mut geolocation_applied = true;
// A caller-supplied device is a set of explicit field choices, not a
// payload to store. On a browser with the identity API it becomes the
// identity's overrides and its location, and the device is minted from a
// freshly created identity below like any other profile's. A browser
// without that API has nowhere to put the choices, so there it stays the
// stored payload.
let supplied_device = if crate::wayfern_manager::supports_identity_api(version) {
config
.fingerprint
.take()
.and_then(|json| crate::wayfern_manager::WayfernManager::fingerprint_object(&json))
} else {
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() {
@@ -248,6 +263,19 @@ impl ProfileManager {
log::info!("Using provided fingerprint for Wayfern profile: {name}");
}
if let Some(object) = supplied_device {
let overrides =
crate::wayfern_manager::WayfernManager::overrides_from_explicit_fingerprint(&object);
if !overrides.is_empty() {
config.identity_overrides = serde_json::to_string(&overrides).ok();
}
// A location the caller named wins over the one resolved for the exit;
// whatever it leaves out keeps the resolved value.
if let Some(location) = crate::wayfern_manager::WayfernManager::location_of(&object) {
config.location = Some(location);
}
}
// Record which proxy/geoip the fingerprint's location data was computed
// for. On launch this is compared against the profile's current routing
// so a proxy that was changed after creation triggers a location refresh
@@ -1128,7 +1156,7 @@ impl ProfileManager {
updated_at: Some(crate::proxy_manager::now_secs()),
};
// Donut: a clone must NOT be linkable to its source. The source
// A clone must NOT be linkable to its source. The source
// wayfern_config embeds the persisted fingerprint JSON (including the
// canvas_noise_seed), so copying it verbatim makes the clone emit
// BYTE-IDENTICAL canvas/WebGL/audio readback hashes and identical device
+24
View File
@@ -923,6 +923,19 @@ impl ProfileImporter {
let final_wayfern_config = if mapped == "wayfern" {
let mut config = wayfern_config.unwrap_or_default();
// A caller-supplied device is a set of explicit field choices, not a
// payload to store: on a browser with the identity API it becomes the
// identity's overrides and its location, and the device is minted from a
// freshly created identity below.
let supplied_device = if crate::wayfern_manager::supports_identity_api(&version) {
config
.fingerprint
.take()
.and_then(|json| crate::wayfern_manager::WayfernManager::fingerprint_object(&json))
} else {
None
};
if let Some(ref proxy_id_val) = proxy_id {
if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_val) {
let proxy_url = if let (Some(username), Some(password)) =
@@ -1012,6 +1025,17 @@ impl ProfileImporter {
}
}
if let Some(object) = supplied_device {
let overrides =
crate::wayfern_manager::WayfernManager::overrides_from_explicit_fingerprint(&object);
if !overrides.is_empty() {
config.identity_overrides = serde_json::to_string(&overrides).ok();
}
if let Some(location) = crate::wayfern_manager::WayfernManager::location_of(&object) {
config.location = Some(location);
}
}
config.proxy = None;
Some(config)
} else {
+1 -1
View File
@@ -1191,7 +1191,7 @@ fn build_reqwest_client_with_proxy(
Proxy::http(upstream_url)?
}
"socks5" => {
// Donut: force REMOTE (proxy-side) DNS for plaintext HTTP over a SOCKS5
// Force REMOTE (proxy-side) DNS for plaintext HTTP over a SOCKS5
// upstream. reqwest maps the bare `socks5` scheme to DnsResolve::Local,
// which resolves the destination hostname on the HOST (getaddrinfo) BEFORE
// connecting — leaking the destination domain to the host's DNS resolver
+1 -1
View File
@@ -3522,7 +3522,7 @@ pub async fn set_profile_sync_mode(
// tokio::spawn here allowed the tombstone-write to land *after* a fast
// user-triggered re-enable's tombstone-clear, re-introducing the
// tombstone and tripping the reconcile-pass deletion of a profile the
// user had just re-enabled (e.g. Personal (z.ai) on 2026-05-20).
// user had just re-enabled.
if old_mode != SyncMode::Disabled {
match SyncEngine::create_from_settings(&app_handle).await {
Ok(engine) => {
+122 -177
View File
@@ -15,7 +15,11 @@ use tokio_tungstenite::{connect_async, tungstenite::Message};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WayfernConfig {
#[serde(default)]
/// LEGACY device payload, carried only by a profile whose browser has no
/// identity API. Every other profile is rebuilt from `identity_id`, so this
/// is read from older metadata and from a caller that supplies a whole
/// device, and is never written once the profile has an identity.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<String>,
#[serde(default)]
pub randomize_fingerprint_on_launch: Option<bool>,
@@ -55,8 +59,9 @@ pub struct WayfernConfig {
pub identity_id: Option<String>,
/// 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)]
/// stored payload. Cleared by the migration and never serialized again, so
/// a migrated profile carries no trace of it.
#[serde(default, skip_serializing_if = "Option::is_none")]
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
@@ -158,16 +163,6 @@ pub struct WayfernLaunchResult {
pub profilePath: Option<String>,
pub url: Option<String>,
pub cdp_port: Option<u16>,
/// The fingerprint the browser echoed back after applying it. It may differ
/// from what was sent, so it is this value that gets persisted. Internal
/// only — never sent to the frontend.
#[serde(default, skip_serializing)]
pub used_fingerprint: Option<String>,
/// The refreshed baseline to persist alongside `used_fingerprint`. Keeping
/// it in step is what stops an unedited field from being mistaken for a user
/// edit on the next launch. Internal only.
#[serde(default, skip_serializing)]
pub used_identity_baseline: Option<String>,
}
struct WayfernInstance {
@@ -500,34 +495,6 @@ impl WayfernManager {
params
}
/// Fill in any location field the applied device does not already carry.
///
/// `setIdentity` takes the location as its own parameters rather than inside
/// the identity, so the view it echoes back may omit part of it — and donut's
/// stored fingerprint must always carry the whole block, because the launch
/// gate reads it before any browser is running and a stored device with no
/// timezone turns the exit-vs-fingerprint check into a no-op.
///
/// Only ABSENT fields are filled. Anything the browser did send back is its
/// own and is kept: it re-roots the `languages` ladder onto the exit's
/// language, which is a better answer than the two-entry list donut computes.
fn carry_over_locale(
from: &serde_json::Map<String, serde_json::Value>,
into: &mut serde_json::Value,
) {
let Some(target) = into.as_object_mut() else {
return;
};
for key in LOCALE_CARRY_OVER_KEYS {
if target.get(key).is_some_and(|v| !v.is_null()) {
continue;
}
if let Some(value) = from.get(key) {
target.insert(key.to_string(), value.clone());
}
}
}
/// One of Wayfern's five `operatingSystem` names, or `None` for anything
/// else. Unknown names are not guessed at: the caller treats `None` as "donut
/// does not know what this profile claims" and lets the browser decide.
@@ -1468,11 +1435,6 @@ impl WayfernManager {
let page_targets: Vec<_> = targets.iter().filter(|t| t.target_type == "page").collect();
log::info!("Found {} page targets", page_targets.len());
// Apply fingerprint if configured
let mut used_fingerprint: Option<String> = None;
// 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.
@@ -1616,77 +1578,20 @@ impl WayfernManager {
// Include wayfern token if available (enables cross-OS fingerprinting for paid users)
let wayfern_token = crate::cloud_auth::CLOUD_AUTH.get_wayfern_token().await;
// The device as donut holds it: the diff source for the overrides below,
// and the fallback for any location field the echo does not return.
// The device as donut holds it, for the diagnostic below.
let stored = fingerprint_for_cdp.as_object().cloned().unwrap_or_default();
// Which command applies this profile's device. It is a property of the
// PROFILE, not of the browser version, so a profile that stores a whole
// payload keeps being applied with the payload command.
//
// `webglProfileId` is the discriminator: only a whole-payload profile
// carries it, and the browser refuses it as an override. Sending it would
// fail the call on every launch.
let apply_by_identity = supports_identity_api(&profile.version)
&& config.identity_id.is_some()
&& stored.get("webglProfileId").is_none();
// 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) =
match config.identity_id.as_deref().filter(|_| apply_by_identity) {
Some(identity_id) => {
let previous_baseline = config
.identity_baseline
.as_deref()
.and_then(Self::fingerprint_object)
.unwrap_or_default();
let overrides = Self::identity_overrides(&stored, &previous_baseline);
let mut params = serde_json::Map::new();
params.insert("identityId".to_string(), json!(identity_id));
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(&stored));
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<_>>()
);
(
"Wayfern.setIdentity",
serde_json::Value::Object(params),
previous_baseline,
overrides,
)
}
None => {
let mut params = fingerprint_for_cdp.clone();
if let Some(ref token) = wayfern_token {
if let Some(obj) = params.as_object_mut() {
obj.insert("wayfernToken".to_string(), json!(token));
}
}
(
"Wayfern.setFingerprint",
params,
serde_json::Map::new(),
serde_json::Map::new(),
)
}
};
// `setFingerprint` is the only command that reproduces a whole payload
// exactly, and on a browser without the identity API it is the only
// command there is. A profile whose browser HAS that API never reaches
// here: the launch path mints it an identity and drops the payload
// first, so a stored device is never sent as a device again.
let mut apply_params = fingerprint_for_cdp.clone();
if let Some(ref token) = wayfern_token {
if let Some(obj) = apply_params.as_object_mut() {
obj.insert("wayfernToken".to_string(), json!(token));
}
}
// An apply that never lands is the worst outcome this launch has: the
// window opens on an unmanaged device while every surface in the app
@@ -1699,45 +1604,12 @@ impl WayfernManager {
if let Some(ws_url) = &target.websocket_debugger_url {
log::info!("Applying fingerprint to page target");
match self
.send_cdp_command(ws_url, apply_method, apply_params.clone())
.send_cdp_command(ws_url, "Wayfern.setFingerprint", apply_params.clone())
.await
{
Ok(result) => {
// The device is on the target. Whether the ECHO parses is a
// separate question — it only decides what we persist.
Ok(_) => {
applied_ok = true;
log::info!("Successfully applied fingerprint to page target");
// Both commands echo back the device the browser actually used,
// which may differ from what we sent. Capture it once, from the
// first target that succeeds, so the caller can persist it.
if used_fingerprint.is_none() {
// setIdentity wraps the object as { identity: {...} },
// setFingerprint as { fingerprint: {...} }; tolerate a bare
// object too.
let applied = result
.get("identity")
.or_else(|| result.get("fingerprint"))
.cloned()
.unwrap_or(result);
if let Some(applied_obj) = applied.as_object() {
let mut persisted = applied;
if apply_by_identity {
// The location travelled as setIdentity parameters rather
// than inside the identity, so make sure it survives into
// what we store. The launch gate and the pre-launch window
// sizing both read the stored fingerprint before any
// browser is running, and a stored device with no timezone
// silently turns the exit-vs-fingerprint check into a no-op.
Self::carry_over_locale(&stored, &mut persisted);
}
match serde_json::to_string(&Self::normalize_fingerprint(persisted)) {
Ok(s) => used_fingerprint = Some(s),
Err(e) => {
log::warn!("Failed to serialize used fingerprint: {e}")
}
}
}
}
}
Err(e) => {
log::error!("Failed to apply fingerprint to target: {e}");
@@ -1829,8 +1701,6 @@ impl WayfernManager {
profilePath: Some(profile_path.to_string()),
url: url.map(|s| s.to_string()),
cdp_port: Some(port),
used_fingerprint,
used_identity_baseline,
})
}
@@ -1958,8 +1828,6 @@ impl WayfernManager {
profilePath: instance.profile_path.clone(),
url: instance.url.clone(),
cdp_port: instance.cdp_port,
used_fingerprint: None,
used_identity_baseline: None,
});
} else {
log::info!(
@@ -2002,8 +1870,6 @@ impl WayfernManager {
profilePath: Some(found_profile_path),
url: None,
cdp_port,
used_fingerprint: None,
used_identity_baseline: None,
});
}
@@ -2283,31 +2149,110 @@ mod tests {
}
#[test]
fn the_launch_echo_only_fills_location_the_browser_left_out() {
// setIdentity carries the location in its own parameters, so the applied
// view may not echo all of it back. The stored fingerprint has to keep it:
// the launch gate reads `timezone` before any browser is running, and a
// stored device without one turns that check into a no-op.
let stored = obj(
r#"{"timezone": "Europe/Berlin", "timezoneOffset": -60,
"language": "de-DE", "languages": ["de-DE", "de"]}"#,
);
// The browser returned its own, richer `languages` ladder and dropped the
// rest.
let mut applied = json!({"languages": ["de-DE", "de", "en-US", "en"]});
fn migration_moves_a_stored_payload_into_overrides_and_location() {
let mut config = WayfernConfig {
identity_id: Some("id-1".to_string()),
identity_baseline: Some(r#"{"hardwareConcurrency": 8, "platform": "Win32"}"#.to_string()),
fingerprint: Some(
r#"{"hardwareConcurrency": 16, "platform": "Win32", "timezone": "Europe/Berlin"}"#
.to_string(),
),
..Default::default()
};
WayfernManager::carry_over_locale(&stored, &mut applied);
let applied = applied.as_object().unwrap();
assert!(WayfernManager::migrate_identity_config(&mut config));
assert!(config.fingerprint.is_none());
assert!(config.identity_baseline.is_none());
assert_eq!(applied.get("timezone"), Some(&json!("Europe/Berlin")));
assert_eq!(applied.get("timezoneOffset"), Some(&json!(-60)));
assert_eq!(applied.get("language"), Some(&json!("de-DE")));
// What the browser DID return wins: it re-roots the ladder onto the exit's
// language, which is a better answer than the two-entry list donut builds.
let overrides = obj(config.identity_overrides.as_deref().unwrap());
assert_eq!(overrides.get("hardwareConcurrency"), Some(&json!(16)));
assert!(overrides.get("platform").is_none());
// Location is the one piece of device state a migrated profile keeps: it
// follows the exit, not the identity.
let location = obj(config.location.as_deref().unwrap());
assert_eq!(location.get("timezone"), Some(&json!("Europe/Berlin")));
// Running again must change nothing, because a profile is migrated on
// whichever launch reaches it first and every later launch repeats it.
let after_first = serde_json::to_string(&config).unwrap();
assert!(!WayfernManager::migrate_identity_config(&mut config));
assert_eq!(serde_json::to_string(&config).unwrap(), after_first);
}
#[test]
fn migration_is_a_no_op_for_an_already_identity_only_profile() {
let mut config = WayfernConfig {
identity_id: Some("id-1".to_string()),
identity_overrides: Some(r#"{"doNotTrack":"1"}"#.to_string()),
location: Some(r#"{"timezone":"Europe/Berlin"}"#.to_string()),
..Default::default()
};
assert!(!WayfernManager::migrate_identity_config(&mut config));
assert!(config.fingerprint.is_none());
assert_eq!(
applied.get("languages"),
Some(&json!(["de-DE", "de", "en-US", "en"]))
config.identity_overrides.as_deref(),
Some(r#"{"doNotTrack":"1"}"#)
);
assert_eq!(
config.location.as_deref(),
Some(r#"{"timezone":"Europe/Berlin"}"#)
);
}
#[test]
fn migration_leaves_a_payload_only_profile_for_the_launch_path() {
// A legacy profile has no identity for its edits to sit on, and only the
// browser can mint one. The payload stays until the launch path replaces
// it with a fresh identity, so the profile is never left with neither.
let mut config = WayfernConfig {
fingerprint: Some(r#"{"platform":"Win32"}"#.to_string()),
..Default::default()
};
assert!(!WayfernManager::migrate_identity_config(&mut config));
assert_eq!(config.fingerprint.as_deref(), Some(r#"{"platform":"Win32"}"#));
assert!(config.identity_id.is_none());
assert!(config.identity_overrides.is_none());
}
#[test]
fn migration_has_nothing_to_do_for_a_config_with_neither() {
let mut config = WayfernConfig::default();
assert!(!WayfernManager::migrate_identity_config(&mut config));
assert!(config.fingerprint.is_none());
assert!(config.identity_id.is_none());
assert!(config.identity_overrides.is_none());
assert!(config.location.is_none());
}
#[test]
fn migration_clears_a_baseline_left_behind_without_a_payload() {
let mut config = WayfernConfig {
identity_id: Some("id-1".to_string()),
identity_baseline: Some(r#"{"platform":"Win32"}"#.to_string()),
..Default::default()
};
assert!(WayfernManager::migrate_identity_config(&mut config));
assert!(config.identity_baseline.is_none());
assert!(!WayfernManager::migrate_identity_config(&mut config));
}
#[test]
fn a_migrated_config_writes_no_device_to_disk() {
let mut config = WayfernConfig {
identity_id: Some("id-1".to_string()),
fingerprint: Some(r#"{"platform":"Win32","timezone":"Europe/Berlin"}"#.to_string()),
..Default::default()
};
assert!(WayfernManager::migrate_identity_config(&mut config));
let written = serde_json::to_string(&config).unwrap();
assert!(!written.contains("\"fingerprint\""));
assert!(!written.contains("\"identity_baseline\""));
assert!(written.contains("\"location\""));
}
#[test]
-1
View File
@@ -120,7 +120,6 @@ export function WayfernConfigForm({
result.identity_id ? undefined : result.fingerprint,
);
onConfigChange("identity_overrides", undefined);
onConfigChange("identity_baseline", undefined);
} catch (error) {
console.error("Failed to generate fingerprint:", error);
} finally {
+4 -8
View File
@@ -74,14 +74,10 @@ export function useGroupEvents() {
void setupListeners();
// Cleanup listeners on unmount.
// NOTE: the previous version stored both unlisten fns by reassigning
// `groupsUnlisten` to a wrapper that called itself, which produced a
// `Maximum call stack size exceeded` crash whenever this effect tore
// down. React's reconciler then bailed out mid-commit and left stale
// overlay nodes in the DOM, blocking every subsequent click in the
// window. Holding the two unlisten fns in separate locals avoids both
// problems.
// Cleanup listeners on unmount. The two unlisten fns stay in separate
// locals: merging them into one wrapper that reassigns the local it then
// calls makes that wrapper call itself, and the stack overflow aborts the
// teardown mid-commit, leaving stale overlay nodes that swallow clicks.
return () => {
if (groupsUnlisten) groupsUnlisten();
if (profilesUnlisten) profilesUnlisten();
-1
View File
@@ -414,7 +414,6 @@ 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; // 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)
}