mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-28 05:40:25 +02:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b4f657e15 | ||
|
|
405e11eeb6 | ||
|
|
90f1bf2569 | ||
|
|
33d3af0386 | ||
|
|
de88fbbafe | ||
|
|
4e1ec4b882 | ||
|
|
c602a1ce0c | ||
|
|
0b1b05c1db | ||
|
|
634511d0b0 | ||
|
|
1ca8ea3691 | ||
|
|
5d77c5dd61 |
@@ -1,29 +0,0 @@
|
||||
name: Contributors
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
contrib-readme-job:
|
||||
if: github.repository == 'zhom/donutbrowser'
|
||||
runs-on: ubuntu-latest
|
||||
name: Automatically update the contributors list in the README
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
|
||||
- name: Contribute List
|
||||
uses: akhilmhdh/contributors-readme-action@83ea0b4f1ac928fbfe88b9e8460a932a528eb79f #v2.3.11
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,6 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## v0.29.5 (2026-08-16)
|
||||
|
||||
### Features
|
||||
|
||||
- extension export via api
|
||||
|
||||
### Refactoring
|
||||
|
||||
- better proxy clipboard autofill
|
||||
- improve ephemeral ux
|
||||
- store logs and window state inside portable build
|
||||
|
||||
### Maintenance
|
||||
|
||||
- chore: version bump
|
||||
- chore: update flake.nix for v0.29.4 [skip ci] (#561)
|
||||
|
||||
|
||||
## v0.29.4 (2026-08-15)
|
||||
|
||||
### Refactoring
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
| | Apple Silicon | Intel |
|
||||
|---|---|---|
|
||||
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut_0.29.4_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut_0.29.4_x64.dmg) |
|
||||
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut_0.29.5_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut_0.29.5_x64.dmg) |
|
||||
|
||||
Or install via Homebrew:
|
||||
|
||||
@@ -56,15 +56,15 @@ brew install --cask donut
|
||||
|
||||
### Windows
|
||||
|
||||
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut_0.29.4_x64-setup.exe) · [Portable (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut_0.29.4_x64-portable.zip)
|
||||
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut_0.29.5_x64-setup.exe) · [Portable (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut_0.29.5_x64-portable.zip)
|
||||
|
||||
### Linux
|
||||
|
||||
| Format | x86_64 | ARM64 |
|
||||
|---|---|---|
|
||||
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut_0.29.4_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut_0.29.4_arm64.deb) |
|
||||
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut-0.29.4-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut-0.29.4-1.aarch64.rpm) |
|
||||
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut_0.29.4_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut_0.29.4_aarch64.AppImage) |
|
||||
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut_0.29.5_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut_0.29.5_arm64.deb) |
|
||||
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut-0.29.5-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut-0.29.5-1.aarch64.rpm) |
|
||||
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut_0.29.5_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut_0.29.5_aarch64.AppImage) |
|
||||
<!-- install-links-end -->
|
||||
|
||||
Or install via package manager:
|
||||
@@ -109,103 +109,21 @@ Donut Browser is built by the people who use it, and plenty of the most useful h
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/?repos=zhom%2Fdonutbrowser&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=zhom/donutbrowser&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=zhom/donutbrowser&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=zhom/donutbrowser&type=date&legend=top-left" />
|
||||
</picture>
|
||||
<a href="https://gitdebt.com/zhom/donutbrowser?ref=readme">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.gitdebt.com/api/repos/zhom/donutbrowser/chart.svg?theme=dark&animate=1" />
|
||||
<img alt="Cumulative GitHub stars for zhom/donutbrowser over time" src="https://api.gitdebt.com/api/repos/zhom/donutbrowser/chart.svg?theme=light&animate=1" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contributors
|
||||
|
||||
<!-- readme: collaborators,contributors -start -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/zhom">
|
||||
<img src="https://avatars.githubusercontent.com/u/2717306?v=4" width="100;" alt="zhom"/>
|
||||
<br />
|
||||
<sub><b>zhom</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/kalinichaa">
|
||||
<img src="https://avatars.githubusercontent.com/u/81773493?v=4" width="100;" alt="kalinichaa"/>
|
||||
<br />
|
||||
<sub><b>Alexander Kalinich</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/xenos1337">
|
||||
<img src="https://avatars.githubusercontent.com/u/66328734?v=4" width="100;" alt="xenos1337"/>
|
||||
<br />
|
||||
<sub><b>xenos</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/webees">
|
||||
<img src="https://avatars.githubusercontent.com/u/5155291?v=4" width="100;" alt="webees"/>
|
||||
<br />
|
||||
<sub><b>JockLee</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/yb403">
|
||||
<img src="https://avatars.githubusercontent.com/u/87396571?v=4" width="100;" alt="yb403"/>
|
||||
<br />
|
||||
<sub><b>yb403</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/huy97">
|
||||
<img src="https://avatars.githubusercontent.com/u/30153437?v=4" width="100;" alt="huy97"/>
|
||||
<br />
|
||||
<sub><b>Huy Le</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/drunkod">
|
||||
<img src="https://avatars.githubusercontent.com/u/9677471?v=4" width="100;" alt="drunkod"/>
|
||||
<br />
|
||||
<sub><b>drunkod</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/JorySeverijnse">
|
||||
<img src="https://avatars.githubusercontent.com/u/117462355?v=4" width="100;" alt="JorySeverijnse"/>
|
||||
<br />
|
||||
<sub><b>Jory Severijnse</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ThiagoMafra-Integrare">
|
||||
<img src="https://avatars.githubusercontent.com/u/222241596?v=4" width="100;" alt="ThiagoMafra-Integrare"/>
|
||||
<br />
|
||||
<sub><b>Thiago Mafra</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mchnkkc">
|
||||
<img src="https://avatars.githubusercontent.com/u/251900355?v=4" width="100;" alt="mchnkkc"/>
|
||||
<br />
|
||||
<sub><b>mchnkkc</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/liasica">
|
||||
<img src="https://avatars.githubusercontent.com/u/671431?v=4" width="100;" alt="liasica"/>
|
||||
<br />
|
||||
<sub><b>liasica</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tbody>
|
||||
</table>
|
||||
<!-- readme: collaborators,contributors -end -->
|
||||
<a href="https://gitdebt.com/zhom/donutbrowser?ref=readme">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.gitdebt.com/api/repos/zhom/donutbrowser/stats/contributors.svg?theme=dark&animate=1" />
|
||||
<img alt="Everyone who has landed commits in zhom/donutbrowser, ranked by commit count" src="https://api.gitdebt.com/api/repos/zhom/donutbrowser/stats/contributors.svg?theme=light&animate=1" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contact
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -1803,7 +1803,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donutbrowser"
|
||||
version = "0.29.4"
|
||||
version = "0.29.6"
|
||||
dependencies = [
|
||||
"aes 0.9.1",
|
||||
"aes-gcm 0.11.0",
|
||||
@@ -2889,9 +2889,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.15"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||
checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
|
||||
@@ -217,11 +217,26 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
|
||||
version: prepared.version,
|
||||
configJson: JSON.stringify({ geoip: false }),
|
||||
});
|
||||
const fingerprint = JSON.parse(sample);
|
||||
const fingerprint = JSON.parse(sample.fingerprint);
|
||||
assert.ok(
|
||||
Object.keys(fingerprint).length >= 10,
|
||||
"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(
|
||||
app,
|
||||
@@ -232,6 +247,13 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
|
||||
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.
|
||||
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("is_geoip_database_available"), false);
|
||||
await app.invoke("download_geoip_database");
|
||||
@@ -245,6 +267,25 @@ test("real Wayfern fingerprinting, terms, API automation, CDP, cookies, and proc
|
||||
profileId: profile.id,
|
||||
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
|
||||
// proxy, an Xray worker or the browser.
|
||||
const checks = await app.invoke("get_profile_pre_launch_checks", {
|
||||
|
||||
@@ -567,8 +567,24 @@ test("authenticated REST API serves its complete OpenAPI contract and CRUD lifec
|
||||
);
|
||||
assert.equal(assigned.response.status, 200, JSON.stringify(assigned.value));
|
||||
assert.equal(assigned.value.profile.id, launchProfile.id);
|
||||
// `ApiProfile` carries no `extension_group_id`, so the assignment can only
|
||||
// be read back through the surface the launcher itself resolves.
|
||||
// A caller that can set the assignment must be able to read it back without
|
||||
// dropping to the desktop app.
|
||||
assert.equal(
|
||||
assigned.value.profile.extension_group_id,
|
||||
launchGroupId,
|
||||
"the update response must echo the assignment it just made",
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await jsonRequest(`${base}/v1/profiles/${launchProfile.id}`, {
|
||||
token: saved.api_token,
|
||||
})
|
||||
).value.profile.extension_group_id,
|
||||
launchGroupId,
|
||||
"a fresh GET must report the assignment",
|
||||
);
|
||||
// Also read it back through the surface the launcher itself resolves, so
|
||||
// the REST field and the launch path cannot drift apart.
|
||||
const assignedGroup = () =>
|
||||
app.invoke("get_extension_group_for_profile", {
|
||||
profileId: launchProfile.id,
|
||||
@@ -610,6 +626,15 @@ test("authenticated REST API serves its complete OpenAPI contract and CRUD lifec
|
||||
200,
|
||||
);
|
||||
assert.equal(await assignedGroup(), null);
|
||||
assert.equal(
|
||||
(
|
||||
await jsonRequest(`${base}/v1/profiles/${launchProfile.id}`, {
|
||||
token: saved.api_token,
|
||||
})
|
||||
).value.profile.extension_group_id,
|
||||
null,
|
||||
"clearing the assignment must be visible over REST too",
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await jsonRequest(`${base}/v1/extension-groups/${launchGroupId}`, {
|
||||
|
||||
@@ -96,17 +96,17 @@
|
||||
pkgConfigPath = lib.makeSearchPath "lib/pkgconfig" (
|
||||
pkgConfigLibs ++ map lib.getDev pkgConfigLibs
|
||||
);
|
||||
releaseVersion = "0.29.4";
|
||||
releaseVersion = "0.29.5";
|
||||
releaseAppImage =
|
||||
if system == "x86_64-linux" then
|
||||
pkgs.fetchurl {
|
||||
url = "https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut_0.29.4_amd64.AppImage";
|
||||
hash = "sha256-Y9HpvwsVXQ4gg4vUMPsjtLEd4Yu8sLK/W09mngtPajI=";
|
||||
url = "https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut_0.29.5_amd64.AppImage";
|
||||
hash = "sha256-/zOFGaHTk4S53DLZe2BXmILe/P1dlb9dT94hyvEqXcM=";
|
||||
}
|
||||
else if system == "aarch64-linux" then
|
||||
pkgs.fetchurl {
|
||||
url = "https://github.com/zhom/donutbrowser/releases/download/v0.29.4/Donut_0.29.4_aarch64.AppImage";
|
||||
hash = "sha256-duw3lDRLH7rYhOWn5SKn1bevsUIDLmXEs+wnyeiNt1c=";
|
||||
url = "https://github.com/zhom/donutbrowser/releases/download/v0.29.5/Donut_0.29.5_aarch64.AppImage";
|
||||
hash = "sha256-w2xibn/GZq681vZDj9pYNOrwhnqxX0POhmBO8XMFb+c=";
|
||||
}
|
||||
else
|
||||
null;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "donutbrowser",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.29.5",
|
||||
"version": "0.29.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"predev": "pnpm licenses:generate",
|
||||
|
||||
Generated
+3
-3
@@ -1815,7 +1815,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donutbrowser"
|
||||
version = "0.29.5"
|
||||
version = "0.29.6"
|
||||
dependencies = [
|
||||
"aes 0.9.1",
|
||||
"aes-gcm 0.11.0",
|
||||
@@ -2827,9 +2827,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.15"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||
checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "donutbrowser"
|
||||
version = "0.29.5"
|
||||
version = "0.29.6"
|
||||
description = "Simple Yet Powerful Anti-Detect Browser"
|
||||
authors = ["zhom@github"]
|
||||
edition = "2021"
|
||||
|
||||
+1
-1
@@ -188,7 +188,7 @@ fn generate_tray_icons() {
|
||||
// macOS will automatically handle light/dark mode by inverting the icon
|
||||
// For template icons: RGB should be 0,0,0 (black) and alpha controls visibility
|
||||
let data = pixmap.data_mut();
|
||||
for pixel in data.chunks_exact_mut(4) {
|
||||
for pixel in data.as_chunks_mut::<4>().0 {
|
||||
// Keep the original alpha (shows where icon content is)
|
||||
// but make the color black for template icon format
|
||||
pixel[0] = 0; // R
|
||||
|
||||
@@ -458,38 +458,91 @@ impl BrowserRunner {
|
||||
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();
|
||||
if wayfern_config.randomize_fingerprint_on_launch == Some(true) {
|
||||
log::info!(
|
||||
"Generating random fingerprint for Wayfern profile: {}",
|
||||
profile.name
|
||||
);
|
||||
let randomize_requested = wayfern_config.randomize_fingerprint_on_launch == Some(true);
|
||||
let needs_device = wayfern_config.fingerprint.is_none();
|
||||
if randomize_requested || needs_device {
|
||||
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
|
||||
let mut config_for_generation = wayfern_config.clone();
|
||||
config_for_generation.fingerprint = None;
|
||||
|
||||
// Generate a new fingerprint
|
||||
let (new_fingerprint, geolocation_applied) = self
|
||||
// A failed generation fails the launch on purpose: continuing would
|
||||
// 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
|
||||
.generate_fingerprint_config(&app_handle, profile, &config_for_generation)
|
||||
.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!(
|
||||
"New fingerprint generated, length: {} chars",
|
||||
new_fingerprint.len()
|
||||
"New fingerprint generated, length: {} chars, identity: {:?}",
|
||||
generated.fingerprint.len(),
|
||||
generated.identity_id
|
||||
);
|
||||
|
||||
// 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.
|
||||
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
|
||||
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
|
||||
if wayfern_config.os.is_some() {
|
||||
updated_wayfern_config.os = wayfern_config.os.clone();
|
||||
@@ -601,7 +654,11 @@ impl BrowserRunner {
|
||||
)
|
||||
.await
|
||||
.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
|
||||
@@ -653,20 +710,27 @@ impl BrowserRunner {
|
||||
guard.worker_id = None;
|
||||
}
|
||||
|
||||
// Wayfern.setFingerprint echoes back the fingerprint the browser actually
|
||||
// applied, which may be UPGRADED from the stored one (e.g. when the
|
||||
// stored fingerprint targets an older browser version). Persist it so the
|
||||
// next launch starts from the upgraded value — saved below via
|
||||
// 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).
|
||||
if let Some(used_fp) = wayfern_result.used_fingerprint.clone() {
|
||||
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!(
|
||||
"Persisting upgraded fingerprint from Wayfern.setFingerprint for profile: {} (len {})",
|
||||
"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);
|
||||
}
|
||||
updated_profile.wayfern_config = Some(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-2
@@ -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]
|
||||
async fn generate_sample_fingerprint(
|
||||
app_handle: tauri::AppHandle,
|
||||
browser: String,
|
||||
version: String,
|
||||
config_json: String,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<SampleFingerprint, String> {
|
||||
let temp_profile = crate::profile::BrowserProfile {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: "temp_fingerprint_gen".to_string(),
|
||||
@@ -1309,7 +1323,11 @@ async fn generate_sample_fingerprint(
|
||||
manager
|
||||
.generate_fingerprint_config(&app_handle, &temp_profile, &config)
|
||||
.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}"))
|
||||
} else {
|
||||
Err(format!(
|
||||
|
||||
@@ -223,9 +223,14 @@ impl ProfileManager {
|
||||
.generate_fingerprint_config(app_handle, &temp_profile, &config)
|
||||
.await
|
||||
{
|
||||
Ok((generated_fingerprint, geo_applied)) => {
|
||||
config.fingerprint = Some(generated_fingerprint);
|
||||
geolocation_applied = geo_applied;
|
||||
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.
|
||||
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}");
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -628,6 +633,36 @@ impl ProfileManager {
|
||||
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
|
||||
profile.version = version.to_string();
|
||||
|
||||
@@ -1101,6 +1136,11 @@ impl ProfileManager {
|
||||
// isolation between a clone and its source.
|
||||
if let Some(cfg) = new_profile.wayfern_config.as_mut() {
|
||||
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)?;
|
||||
@@ -1116,7 +1156,7 @@ impl ProfileManager {
|
||||
&self,
|
||||
app_handle: tauri::AppHandle,
|
||||
profile_id: &str,
|
||||
config: WayfernConfig,
|
||||
mut config: WayfernConfig,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Find the profile by ID
|
||||
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
|
||||
profile.wayfern_config = Some(config);
|
||||
|
||||
|
||||
@@ -988,7 +988,11 @@ impl ProfileImporter {
|
||||
{
|
||||
// geo_proxy_signature is intentionally left unset here: the first
|
||||
// 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) => {
|
||||
let _ = fs::remove_dir_all(&new_profile_uuid_dir);
|
||||
return Err(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Donut",
|
||||
"version": "0.29.5",
|
||||
"version": "0.29.6",
|
||||
"identifier": "com.donutbrowser",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
|
||||
|
||||
@@ -41,6 +41,14 @@ const isFingerprintEditingDisabled = (config: WayfernConfig): boolean => {
|
||||
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 => {
|
||||
if (typeof navigator === "undefined") return "linux";
|
||||
const platform = navigator.platform.toLowerCase();
|
||||
@@ -83,12 +91,23 @@ export function WayfernConfigForm({
|
||||
setIsGeneratingFingerprint(true);
|
||||
try {
|
||||
const configJson = JSON.stringify(config);
|
||||
const result = await invoke<string>("generate_sample_fingerprint", {
|
||||
browser: profileBrowser ?? "wayfern",
|
||||
version: profileVersion,
|
||||
configJson,
|
||||
});
|
||||
onConfigChange("fingerprint", result);
|
||||
const result = await invoke<GeneratedFingerprint>(
|
||||
"generate_sample_fingerprint",
|
||||
{
|
||||
browser: profileBrowser ?? "wayfern",
|
||||
version: profileVersion,
|
||||
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) {
|
||||
console.error("Failed to generate fingerprint:", error);
|
||||
} finally {
|
||||
@@ -171,6 +190,12 @@ export function WayfernConfigForm({
|
||||
|
||||
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 = () => (
|
||||
<div className="space-y-6">
|
||||
{/* Operating System Selection */}
|
||||
@@ -894,21 +919,23 @@ export function WayfernConfigForm({
|
||||
</div>
|
||||
|
||||
{/* WebGL Parameters (JSON) */}
|
||||
<div className="space-y-3">
|
||||
<Label>{t("fingerprint.webglParametersJson")}</Label>
|
||||
<Textarea
|
||||
value={fingerprintConfig.webglParameters ?? ""}
|
||||
onChange={(e) => {
|
||||
updateFingerprintConfig(
|
||||
"webglParameters",
|
||||
e.target.value || undefined,
|
||||
);
|
||||
}}
|
||||
placeholder='{"7936": "Intel", "7937": "Intel(R) HD Graphics"}'
|
||||
className="font-mono text-sm"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
{!isIdentityDerived && (
|
||||
<div className="space-y-3">
|
||||
<Label>{t("fingerprint.webglParametersJson")}</Label>
|
||||
<Textarea
|
||||
value={fingerprintConfig.webglParameters ?? ""}
|
||||
onChange={(e) => {
|
||||
updateFingerprintConfig(
|
||||
"webglParameters",
|
||||
e.target.value || undefined,
|
||||
);
|
||||
}}
|
||||
placeholder='{"7936": "Intel", "7937": "Intel(R) HD Graphics"}'
|
||||
className="font-mono text-sm"
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Canvas Noise Seed */}
|
||||
<div className="space-y-3">
|
||||
@@ -1040,37 +1067,49 @@ export function WayfernConfigForm({
|
||||
{/* Vendor Info */}
|
||||
<div className="space-y-3">
|
||||
<Label>{t("fingerprint.vendorInfo")}</Label>
|
||||
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="vendor">{t("fingerprint.vendor")}</Label>
|
||||
<Input
|
||||
id="vendor"
|
||||
value={fingerprintConfig.vendor ?? ""}
|
||||
onChange={(e) => {
|
||||
updateFingerprintConfig(
|
||||
"vendor",
|
||||
e.target.value || undefined,
|
||||
);
|
||||
}}
|
||||
placeholder={t("common.placeholders.example", {
|
||||
value: "Google Inc.",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="vendor-sub">{t("fingerprint.vendorSub")}</Label>
|
||||
<Input
|
||||
id="vendor-sub"
|
||||
value={fingerprintConfig.vendorSub ?? ""}
|
||||
onChange={(e) => {
|
||||
updateFingerprintConfig(
|
||||
"vendorSub",
|
||||
e.target.value || undefined,
|
||||
);
|
||||
}}
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
isIdentityDerived
|
||||
? "grid grid-cols-1 gap-4"
|
||||
: "grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3"
|
||||
}
|
||||
>
|
||||
{!isIdentityDerived && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="vendor">{t("fingerprint.vendor")}</Label>
|
||||
<Input
|
||||
id="vendor"
|
||||
value={fingerprintConfig.vendor ?? ""}
|
||||
onChange={(e) => {
|
||||
updateFingerprintConfig(
|
||||
"vendor",
|
||||
e.target.value || undefined,
|
||||
);
|
||||
}}
|
||||
placeholder={t("common.placeholders.example", {
|
||||
value: "Google Inc.",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="vendor-sub">
|
||||
{t("fingerprint.vendorSub")}
|
||||
</Label>
|
||||
<Input
|
||||
id="vendor-sub"
|
||||
value={fingerprintConfig.vendorSub ?? ""}
|
||||
onChange={(e) => {
|
||||
updateFingerprintConfig(
|
||||
"vendorSub",
|
||||
e.target.value || undefined,
|
||||
);
|
||||
}}
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="product-sub">
|
||||
{t("fingerprint.productSub")}
|
||||
|
||||
@@ -994,6 +994,7 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"basicWarning": "Warning: Only edit these parameters if you know what you're doing.",
|
||||
"automatic": "Automatic",
|
||||
@@ -1977,7 +1978,11 @@
|
||||
"noE2ePasswordSet": "No end-to-end encryption password is set. Set one before syncing encrypted data.",
|
||||
"importSourceNotChromium": "This folder is not a Chromium browser profile",
|
||||
"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": {
|
||||
"profiles": "Profiles",
|
||||
|
||||
@@ -128,6 +128,21 @@ export type BackendErrorCode =
|
||||
| "COOKIE_BOT_REQUIRES_PROXY"
|
||||
| "COOKIE_BOT_TOUCH_FINGERPRINT_UNSUPPORTED"
|
||||
| "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"
|
||||
| "VPN_WORKER_START_FAILED"
|
||||
| "EXIT_PROBE_FAILED"
|
||||
@@ -472,6 +487,20 @@ export function translateBackendError(t: TFunction, err: unknown): string {
|
||||
// room for one sentence.
|
||||
case "FINGERPRINT_EXIT_MISMATCH":
|
||||
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":
|
||||
return t("backendErrors.launchConsentExpired");
|
||||
case "VPN_WORKER_START_FAILED":
|
||||
|
||||
@@ -395,6 +395,8 @@ export interface WayfernConfig {
|
||||
randomize_fingerprint_on_launch?: boolean; // Generate new fingerprint on every launch
|
||||
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
|
||||
}
|
||||
|
||||
// Wayfern fingerprint config - matches the C++ FingerprintData structure
|
||||
|
||||
Reference in New Issue
Block a user