refactor: cleanup

This commit is contained in:
zhom
2026-08-24 00:10:37 +04:00
parent c602a1ce0c
commit de88fbbafe
10 changed files with 1250 additions and 167 deletions
+42 -1
View File
@@ -217,11 +217,26 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
version: prepared.version, version: prepared.version,
configJson: JSON.stringify({ geoip: false }), configJson: JSON.stringify({ geoip: false }),
}); });
const fingerprint = JSON.parse(sample); const fingerprint = JSON.parse(sample.fingerprint);
assert.ok( assert.ok(
Object.keys(fingerprint).length >= 10, Object.keys(fingerprint).length >= 10,
"Wayfern returned an incomplete fingerprint", "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.
const identityCapable =
Number.parseInt(prepared.version.split(".")[0], 10) >= 151;
assert.equal(
typeof sample.identity_id === "string",
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( const profile = await createRealProfile(
app, app,
@@ -232,6 +247,13 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
assert.ok( assert.ok(
Object.keys(JSON.parse(profile.wayfern_config.fingerprint)).length >= 10, 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.
assert.equal(
typeof profile.wayfern_config.identity_id === "string",
identityCapable,
"a created profile must carry the identity its device came from",
);
assert.equal(await app.invoke("check_missing_geoip_database"), true); assert.equal(await app.invoke("check_missing_geoip_database"), true);
assert.equal(await app.invoke("is_geoip_database_available"), false); assert.equal(await app.invoke("is_geoip_database_available"), false);
await app.invoke("download_geoip_database"); await app.invoke("download_geoip_database");
@@ -245,6 +267,25 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
profileId: profile.id, profileId: profile.id,
exitIp: "8.8.8.8", exitIp: "8.8.8.8",
}); });
// The identity is internal state that neither call above sends back.
// Losing it would silently re-mint the device on the next launch and throw
// the user's edits away with it, so both paths must carry it forward
// unchanged.
if (identityCapable) {
const stored = (await app.invoke("list_browser_profiles")).find(
(p) => p.id === profile.id,
);
assert.equal(
stored.wayfern_config.identity_id,
profile.wayfern_config.identity_id,
"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",
);
}
// Pre-launch gate: local-only checks that must answer without starting a // Pre-launch gate: local-only checks that must answer without starting a
// proxy, an Xray worker or the browser. // proxy, an Xray worker or the browser.
const checks = await app.invoke("get_profile_pre_launch_checks", { const checks = await app.invoke("get_profile_pre_launch_checks", {
+85 -21
View File
@@ -458,38 +458,91 @@ impl BrowserRunner {
wayfern_config.proxy wayfern_config.proxy
); );
// Check if we need to generate a new fingerprint on every launch // 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.
//
// 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.
let mut updated_profile = profile.clone(); let mut updated_profile = profile.clone();
if wayfern_config.randomize_fingerprint_on_launch == Some(true) { let randomize_requested = wayfern_config.randomize_fingerprint_on_launch == Some(true);
log::info!( let needs_device = wayfern_config.fingerprint.is_none();
"Generating random fingerprint for Wayfern profile: {}", if randomize_requested || needs_device {
profile.name if needs_device && !randomize_requested {
); log::info!(
"No stored device for Wayfern profile {}; generating one",
profile.name
);
} else {
log::info!(
"Generating random fingerprint for Wayfern profile: {}",
profile.name
);
}
// Create a config copy without the existing fingerprint to force generation of a new one // Create a config copy without the existing fingerprint to force generation of a new one
let mut config_for_generation = wayfern_config.clone(); let mut config_for_generation = wayfern_config.clone();
config_for_generation.fingerprint = None; config_for_generation.fingerprint = None;
// Generate a new fingerprint // A failed generation fails the launch on purpose: continuing would
let (new_fingerprint, geolocation_applied) = self // start the browser on whatever device it drew for itself, unmanaged
// and unrecorded, while the UI still reports a successful launch. For
// an anti-detect product a silently wrong device is worse than no
// launch at all, because nothing tells the user to stop using it.
//
// Structured rather than prose, because the most common failure is the
// browser refusing a generation once the account's hourly quota is
// spent. That has to reach the user as an explanation; a raw CDP string
// is not one, and the frontend only translates a coded error.
let generated = self
.wayfern_manager .wayfern_manager
.generate_fingerprint_config(&app_handle, profile, &config_for_generation) .generate_fingerprint_config(&app_handle, profile, &config_for_generation)
.await .await
.map_err(|e| format!("Failed to generate random fingerprint: {e}"))?; .map_err(|e| {
let detail = e.to_string();
// BOTH refusal texts, because this path serves BOTH releases. 151
// says "Fingerprint generation limit reached for this account.";
// the shipped 150 browser says "Too many profiles are being
// created." Matching only the 151 wording leaves a quota-blocked
// 150 user staring at a raw CDP string, which is the exact defect
// this mapping exists to remove.
if detail.contains("generation limit reached") || detail.contains("Too many profiles") {
crate::backend_error_with_detail("WAYFERN_GENERATION_LIMIT_REACHED", detail)
} else {
crate::backend_error_with_detail("WAYFERN_FINGERPRINT_GENERATION_FAILED", detail)
}
})?;
let geolocation_applied = generated.geolocation_applied;
log::info!( log::info!(
"New fingerprint generated, length: {} chars", "New fingerprint generated, length: {} chars, identity: {:?}",
new_fingerprint.len() generated.fingerprint.len(),
generated.identity_id
); );
// Update the config with the new fingerprint for launching // Update the config with the new fingerprint for launching
wayfern_config.fingerprint = Some(new_fingerprint.clone()); wayfern_config.fingerprint = Some(generated.fingerprint.clone());
wayfern_config.identity_id = generated.identity_id.clone();
wayfern_config.identity_baseline = generated.identity_baseline.clone();
// Save the updated fingerprint to the profile so it persists. // Save the updated fingerprint to the profile so it persists.
let mut updated_wayfern_config = updated_profile.wayfern_config.clone().unwrap_or_default(); let mut updated_wayfern_config = updated_profile.wayfern_config.clone().unwrap_or_default();
updated_wayfern_config.fingerprint = Some(new_fingerprint); updated_wayfern_config.fingerprint = Some(generated.fingerprint);
updated_wayfern_config.identity_id = generated.identity_id;
updated_wayfern_config.identity_baseline = generated.identity_baseline;
// Preserve the randomize flag so it persists across launches // Preserve the randomize flag so it persists across launches
updated_wayfern_config.randomize_fingerprint_on_launch = Some(true); updated_wayfern_config.randomize_fingerprint_on_launch =
wayfern_config.randomize_fingerprint_on_launch;
// Preserve the OS setting so it's used for future fingerprint generation // Preserve the OS setting so it's used for future fingerprint generation
if wayfern_config.os.is_some() { if wayfern_config.os.is_some() {
updated_wayfern_config.os = wayfern_config.os.clone(); updated_wayfern_config.os = wayfern_config.os.clone();
@@ -601,7 +654,11 @@ impl BrowserRunner {
) )
.await .await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { .map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to launch Wayfern: {e}").into() // A refused apply reports itself as a structured error so the dialog
// can name the cause. Prefixing it would put English in front of the
// JSON the frontend parses, and the whole thing would reach the user
// as raw machine output.
crate::wrap_backend_error(e, "Failed to launch Wayfern").into()
})?; })?;
// Get the process ID from launch result // Get the process ID from launch result
@@ -653,20 +710,27 @@ impl BrowserRunner {
guard.worker_id = None; guard.worker_id = None;
} }
// Wayfern.setFingerprint echoes back the fingerprint the browser actually // The apply command echoes back the device the browser actually used,
// applied, which may be UPGRADED from the stored one (e.g. when the // which may differ from the stored one. Persist it so the next launch
// stored fingerprint targets an older browser version). Persist it so the // starts from that value — saved below via
// next launch starts from the upgraded value — saved below via
// save_process_info(&updated_profile). // save_process_info(&updated_profile).
if let Some(used_fp) = wayfern_result.used_fingerprint.clone() { if let Some(used_fp) = wayfern_result.used_fingerprint.clone() {
let mut cfg = updated_profile.wayfern_config.clone().unwrap_or_default(); let mut cfg = updated_profile.wayfern_config.clone().unwrap_or_default();
if cfg.fingerprint.as_deref() != Some(used_fp.as_str()) { 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 {
log::info!( log::info!(
"Persisting upgraded fingerprint from Wayfern.setFingerprint for profile: {} (len {})", "Persisting applied fingerprint echoed by Wayfern for profile: {} (len {})",
profile.name, profile.name,
used_fp.len() used_fp.len()
); );
cfg.fingerprint = Some(used_fp); 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);
}
updated_profile.wayfern_config = Some(cfg); updated_profile.wayfern_config = Some(cfg);
} }
} }
+20 -2
View File
@@ -1263,13 +1263,27 @@ async fn list_active_vpn_connections() -> Result<Vec<vpn::VpnStatus>, String> {
) )
} }
/// What the fingerprint form gets back from `generate_sample_fingerprint`.
///
/// The identity fields are `None` on a browser without the identity API. They
/// have to travel with the fingerprint rather than be re-derived later: the
/// form writes all three into the profile's Wayfern config in one edit, and a
/// fingerprint stored without its identity is one the launch path would throw
/// away and mint again.
#[derive(serde::Serialize)]
struct SampleFingerprint {
fingerprint: String,
identity_id: Option<String>,
identity_baseline: Option<String>,
}
#[tauri::command] #[tauri::command]
async fn generate_sample_fingerprint( async fn generate_sample_fingerprint(
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
browser: String, browser: String,
version: String, version: String,
config_json: String, config_json: String,
) -> Result<String, String> { ) -> Result<SampleFingerprint, String> {
let temp_profile = crate::profile::BrowserProfile { let temp_profile = crate::profile::BrowserProfile {
id: uuid::Uuid::new_v4(), id: uuid::Uuid::new_v4(),
name: "temp_fingerprint_gen".to_string(), name: "temp_fingerprint_gen".to_string(),
@@ -1309,7 +1323,11 @@ async fn generate_sample_fingerprint(
manager manager
.generate_fingerprint_config(&app_handle, &temp_profile, &config) .generate_fingerprint_config(&app_handle, &temp_profile, &config)
.await .await
.map(|(fingerprint, _geolocation_applied)| fingerprint) .map(|generated| SampleFingerprint {
fingerprint: generated.fingerprint,
identity_id: generated.identity_id,
identity_baseline: generated.identity_baseline,
})
.map_err(|e| format!("Failed to generate fingerprint: {e}")) .map_err(|e| format!("Failed to generate fingerprint: {e}"))
} else { } else {
Err(format!( Err(format!(
+56 -4
View File
@@ -223,9 +223,14 @@ impl ProfileManager {
.generate_fingerprint_config(app_handle, &temp_profile, &config) .generate_fingerprint_config(app_handle, &temp_profile, &config)
.await .await
{ {
Ok((generated_fingerprint, geo_applied)) => { Ok(generated) => {
config.fingerprint = Some(generated_fingerprint); config.fingerprint = Some(generated.fingerprint);
geolocation_applied = geo_applied; // 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.
config.identity_id = generated.identity_id;
config.identity_baseline = generated.identity_baseline;
geolocation_applied = generated.geolocation_applied;
log::info!("Successfully generated fingerprint for Wayfern profile: {name}"); log::info!("Successfully generated fingerprint for Wayfern profile: {name}");
} }
Err(e) => { Err(e) => {
@@ -628,6 +633,36 @@ impl ProfileManager {
return Err(format!("Browser version {version} is not downloaded").into()); return Err(format!("Browser version {version} is not downloaded").into());
} }
// A move back to a version without the identity API cannot carry an
// identity-backed device with it, and leaving it in place is worse than
// dropping it: the older browser would splice it onto a freshly drawn
// device and persist the result, which then fails on every later launch.
// Clearing `fingerprint` makes the next launch generate a fresh one;
// `geo_proxy_signature` goes with it because it certifies location fields
// of a fingerprint that no longer exists.
//
// Do NOT refuse the version change: `consolidate_browser_versions` only
// reaches this direction once the newer binary is already gone from disk,
// so refusing would strand the profile pointing at a missing executable.
// Every other direction falls through untouched.
let target_speaks_identity_api = crate::wayfern_manager::supports_identity_api(version);
if let Some(cfg) = profile
.wayfern_config
.as_mut()
.filter(|c| c.identity_id.is_some() && !target_speaks_identity_api)
{
cfg.identity_id = None;
cfg.identity_baseline = None;
cfg.fingerprint = None;
cfg.geo_proxy_signature = None;
log::warn!(
"Profile '{}' moved from Wayfern {} to {}. Its stored fingerprint cannot be used there, so it was cleared and a fresh one will be generated on the next launch.",
profile.name,
profile.version,
version
);
}
// Update version // Update version
profile.version = version.to_string(); profile.version = version.to_string();
@@ -1101,6 +1136,11 @@ impl ProfileManager {
// isolation between a clone and its source. // isolation between a clone and its source.
if let Some(cfg) = new_profile.wayfern_config.as_mut() { if let Some(cfg) = new_profile.wayfern_config.as_mut() {
cfg.fingerprint = None; cfg.fingerprint = None;
// The identity is a stronger link than the payload: the same UUID rebuilds
// the SAME device on any version, so a clone that kept it would stay
// byte-identical to its source forever, not just until the next upgrade.
cfg.identity_id = None;
cfg.identity_baseline = None;
} }
self.save_profile(&new_profile)?; self.save_profile(&new_profile)?;
@@ -1116,7 +1156,7 @@ impl ProfileManager {
&self, &self,
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
profile_id: &str, profile_id: &str,
config: WayfernConfig, mut config: WayfernConfig,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Find the profile by ID // Find the profile by ID
let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err( let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err(
@@ -1148,6 +1188,18 @@ impl ProfileManager {
); );
} }
// The identity is internal state, so a caller that edits the fingerprint
// through the API or MCP will not send it back. Dropping it would silently
// 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() {
config.identity_id = stored.identity_id.clone();
config.identity_baseline = stored.identity_baseline.clone();
}
}
// Update the Wayfern configuration // Update the Wayfern configuration
profile.wayfern_config = Some(config); profile.wayfern_config = Some(config);
+5 -1
View File
@@ -988,7 +988,11 @@ impl ProfileImporter {
{ {
// geo_proxy_signature is intentionally left unset here: the first // geo_proxy_signature is intentionally left unset here: the first
// launch's signature-mismatch refresh verifies the location either way. // launch's signature-mismatch refresh verifies the location either way.
Ok((fp, _geolocation_applied)) => config.fingerprint = Some(fp), Ok(generated) => {
config.fingerprint = Some(generated.fingerprint);
config.identity_id = generated.identity_id;
config.identity_baseline = generated.identity_baseline;
}
Err(e) => { Err(e) => {
let _ = fs::remove_dir_all(&new_profile_uuid_dir); let _ = fs::remove_dir_all(&new_profile_uuid_dir);
return Err( return Err(
File diff suppressed because it is too large Load Diff
+91 -52
View File
@@ -41,6 +41,14 @@ const isFingerprintEditingDisabled = (config: WayfernConfig): boolean => {
return config.randomize_fingerprint_on_launch === true; return config.randomize_fingerprint_on_launch === true;
}; };
/** What `generate_sample_fingerprint` returns. Identity fields are null on a
* browser version that predates the Wayfern identity API. */
interface GeneratedFingerprint {
fingerprint: string;
identity_id: string | null;
identity_baseline: string | null;
}
const getCurrentOS = (): WayfernOS => { const getCurrentOS = (): WayfernOS => {
if (typeof navigator === "undefined") return "linux"; if (typeof navigator === "undefined") return "linux";
const platform = navigator.platform.toLowerCase(); const platform = navigator.platform.toLowerCase();
@@ -83,12 +91,23 @@ export function WayfernConfigForm({
setIsGeneratingFingerprint(true); setIsGeneratingFingerprint(true);
try { try {
const configJson = JSON.stringify(config); const configJson = JSON.stringify(config);
const result = await invoke<string>("generate_sample_fingerprint", { const result = await invoke<GeneratedFingerprint>(
browser: profileBrowser ?? "wayfern", "generate_sample_fingerprint",
version: profileVersion, {
configJson, browser: profileBrowser ?? "wayfern",
}); version: profileVersion,
onConfigChange("fingerprint", result); 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.
onConfigChange("identity_id", result.identity_id ?? undefined);
onConfigChange(
"identity_baseline",
result.identity_baseline ?? undefined,
);
} catch (error) { } catch (error) {
console.error("Failed to generate fingerprint:", error); console.error("Failed to generate fingerprint:", error);
} finally { } finally {
@@ -171,6 +190,12 @@ export function WayfernConfigForm({
const isEditingDisabled = isFingerprintEditingDisabled(config) || readOnly; const isEditingDisabled = isFingerprintEditingDisabled(config) || readOnly;
/** For an identity-backed profile these fields have no stored value to show,
* and editing them by hand produces an inconsistent device. Hidden rather
* than rendered blank-and-disabled, because a blank box reads as data loss
* whereas their absence is the truth. */
const isIdentityDerived = config.identity_id != null;
const renderAdvancedForm = () => ( const renderAdvancedForm = () => (
<div className="space-y-6"> <div className="space-y-6">
{/* Operating System Selection */} {/* Operating System Selection */}
@@ -894,21 +919,23 @@ export function WayfernConfigForm({
</div> </div>
{/* WebGL Parameters (JSON) */} {/* WebGL Parameters (JSON) */}
<div className="space-y-3"> {!isIdentityDerived && (
<Label>{t("fingerprint.webglParametersJson")}</Label> <div className="space-y-3">
<Textarea <Label>{t("fingerprint.webglParametersJson")}</Label>
value={fingerprintConfig.webglParameters ?? ""} <Textarea
onChange={(e) => { value={fingerprintConfig.webglParameters ?? ""}
updateFingerprintConfig( onChange={(e) => {
"webglParameters", updateFingerprintConfig(
e.target.value || undefined, "webglParameters",
); e.target.value || undefined,
}} );
placeholder='{"7936": "Intel", "7937": "Intel(R) HD Graphics"}' }}
className="font-mono text-sm" placeholder='{"7936": "Intel", "7937": "Intel(R) HD Graphics"}'
rows={4} className="font-mono text-sm"
/> rows={4}
</div> />
</div>
)}
{/* Canvas Noise Seed */} {/* Canvas Noise Seed */}
<div className="space-y-3"> <div className="space-y-3">
@@ -1040,37 +1067,49 @@ export function WayfernConfigForm({
{/* Vendor Info */} {/* Vendor Info */}
<div className="space-y-3"> <div className="space-y-3">
<Label>{t("fingerprint.vendorInfo")}</Label> <Label>{t("fingerprint.vendorInfo")}</Label>
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3"> <div
<div className="space-y-2"> className={
<Label htmlFor="vendor">{t("fingerprint.vendor")}</Label> isIdentityDerived
<Input ? "grid grid-cols-1 gap-4"
id="vendor" : "grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3"
value={fingerprintConfig.vendor ?? ""} }
onChange={(e) => { >
updateFingerprintConfig( {!isIdentityDerived && (
"vendor", <>
e.target.value || undefined, <div className="space-y-2">
); <Label htmlFor="vendor">{t("fingerprint.vendor")}</Label>
}} <Input
placeholder={t("common.placeholders.example", { id="vendor"
value: "Google Inc.", value={fingerprintConfig.vendor ?? ""}
})} onChange={(e) => {
/> updateFingerprintConfig(
</div> "vendor",
<div className="space-y-2"> e.target.value || undefined,
<Label htmlFor="vendor-sub">{t("fingerprint.vendorSub")}</Label> );
<Input }}
id="vendor-sub" placeholder={t("common.placeholders.example", {
value={fingerprintConfig.vendorSub ?? ""} value: "Google Inc.",
onChange={(e) => { })}
updateFingerprintConfig( />
"vendorSub", </div>
e.target.value || undefined, <div className="space-y-2">
); <Label htmlFor="vendor-sub">
}} {t("fingerprint.vendorSub")}
placeholder="" </Label>
/> <Input
</div> id="vendor-sub"
value={fingerprintConfig.vendorSub ?? ""}
onChange={(e) => {
updateFingerprintConfig(
"vendorSub",
e.target.value || undefined,
);
}}
placeholder=""
/>
</div>
</>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="product-sub"> <Label htmlFor="product-sub">
{t("fingerprint.productSub")} {t("fingerprint.productSub")}
+6 -1
View File
@@ -994,6 +994,7 @@
"autoLocationDescription": "Automatically configure location information based on proxy configuration or your connection if no proxy provided", "autoLocationDescription": "Automatically configure location information based on proxy configuration or your connection if no proxy provided",
"editingDisabledRunning": "Fingerprint editing is disabled because the profile is currently running. Stop the profile to make changes.", "editingDisabledRunning": "Fingerprint editing is disabled because the profile is currently running. Stop the profile to make changes.",
"editingDisabledRandomized": "Fingerprint editing is disabled because random fingerprint generation is enabled. Disable the option above to manually edit the fingerprint configuration.", "editingDisabledRandomized": "Fingerprint editing is disabled because random fingerprint generation is enabled. Disable the option above to manually edit the fingerprint configuration.",
"derivedFromIdentity": "Derived from this profile's identity and not editable.",
"advancedWarning": "Warning: Only edit these parameters if you know what you're doing. Incorrect values may break websites, make them detect you, and lead to hard-to-debug bugs.", "advancedWarning": "Warning: Only edit these parameters if you know what you're doing. Incorrect values may break websites, make them detect you, and lead to hard-to-debug bugs.",
"basicWarning": "Warning: Only edit these parameters if you know what you're doing.", "basicWarning": "Warning: Only edit these parameters if you know what you're doing.",
"automatic": "Automatic", "automatic": "Automatic",
@@ -1977,7 +1978,11 @@
"noE2ePasswordSet": "No end-to-end encryption password is set. Set one before syncing encrypted data.", "noE2ePasswordSet": "No end-to-end encryption password is set. Set one before syncing encrypted data.",
"importSourceNotChromium": "This folder is not a Chromium browser profile", "importSourceNotChromium": "This folder is not a Chromium browser profile",
"importSourceNotChromiumNamed": "{{family}} profiles cannot be imported; only Chromium-based browsers are supported", "importSourceNotChromiumNamed": "{{family}} profiles cannot be imported; only Chromium-based browsers are supported",
"importSourceBrowserRunning": "Close {{browser}} first, or choose to import anyway" "importSourceBrowserRunning": "Close {{browser}} first, or choose to import anyway",
"wayfernFingerprintApplyFailed": "Could not apply this profile's fingerprint, so the browser was not started. {{detail}}",
"wayfernFingerprintGenerationFailed": "Could not create a fingerprint for this profile. {{detail}}",
"wayfernGenerationLimitReached": "The fingerprint generation limit for this account has been reached. New fingerprints are unavailable for up to 24 hours. This limit applies to this computer, not to one profile.",
"wayfernCrossOsRequiresPlan": "This profile claims {{detail}}, which needs a paid plan and an active sign-in. Sign in or switch the profile to your own operating system."
}, },
"rail": { "rail": {
"profiles": "Profiles", "profiles": "Profiles",
+29
View File
@@ -128,6 +128,21 @@ export type BackendErrorCode =
| "COOKIE_BOT_REQUIRES_PROXY" | "COOKIE_BOT_REQUIRES_PROXY"
| "COOKIE_BOT_TOUCH_FINGERPRINT_UNSUPPORTED" | "COOKIE_BOT_TOUCH_FINGERPRINT_UNSUPPORTED"
| "FINGERPRINT_EXIT_MISMATCH" | "FINGERPRINT_EXIT_MISMATCH"
// The launch refuses instead of opening a window on an unmanaged device: a
// silent fallback would leave the user browsing a random fingerprint while
// the UI reported success, which is the worst failure an anti-detect product
// can have. `detail` carries the underlying browser error for support.
| "WAYFERN_FINGERPRINT_APPLY_FAILED"
| "WAYFERN_FINGERPRINT_GENERATION_FAILED"
// Its own code rather than a generation failure: the browser's quota block is
// account-wide and lasts 24 hours, so "try again" is wrong advice, and a user
// whose every profile refuses at once has to be told this is one limit and
// not a broken install.
| "WAYFERN_GENERATION_LIMIT_REACHED"
// A cross-OS claim needs a signed plan token, so an expired or offline
// session cannot apply one. Distinct from the generic apply failure because
// signing in again is the fix; `detail` names the claimed OS.
| "WAYFERN_CROSS_OS_REQUIRES_PLAN"
| "LAUNCH_CONSENT_EXPIRED" | "LAUNCH_CONSENT_EXPIRED"
| "VPN_WORKER_START_FAILED" | "VPN_WORKER_START_FAILED"
| "EXIT_PROBE_FAILED" | "EXIT_PROBE_FAILED"
@@ -472,6 +487,20 @@ export function translateBackendError(t: TFunction, err: unknown): string {
// room for one sentence. // room for one sentence.
case "FINGERPRINT_EXIT_MISMATCH": case "FINGERPRINT_EXIT_MISMATCH":
return t("backendErrors.fingerprintExitMismatch"); return t("backendErrors.fingerprintExitMismatch");
case "WAYFERN_FINGERPRINT_APPLY_FAILED":
return t("backendErrors.wayfernFingerprintApplyFailed", {
detail: parsed.params?.detail ?? "",
});
case "WAYFERN_FINGERPRINT_GENERATION_FAILED":
return t("backendErrors.wayfernFingerprintGenerationFailed", {
detail: parsed.params?.detail ?? "",
});
case "WAYFERN_GENERATION_LIMIT_REACHED":
return t("backendErrors.wayfernGenerationLimitReached");
case "WAYFERN_CROSS_OS_REQUIRES_PLAN":
return t("backendErrors.wayfernCrossOsRequiresPlan", {
detail: parsed.params?.detail ?? "",
});
case "LAUNCH_CONSENT_EXPIRED": case "LAUNCH_CONSENT_EXPIRED":
return t("backendErrors.launchConsentExpired"); return t("backendErrors.launchConsentExpired");
case "VPN_WORKER_START_FAILED": case "VPN_WORKER_START_FAILED":
+2
View File
@@ -395,6 +395,8 @@ export interface WayfernConfig {
randomize_fingerprint_on_launch?: boolean; // Generate new fingerprint on every launch randomize_fingerprint_on_launch?: boolean; // Generate new fingerprint on every launch
os?: WayfernOS; // Operating system for fingerprint generation os?: WayfernOS; // Operating system for fingerprint generation
geo_proxy_signature?: string; // Internal: routing the fingerprint's location was computed for 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
} }
// Wayfern fingerprint config - matches the C++ FingerprintData structure // Wayfern fingerprint config - matches the C++ FingerprintData structure