mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-09 20:54:28 +02:00
refactor: update logic and locks around vpn extensions
This commit is contained in:
+64
-26
@@ -409,9 +409,19 @@ export default function Home() {
|
||||
// a bulk run enqueues one per profile, and every waiter must settle or the
|
||||
// Promise.allSettled below it never resolves and the bulk spinner sticks.
|
||||
const gateQueueRef = useRef<
|
||||
Array<{ req: GateRequest; resolve: (decision: GateDecision) => void }>
|
||||
Array<{
|
||||
id: number;
|
||||
req: GateRequest;
|
||||
/// The bulk run this request belongs to, or undefined for a single
|
||||
/// launch. Carried per entry so a blanket "apply to the rest" can only
|
||||
/// ever claim the run its own dialog came from.
|
||||
runId: number | undefined;
|
||||
resolve: (decision: GateDecision) => void;
|
||||
}>
|
||||
>([]);
|
||||
const gateRequestSeqRef = useRef(0);
|
||||
const [gateState, setGateState] = useState<{
|
||||
id: number;
|
||||
req: GateRequest;
|
||||
remaining: number;
|
||||
} | null>(null);
|
||||
@@ -993,19 +1003,15 @@ export default function Home() {
|
||||
[selectedGroupId, t],
|
||||
);
|
||||
|
||||
// Show the queue's head, and how many are waiting behind it.
|
||||
// The backend gate downgrades to advisory rather than blocking when it
|
||||
// cannot trust its own measurement (a confirmed VPN extension can reroute
|
||||
// traffic away from the proxy it just probed), and for unattended launches.
|
||||
// Without a listener that finding was emitted into the void.
|
||||
// Unattended launches — REST and MCP automation — are the only ones the
|
||||
// backend gate lets past a measured mismatch, because there is no dialog for
|
||||
// them to answer. Without a listener that finding was emitted into the void.
|
||||
useEffect(() => {
|
||||
const unlisten = listen<ConsistencyResult>(
|
||||
"fingerprint-consistency-warning",
|
||||
(event) => {
|
||||
const { exit_timezone, fingerprint_timezone } = event.payload;
|
||||
showErrorToast(t("backendErrors.fingerprintExitMismatch"), {
|
||||
// The cause differs by path (an unverifiable measurement vs an
|
||||
// unattended launch), so state the measurement rather than guess.
|
||||
description:
|
||||
exit_timezone && fingerprint_timezone
|
||||
? t("consistencyWarning.timezoneDetail", {
|
||||
@@ -1024,11 +1030,12 @@ export default function Home() {
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
// Show the queue's head, and how many are waiting behind it.
|
||||
const syncGateUi = useCallback(() => {
|
||||
const queue = gateQueueRef.current;
|
||||
setGateState(
|
||||
queue.length > 0
|
||||
? { req: queue[0].req, remaining: queue.length - 1 }
|
||||
? { id: queue[0].id, req: queue[0].req, remaining: queue.length - 1 }
|
||||
: null,
|
||||
);
|
||||
}, []);
|
||||
@@ -1053,7 +1060,13 @@ export default function Home() {
|
||||
});
|
||||
}
|
||||
return new Promise<GateDecision>((resolve) => {
|
||||
gateQueueRef.current.push({ req, resolve });
|
||||
gateRequestSeqRef.current += 1;
|
||||
gateQueueRef.current.push({
|
||||
id: gateRequestSeqRef.current,
|
||||
req,
|
||||
runId,
|
||||
resolve,
|
||||
});
|
||||
syncGateUi();
|
||||
});
|
||||
},
|
||||
@@ -1063,24 +1076,37 @@ export default function Home() {
|
||||
const settleGate = useCallback(
|
||||
(decision: GateDecision) => {
|
||||
const entry = gateQueueRef.current.shift();
|
||||
entry?.resolve(decision);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
entry.resolve(decision);
|
||||
if (decision.applyToRemaining) {
|
||||
const coversBlocking = entry?.req.findings.fingerprint !== null;
|
||||
blanketGateDecisionRef.current = {
|
||||
decision,
|
||||
coversBlocking,
|
||||
runId: bulkRunIdRef.current,
|
||||
};
|
||||
const coversBlocking = entry.req.findings.fingerprint !== null;
|
||||
// Only a bulk run gets a standing blanket, and it claims the run the
|
||||
// answered dialog belonged to — never whichever run happens to be in
|
||||
// flight when the dialog is settled. Outside a run there is nothing to
|
||||
// scope one to, and a session-wide blanket would silently answer
|
||||
// unrelated launches later. The queue is still drained either way,
|
||||
// which is what the checkbox actually promises.
|
||||
if (entry.runId !== undefined) {
|
||||
blanketGateDecisionRef.current = {
|
||||
decision,
|
||||
coversBlocking,
|
||||
runId: entry.runId,
|
||||
};
|
||||
}
|
||||
// Drain the queue rather than leaving promises pending forever — but
|
||||
// only those the blanket actually covers. A hard block still deserves
|
||||
// its own dialog even after the user blanket-approved a warning.
|
||||
// its own dialog even after the user blanket-approved a warning, and a
|
||||
// launch started outside this run was never part of the answer.
|
||||
const remaining = gateQueueRef.current.splice(0);
|
||||
const kept = remaining.filter(
|
||||
(queued) =>
|
||||
!coversBlocking && queued.req.findings.fingerprint !== null,
|
||||
);
|
||||
const kept = [];
|
||||
for (const queued of remaining) {
|
||||
if (kept.includes(queued)) {
|
||||
const covered =
|
||||
queued.runId === entry.runId &&
|
||||
(coversBlocking || queued.req.findings.fingerprint === null);
|
||||
if (!covered) {
|
||||
kept.push(queued);
|
||||
continue;
|
||||
}
|
||||
queued.resolve({
|
||||
@@ -1167,6 +1193,12 @@ export default function Home() {
|
||||
// verdict. No network, no worker started, so a profile whose exit is
|
||||
// already known blocks before the launch touches anything.
|
||||
let consentToken: string | null = null;
|
||||
// Kept for the tier-2 dialog below: the extensions are the same ones,
|
||||
// and a mismatch measured mid-launch is exactly when knowing that one of
|
||||
// them can change the proxy matters most. Minus anything the user just
|
||||
// acknowledged, so a box they ticked seconds ago is not shown again.
|
||||
let localChecks: PreLaunchChecks | null = null;
|
||||
let ackedExtensionKeys: string[] = [];
|
||||
try {
|
||||
// One-shot migration of the old per-profile "don't warn again" flag,
|
||||
// so a user who already dismissed this profile isn't hard-blocked by
|
||||
@@ -1188,6 +1220,7 @@ export default function Home() {
|
||||
"get_profile_pre_launch_checks",
|
||||
{ profileId: profile.id },
|
||||
);
|
||||
localChecks = checks;
|
||||
const blocked =
|
||||
checks.consistency.checked && !checks.consistency.consistent;
|
||||
if (blocked || checks.vpn_extensions.length > 0) {
|
||||
@@ -1208,6 +1241,7 @@ export default function Home() {
|
||||
if (!decision.proceed) {
|
||||
return { status: "cancelled" };
|
||||
}
|
||||
ackedExtensionKeys = decision.ackExtensionKeys;
|
||||
consentToken = checks.consent_token;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1234,10 +1268,13 @@ export default function Home() {
|
||||
{
|
||||
profile,
|
||||
findings: {
|
||||
vpnExtensions: [],
|
||||
scanState: "scanned",
|
||||
vpnExtensions: (localChecks?.vpn_extensions ?? []).filter(
|
||||
(ext) => !ackedExtensionKeys.includes(ext.key),
|
||||
),
|
||||
scanState: localChecks?.scan_state ?? "scanned",
|
||||
fingerprint: consistencyFromErrorParams(parsed.params),
|
||||
measurementUnreliable: false,
|
||||
measurementUnreliable:
|
||||
localChecks?.exit_measurement_unreliable ?? false,
|
||||
probePending: false,
|
||||
},
|
||||
},
|
||||
@@ -2186,6 +2223,7 @@ export default function Home() {
|
||||
isOpen={gateState !== null}
|
||||
profileName={gateState?.req.profile.name ?? ""}
|
||||
profileId={gateState?.req.profile.id ?? ""}
|
||||
requestId={gateState?.id ?? 0}
|
||||
findings={gateState?.req.findings ?? null}
|
||||
remainingCount={gateState?.remaining ?? 0}
|
||||
onResult={settleGate}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LuTriangleAlert } from "react-icons/lu";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -15,16 +15,21 @@ import {
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { translateBackendError } from "@/lib/backend-errors";
|
||||
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||
import type { ConsistencyResult, DetectedVpnExtension } from "@/types";
|
||||
import type {
|
||||
ConsistencyResult,
|
||||
DetectedVpnExtension,
|
||||
ExtensionScanState,
|
||||
} from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
export interface GateFindings {
|
||||
/// Extensions that can reroute traffic. A warning: the user may proceed.
|
||||
/// Extensions that could reroute traffic. A warning: the user may proceed.
|
||||
vpnExtensions: DetectedVpnExtension[];
|
||||
scanState: string;
|
||||
scanState: ExtensionScanState;
|
||||
/// A measured exit/fingerprint mismatch. A block: the browser has not started.
|
||||
fingerprint: ConsistencyResult | null;
|
||||
/// A confirmed proxy-permission extension makes any exit measurement suspect.
|
||||
/// An extension holds the proxy permission, so the exit measurement may not
|
||||
/// describe the route the browser takes. A caveat on the block, not a waiver.
|
||||
measurementUnreliable: boolean;
|
||||
/// The exit has not been measured yet; the launch itself will still check.
|
||||
probePending: boolean;
|
||||
@@ -41,6 +46,9 @@ interface PreLaunchGateDialogProps {
|
||||
isOpen: boolean;
|
||||
profileName: string;
|
||||
profileId: string;
|
||||
/// Identifies this specific request, so state resets even when one gate
|
||||
/// replaces another without the dialog ever closing.
|
||||
requestId: number;
|
||||
findings: GateFindings | null;
|
||||
/// How many further profiles are queued behind this one; >0 offers to apply
|
||||
/// the same decision to all of them.
|
||||
@@ -50,51 +58,155 @@ interface PreLaunchGateDialogProps {
|
||||
onResult: (decision: GateDecision) => void;
|
||||
}
|
||||
|
||||
/// Everything the user can change while one gate is on screen, stamped with
|
||||
/// the gate it belongs to.
|
||||
interface GateAnswerState {
|
||||
requestId: number;
|
||||
ackFingerprint: boolean;
|
||||
ackExtensions: boolean;
|
||||
applyToRemaining: boolean;
|
||||
isMatching: boolean;
|
||||
decided: boolean;
|
||||
}
|
||||
|
||||
/// How long after a decision the footer stops accepting another one. Long
|
||||
/// enough that a double-click cannot answer the gate promoted by its first
|
||||
/// half, short enough that nobody deliberately answering two queued gates in a
|
||||
/// row notices it.
|
||||
const DECISION_COOLDOWN_MS = 500;
|
||||
|
||||
function answersFor(requestId: number): GateAnswerState {
|
||||
return {
|
||||
requestId,
|
||||
ackFingerprint: false,
|
||||
ackExtensions: false,
|
||||
applyToRemaining: false,
|
||||
isMatching: false,
|
||||
decided: false,
|
||||
};
|
||||
}
|
||||
|
||||
function ExtensionEntry({ extension }: { extension: DetectedVpnExtension }) {
|
||||
const { t } = useTranslation();
|
||||
const capability = t(
|
||||
extension.confidence === "confirmed"
|
||||
? "prelaunchGate.vpnExtensionConfirmed"
|
||||
: extension.confidence === "likely"
|
||||
? "prelaunchGate.vpnExtensionLikely"
|
||||
: "prelaunchGate.vpnExtensionCapability",
|
||||
);
|
||||
const source = t(
|
||||
extension.source === "donut"
|
||||
? "prelaunchGate.sourceDonut"
|
||||
: "prelaunchGate.sourceBrowser",
|
||||
);
|
||||
|
||||
return (
|
||||
<li className="text-xs">
|
||||
<span className="font-medium">{extension.name}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{/* A version-less manifest is legal, and interpolating an empty string
|
||||
into the one template left a doubled space before the dash. */}
|
||||
{extension.version
|
||||
? t("prelaunchGate.vpnExtensionEntry", {
|
||||
version: extension.version,
|
||||
capability,
|
||||
source,
|
||||
})
|
||||
: t("prelaunchGate.vpnExtensionEntryNoVersion", {
|
||||
capability,
|
||||
source,
|
||||
})}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreLaunchGateDialog({
|
||||
isOpen,
|
||||
profileName,
|
||||
profileId,
|
||||
requestId,
|
||||
findings,
|
||||
remainingCount,
|
||||
onResult,
|
||||
}: PreLaunchGateDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [ackFingerprint, setAckFingerprint] = useState(false);
|
||||
const [ackExtensions, setAckExtensions] = useState(false);
|
||||
const [applyToRemaining, setApplyToRemaining] = useState(false);
|
||||
const [isMatching, setIsMatching] = useState(false);
|
||||
// The dialog node is reused as the queue advances, so without this a double
|
||||
// click would decide for the next profile too.
|
||||
const [decided, setDecided] = useState(false);
|
||||
// All mutable state is stamped with the request it belongs to, and anything
|
||||
// stamped with an older request is ignored rather than reset. The dialog
|
||||
// never unmounts and a queued gate promotes the next profile without ever
|
||||
// closing it, so state carried across that boundary would tick a checkbox
|
||||
// for a profile the user never saw — and `decided` carried across it left
|
||||
// every button disabled on a dialog that also refused Escape, which is the
|
||||
// freeze this shape exists to make unrepresentable.
|
||||
//
|
||||
// Deliberately not an effect keyed on `requestId`: a reset effect whose body
|
||||
// reads none of its dependencies is exactly what a lint autofix reduces to
|
||||
// `[]`, and that is how the freeze shipped.
|
||||
const [state, setState] = useState<GateAnswerState>(() => answersFor(0));
|
||||
const answers = state.requestId === requestId ? state : answersFor(requestId);
|
||||
|
||||
// Keyed on profileId, not just isOpen: a queued gate promotes the next
|
||||
// profile without ever closing the dialog, so an isOpen-only reset would
|
||||
// carry the previous profile's ticked boxes — and persist an acknowledgement
|
||||
// against a profile the user never saw.
|
||||
// The gate on screen right now, readable from an async callback whose
|
||||
// closure was captured while an earlier gate was showing.
|
||||
const liveRequestRef = useRef(requestId);
|
||||
useEffect(() => {
|
||||
setAckFingerprint(false);
|
||||
setAckExtensions(false);
|
||||
setIsMatching(false);
|
||||
setDecided(false);
|
||||
}, []);
|
||||
liveRequestRef.current = requestId;
|
||||
}, [requestId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setApplyToRemaining(false);
|
||||
const patch = (next: Partial<GateAnswerState>) => {
|
||||
// A callback that resumes after its gate was answered must not write into
|
||||
// the slot the next gate is now using — that would silently untick boxes
|
||||
// the user has since ticked on a different profile.
|
||||
if (liveRequestRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
}, [isOpen]);
|
||||
setState((prev) => ({
|
||||
...(prev.requestId === requestId ? prev : answersFor(requestId)),
|
||||
...next,
|
||||
requestId,
|
||||
}));
|
||||
};
|
||||
const {
|
||||
ackFingerprint,
|
||||
ackExtensions,
|
||||
applyToRemaining,
|
||||
isMatching,
|
||||
decided,
|
||||
} = answers;
|
||||
|
||||
const fingerprint = findings?.fingerprint ?? null;
|
||||
const extensions = findings?.vpnExtensions ?? [];
|
||||
// Two different claims, kept visually apart. The first names extensions as
|
||||
// VPN/proxy tools; the second says only that an extension holds Chromium's
|
||||
// proxy permission, which a download manager needs to route its own
|
||||
// transfers and which says nothing about what the extension is.
|
||||
const vpnExtensions = extensions.filter((e) => e.confidence !== "capability");
|
||||
const proxyCapableExtensions = extensions.filter(
|
||||
(e) => e.confidence === "capability",
|
||||
);
|
||||
const mismatches = fingerprint?.mismatches ?? [];
|
||||
const exitIp = fingerprint?.exit_ip ?? null;
|
||||
const isBlocked = fingerprint !== null;
|
||||
|
||||
// Two guards, because answering a gate promotes the next one into the same
|
||||
// DOM node rather than closing the dialog. The ref settles one gate exactly
|
||||
// once even if two clicks land in the same React batch; the cooldown stops
|
||||
// the second half of a double-click from answering a dialog that appeared
|
||||
// between the two clicks and that nobody has read.
|
||||
const decidedRef = useRef<number | null>(null);
|
||||
const lastDecisionAtRef = useRef(Number.NEGATIVE_INFINITY);
|
||||
|
||||
const decide = (proceed: boolean) => {
|
||||
if (decided) {
|
||||
if (decided || decidedRef.current === requestId) {
|
||||
return;
|
||||
}
|
||||
setDecided(true);
|
||||
const now = performance.now();
|
||||
if (now - lastDecisionAtRef.current < DECISION_COOLDOWN_MS) {
|
||||
return;
|
||||
}
|
||||
decidedRef.current = requestId;
|
||||
lastDecisionAtRef.current = now;
|
||||
patch({ decided: true });
|
||||
onResult({
|
||||
proceed,
|
||||
ackFingerprint: ackFingerprint && isBlocked,
|
||||
@@ -107,21 +219,29 @@ export function PreLaunchGateDialog({
|
||||
if (!exitIp) {
|
||||
return;
|
||||
}
|
||||
setIsMatching(true);
|
||||
const request = requestId;
|
||||
patch({ isMatching: true });
|
||||
try {
|
||||
await invoke("match_profile_fingerprint_to_exit", {
|
||||
profileId,
|
||||
exitIp,
|
||||
});
|
||||
showSuccessToast(t("consistencyWarning.matchSuccess"));
|
||||
patch({ isMatching: false });
|
||||
// Rewriting the fingerprint takes long enough for the user to dismiss
|
||||
// this gate meanwhile. The profile change still stands, but the launch
|
||||
// it belonged to is already settled, and deciding now would answer
|
||||
// whichever gate took its place.
|
||||
if (liveRequestRef.current !== request) {
|
||||
return;
|
||||
}
|
||||
// The fingerprint the block was measured against no longer exists, so
|
||||
// this launch is abandoned rather than forced through with a stale
|
||||
// consent token; the user relaunches against the corrected profile.
|
||||
decide(false);
|
||||
} catch (e) {
|
||||
showErrorToast(translateBackendError(t, e));
|
||||
} finally {
|
||||
setIsMatching(false);
|
||||
patch({ isMatching: false });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -141,8 +261,19 @@ export function PreLaunchGateDialog({
|
||||
})();
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen}>
|
||||
<DialogContent className="sm:max-w-md" dismissible={false}>
|
||||
// Dismissible on purpose: cancelling is the safe outcome, so every way out
|
||||
// of this dialog — Escape, the close X, a click outside — resolves the
|
||||
// waiting launch as "don't start". A gate that can only be answered by two
|
||||
// buttons is one disabled button away from trapping the whole app.
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
decide(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<LuTriangleAlert className="size-5 text-warning-text" />
|
||||
@@ -184,7 +315,7 @@ export function PreLaunchGateDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{extensions.length > 0 && (
|
||||
{vpnExtensions.length > 0 && (
|
||||
<div className="space-y-2 rounded-md border border-warning/50 bg-warning/10 p-3">
|
||||
<p className="font-medium">
|
||||
{t("prelaunchGate.vpnExtensionHeading")}
|
||||
@@ -193,23 +324,8 @@ export function PreLaunchGateDialog({
|
||||
{t("prelaunchGate.vpnExtensionIntro")}
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{extensions.map((ext) => (
|
||||
<li key={ext.key} className="text-xs">
|
||||
<span className="font-medium">{ext.name}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{t("prelaunchGate.vpnExtensionEntry", {
|
||||
version: ext.version ?? "",
|
||||
capability:
|
||||
ext.confidence === "confirmed"
|
||||
? t("prelaunchGate.vpnExtensionConfirmed")
|
||||
: t("prelaunchGate.vpnExtensionLikely"),
|
||||
source:
|
||||
ext.source === "donut"
|
||||
? t("prelaunchGate.sourceDonut")
|
||||
: t("prelaunchGate.sourceBrowser"),
|
||||
})}
|
||||
</span>
|
||||
</li>
|
||||
{vpnExtensions.map((ext) => (
|
||||
<ExtensionEntry key={ext.key} extension={ext} />
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -218,6 +334,22 @@ export function PreLaunchGateDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proxyCapableExtensions.length > 0 && (
|
||||
<div className="space-y-2 rounded-md border border-border bg-muted/40 p-3">
|
||||
<p className="font-medium">
|
||||
{t("prelaunchGate.proxyCapableHeading")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("prelaunchGate.proxyCapableIntro")}
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{proxyCapableExtensions.map((ext) => (
|
||||
<ExtensionEntry key={ext.key} extension={ext} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{findings?.measurementUnreliable && isBlocked && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("prelaunchGate.measurementUnreliable")}
|
||||
@@ -240,7 +372,7 @@ export function PreLaunchGateDialog({
|
||||
<Checkbox
|
||||
id="gate-ack-fingerprint"
|
||||
checked={ackFingerprint}
|
||||
onCheckedChange={(v) => setAckFingerprint(v === true)}
|
||||
onCheckedChange={(v) => patch({ ackFingerprint: v === true })}
|
||||
/>
|
||||
<Label htmlFor="gate-ack-fingerprint" className="text-xs">
|
||||
{t("prelaunchGate.dontBlockAgain")}
|
||||
@@ -252,7 +384,7 @@ export function PreLaunchGateDialog({
|
||||
<Checkbox
|
||||
id="gate-ack-extensions"
|
||||
checked={ackExtensions}
|
||||
onCheckedChange={(v) => setAckExtensions(v === true)}
|
||||
onCheckedChange={(v) => patch({ ackExtensions: v === true })}
|
||||
/>
|
||||
<Label htmlFor="gate-ack-extensions" className="text-xs">
|
||||
{t("prelaunchGate.dontWarnExtensions")}
|
||||
@@ -264,7 +396,9 @@ export function PreLaunchGateDialog({
|
||||
<Checkbox
|
||||
id="gate-apply-remaining"
|
||||
checked={applyToRemaining}
|
||||
onCheckedChange={(v) => setApplyToRemaining(v === true)}
|
||||
onCheckedChange={(v) =>
|
||||
patch({ applyToRemaining: v === true })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="gate-apply-remaining" className="text-xs">
|
||||
{t("prelaunchGate.applyToRemaining")}
|
||||
@@ -276,11 +410,14 @@ export function PreLaunchGateDialog({
|
||||
|
||||
<DialogFooter className="flex-row justify-between sm:justify-between">
|
||||
{/* Cancel is the default action: the browser has not started, and
|
||||
not starting it is the safe outcome. */}
|
||||
not starting it is the safe outcome. Never disabled by `decided`
|
||||
— `decide` is already idempotent, and the one control that ends
|
||||
the dialog safely must not be something a stale flag can switch
|
||||
off. */}
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
onClick={() => decide(false)}
|
||||
disabled={isMatching || decided}
|
||||
disabled={isMatching}
|
||||
autoFocus
|
||||
>
|
||||
{t("common.buttons.cancel")}
|
||||
|
||||
@@ -2488,15 +2488,18 @@
|
||||
"titleBlocked": "Launch blocked",
|
||||
"titleWarning": "Before you launch",
|
||||
"intro": "Review these issues with \"{{name}}\" before starting the browser.",
|
||||
"fingerprintHeading": "Proxy exit doesn't match the fingerprint",
|
||||
"vpnExtensionHeading": "VPN extension detected",
|
||||
"fingerprintHeading": "The measured exit doesn't match the fingerprint",
|
||||
"vpnExtensionHeading": "VPN or proxy extension detected",
|
||||
"vpnExtensionIntro": "Extensions in this profile that can reroute the browser's traffic:",
|
||||
"vpnExtensionConfirmed": "Can change the proxy",
|
||||
"vpnExtensionLikely": "May change the proxy",
|
||||
"vpnExtensionConfirmed": "Known VPN or proxy tool",
|
||||
"vpnExtensionLikely": "Looks like a VPN or proxy tool",
|
||||
"vpnExtensionCapability": "Holds the proxy permission",
|
||||
"vpnExtensionExplainer": "If one of these routes your traffic elsewhere, the browser's real location will no longer match the timezone, language and geolocation this profile was created with, and Donut cannot detect that from the outside.",
|
||||
"proxyCapableHeading": "Extensions that can change the proxy",
|
||||
"proxyCapableIntro": "These don't look like VPNs, but they hold Chromium's proxy permission, which download managers and debugging tools need too. Donut cannot tell whether any of them is using it:",
|
||||
"sourceDonut": "Managed by Donut",
|
||||
"sourceBrowser": "Installed in the profile",
|
||||
"measurementUnreliable": "Because a VPN extension can override the proxy, the exit check may not describe the route the browser actually takes.",
|
||||
"measurementUnreliable": "An extension in this profile holds the proxy permission, so the exit check may not describe the route the browser actually takes.",
|
||||
"scanIncompleteEncrypted": "This profile is encrypted, so only Donut-managed extensions could be checked.",
|
||||
"scanIncompleteEphemeral": "This profile has no data yet, so only Donut-managed extensions could be checked.",
|
||||
"scanIncompletePartial": "The extension scan was cut short, so some extensions may not be listed.",
|
||||
@@ -2506,8 +2509,8 @@
|
||||
"dontWarnExtensions": "Don't warn again about these extensions",
|
||||
"applyToRemaining": "Apply this choice to the remaining profiles",
|
||||
"cancelledSummary": "{{cancelled}} of {{total}} launches cancelled",
|
||||
"cancelled": "Launch cancelled",
|
||||
"vpnExtensionEntry": " {{version}} — {{capability}}, {{source}}",
|
||||
"vpnExtensionEntryNoVersion": " — {{capability}}, {{source}}",
|
||||
"scanIncompleteMissing": "This profile has not been launched yet, so only Donut-managed extensions could be checked."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2517,15 +2517,18 @@
|
||||
"titleBlocked": "Inicio bloqueado",
|
||||
"titleWarning": "Antes de iniciar",
|
||||
"intro": "Revisa estos problemas de \"{{name}}\" antes de iniciar el navegador.",
|
||||
"fingerprintHeading": "La salida del proxy no coincide con la huella digital",
|
||||
"vpnExtensionHeading": "Extensión VPN detectada",
|
||||
"fingerprintHeading": "La salida medida no coincide con la huella digital",
|
||||
"vpnExtensionHeading": "Extensión de VPN o proxy detectada",
|
||||
"vpnExtensionIntro": "Extensiones de este perfil que pueden redirigir el tráfico del navegador:",
|
||||
"vpnExtensionConfirmed": "Puede cambiar el proxy",
|
||||
"vpnExtensionLikely": "Podría cambiar el proxy",
|
||||
"vpnExtensionConfirmed": "Herramienta de VPN o proxy conocida",
|
||||
"vpnExtensionLikely": "Parece una herramienta de VPN o proxy",
|
||||
"vpnExtensionCapability": "Tiene el permiso de proxy",
|
||||
"vpnExtensionExplainer": "Si alguna de ellas redirige tu tráfico a otro lugar, la ubicación real del navegador dejará de coincidir con la zona horaria, el idioma y la geolocalización con los que se creó este perfil, y Donut no puede detectarlo desde fuera.",
|
||||
"proxyCapableHeading": "Extensiones que pueden cambiar el proxy",
|
||||
"proxyCapableIntro": "No parecen VPN, pero tienen el permiso de proxy de Chromium, que también necesitan los gestores de descargas y las herramientas de depuración. Donut no puede saber si alguna lo está usando:",
|
||||
"sourceDonut": "Gestionada por Donut",
|
||||
"sourceBrowser": "Instalada en el perfil",
|
||||
"measurementUnreliable": "Como una extensión VPN puede anular el proxy, la comprobación de salida podría no reflejar la ruta que el navegador usa realmente.",
|
||||
"measurementUnreliable": "Una extensión de este perfil tiene el permiso de proxy, así que la comprobación de salida podría no reflejar la ruta que el navegador usa realmente.",
|
||||
"scanIncompleteEncrypted": "Este perfil está cifrado, así que solo se pudieron comprobar las extensiones gestionadas por Donut.",
|
||||
"scanIncompleteEphemeral": "Este perfil aún no tiene datos, así que solo se pudieron comprobar las extensiones gestionadas por Donut.",
|
||||
"scanIncompletePartial": "El análisis de extensiones se interrumpió, así que puede que falten algunas.",
|
||||
@@ -2535,8 +2538,8 @@
|
||||
"dontWarnExtensions": "No volver a avisar sobre estas extensiones",
|
||||
"applyToRemaining": "Aplicar esta decisión a los perfiles restantes",
|
||||
"cancelledSummary": "{{cancelled}} de {{total}} inicios cancelados",
|
||||
"cancelled": "Inicio cancelado",
|
||||
"vpnExtensionEntry": " {{version}} — {{capability}}, {{source}}",
|
||||
"vpnExtensionEntryNoVersion": " — {{capability}}, {{source}}",
|
||||
"scanIncompleteMissing": "Este perfil aún no se ha iniciado, así que solo se pudieron comprobar las extensiones gestionadas por Donut."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2517,15 +2517,18 @@
|
||||
"titleBlocked": "Lancement bloqué",
|
||||
"titleWarning": "Avant de lancer",
|
||||
"intro": "Examinez ces problèmes concernant « {{name}} » avant de démarrer le navigateur.",
|
||||
"fingerprintHeading": "La sortie du proxy ne correspond pas à l'empreinte",
|
||||
"vpnExtensionHeading": "Extension VPN détectée",
|
||||
"fingerprintHeading": "La sortie mesurée ne correspond pas à l'empreinte",
|
||||
"vpnExtensionHeading": "Extension VPN ou proxy détectée",
|
||||
"vpnExtensionIntro": "Extensions de ce profil pouvant rerouter le trafic du navigateur :",
|
||||
"vpnExtensionConfirmed": "Peut changer le proxy",
|
||||
"vpnExtensionLikely": "Pourrait changer le proxy",
|
||||
"vpnExtensionConfirmed": "Outil VPN ou proxy connu",
|
||||
"vpnExtensionLikely": "Semble être un outil VPN ou proxy",
|
||||
"vpnExtensionCapability": "Détient l'autorisation proxy",
|
||||
"vpnExtensionExplainer": "Si l'une d'elles redirige votre trafic ailleurs, la position réelle du navigateur ne correspondra plus au fuseau horaire, à la langue et à la géolocalisation avec lesquels ce profil a été créé, et Donut ne peut pas le détecter de l'extérieur.",
|
||||
"proxyCapableHeading": "Extensions pouvant changer le proxy",
|
||||
"proxyCapableIntro": "Elles ne ressemblent pas à des VPN, mais elles détiennent l'autorisation proxy de Chromium, dont les gestionnaires de téléchargement et les outils de débogage ont aussi besoin. Donut ne peut pas savoir si l'une d'elles s'en sert :",
|
||||
"sourceDonut": "Gérée par Donut",
|
||||
"sourceBrowser": "Installée dans le profil",
|
||||
"measurementUnreliable": "Comme une extension VPN peut remplacer le proxy, la vérification de la sortie peut ne pas refléter la route réellement empruntée par le navigateur.",
|
||||
"measurementUnreliable": "Une extension de ce profil détient l'autorisation proxy, la vérification de la sortie peut donc ne pas refléter la route réellement empruntée par le navigateur.",
|
||||
"scanIncompleteEncrypted": "Ce profil est chiffré : seules les extensions gérées par Donut ont pu être vérifiées.",
|
||||
"scanIncompleteEphemeral": "Ce profil n'a pas encore de données : seules les extensions gérées par Donut ont pu être vérifiées.",
|
||||
"scanIncompletePartial": "L'analyse des extensions a été interrompue, certaines peuvent manquer.",
|
||||
@@ -2535,8 +2538,8 @@
|
||||
"dontWarnExtensions": "Ne plus m'avertir à propos de ces extensions",
|
||||
"applyToRemaining": "Appliquer ce choix aux profils restants",
|
||||
"cancelledSummary": "{{cancelled}} lancements sur {{total}} annulés",
|
||||
"cancelled": "Lancement annulé",
|
||||
"vpnExtensionEntry": " {{version}} — {{capability}}, {{source}}",
|
||||
"vpnExtensionEntryNoVersion": " — {{capability}}, {{source}}",
|
||||
"scanIncompleteMissing": "Ce profil n'a jamais été lancé : seules les extensions gérées par Donut ont pu être vérifiées."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2488,15 +2488,18 @@
|
||||
"titleBlocked": "起動をブロックしました",
|
||||
"titleWarning": "起動する前に",
|
||||
"intro": "ブラウザーを起動する前に、「{{name}}」に関する次の問題を確認してください。",
|
||||
"fingerprintHeading": "プロキシの出口がフィンガープリントと一致しません",
|
||||
"vpnExtensionHeading": "VPN拡張機能を検出しました",
|
||||
"fingerprintHeading": "測定した出口がフィンガープリントと一致しません",
|
||||
"vpnExtensionHeading": "VPN・プロキシ拡張機能を検出しました",
|
||||
"vpnExtensionIntro": "このプロファイル内で、ブラウザーの通信を経路変更できる拡張機能:",
|
||||
"vpnExtensionConfirmed": "プロキシを変更できます",
|
||||
"vpnExtensionLikely": "プロキシを変更する可能性があります",
|
||||
"vpnExtensionConfirmed": "既知のVPN・プロキシツール",
|
||||
"vpnExtensionLikely": "VPN・プロキシツールと思われます",
|
||||
"vpnExtensionCapability": "プロキシ権限を持っています",
|
||||
"vpnExtensionExplainer": "いずれかが通信を別の経路に変えると、ブラウザーの実際の所在地は、このプロファイルの作成時に設定されたタイムゾーン・言語・位置情報と一致しなくなります。Donutは外部からそれを検出できません。",
|
||||
"proxyCapableHeading": "プロキシを変更できる拡張機能",
|
||||
"proxyCapableIntro": "VPNには見えませんが、Chromiumのプロキシ権限を持っています。ダウンロードマネージャーやデバッグツールにも必要な権限で、実際に使っているかどうかをDonutは判別できません:",
|
||||
"sourceDonut": "Donutが管理",
|
||||
"sourceBrowser": "プロファイルにインストール済み",
|
||||
"measurementUnreliable": "VPN拡張機能はプロキシを上書きできるため、出口の確認結果がブラウザーの実際の経路を表していない可能性があります。",
|
||||
"measurementUnreliable": "このプロファイルの拡張機能がプロキシ権限を持っているため、出口の確認結果がブラウザーの実際の経路を表していない可能性があります。",
|
||||
"scanIncompleteEncrypted": "このプロファイルは暗号化されているため、Donutが管理する拡張機能のみ確認できました。",
|
||||
"scanIncompleteEphemeral": "このプロファイルにはまだデータがないため、Donutが管理する拡張機能のみ確認できました。",
|
||||
"scanIncompletePartial": "拡張機能のスキャンが途中で終了したため、一部が表示されていない可能性があります。",
|
||||
@@ -2506,8 +2509,8 @@
|
||||
"dontWarnExtensions": "これらの拡張機能について今後警告しない",
|
||||
"applyToRemaining": "この選択を残りのプロファイルにも適用",
|
||||
"cancelledSummary": "{{total}}件中{{cancelled}}件の起動をキャンセルしました",
|
||||
"cancelled": "起動をキャンセルしました",
|
||||
"vpnExtensionEntry": " {{version}}({{capability}}、{{source}})",
|
||||
"vpnExtensionEntryNoVersion": "({{capability}}、{{source}})",
|
||||
"scanIncompleteMissing": "このプロファイルはまだ起動されていないため、Donutが管理する拡張機能のみ確認できました。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2488,15 +2488,18 @@
|
||||
"titleBlocked": "실행이 차단됨",
|
||||
"titleWarning": "실행하기 전에",
|
||||
"intro": "브라우저를 시작하기 전에 \"{{name}}\"의 다음 문제를 확인하세요.",
|
||||
"fingerprintHeading": "프록시 출구가 핑거프린트와 일치하지 않음",
|
||||
"vpnExtensionHeading": "VPN 확장 프로그램 감지됨",
|
||||
"fingerprintHeading": "측정된 출구가 핑거프린트와 일치하지 않음",
|
||||
"vpnExtensionHeading": "VPN 또는 프록시 확장 프로그램 감지됨",
|
||||
"vpnExtensionIntro": "이 프로필에서 브라우저 트래픽의 경로를 바꿀 수 있는 확장 프로그램:",
|
||||
"vpnExtensionConfirmed": "프록시를 변경할 수 있음",
|
||||
"vpnExtensionLikely": "프록시를 변경할 수 있음(추정)",
|
||||
"vpnExtensionConfirmed": "알려진 VPN 또는 프록시 도구",
|
||||
"vpnExtensionLikely": "VPN 또는 프록시 도구로 보임",
|
||||
"vpnExtensionCapability": "프록시 권한을 보유함",
|
||||
"vpnExtensionExplainer": "이 중 하나가 트래픽을 다른 곳으로 보내면 브라우저의 실제 위치가 이 프로필을 만들 때 사용한 시간대, 언어, 지리 정보와 더 이상 일치하지 않으며, Donut은 외부에서 이를 감지할 수 없습니다.",
|
||||
"proxyCapableHeading": "프록시를 변경할 수 있는 확장 프로그램",
|
||||
"proxyCapableIntro": "VPN으로 보이지는 않지만 Chromium의 프록시 권한을 가지고 있습니다. 다운로드 관리자나 디버깅 도구에도 필요한 권한이며, 실제로 사용하는지는 Donut이 알 수 없습니다:",
|
||||
"sourceDonut": "Donut이 관리",
|
||||
"sourceBrowser": "프로필에 설치됨",
|
||||
"measurementUnreliable": "VPN 확장 프로그램이 프록시를 덮어쓸 수 있으므로, 출구 확인 결과가 브라우저의 실제 경로와 다를 수 있습니다.",
|
||||
"measurementUnreliable": "이 프로필의 확장 프로그램이 프록시 권한을 가지고 있어, 출구 확인 결과가 브라우저의 실제 경로와 다를 수 있습니다.",
|
||||
"scanIncompleteEncrypted": "이 프로필은 암호화되어 있어 Donut이 관리하는 확장 프로그램만 확인할 수 있었습니다.",
|
||||
"scanIncompleteEphemeral": "이 프로필에는 아직 데이터가 없어 Donut이 관리하는 확장 프로그램만 확인할 수 있었습니다.",
|
||||
"scanIncompletePartial": "확장 프로그램 검사가 중단되어 일부가 표시되지 않을 수 있습니다.",
|
||||
@@ -2506,8 +2509,8 @@
|
||||
"dontWarnExtensions": "이 확장 프로그램에 대해 다시 경고하지 않기",
|
||||
"applyToRemaining": "이 선택을 나머지 프로필에 적용",
|
||||
"cancelledSummary": "{{total}}개 중 {{cancelled}}개의 실행이 취소됨",
|
||||
"cancelled": "실행이 취소됨",
|
||||
"vpnExtensionEntry": " {{version}} — {{capability}}, {{source}}",
|
||||
"vpnExtensionEntryNoVersion": " — {{capability}}, {{source}}",
|
||||
"scanIncompleteMissing": "이 프로필은 아직 실행된 적이 없어 Donut이 관리하는 확장 프로그램만 확인할 수 있었습니다."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2517,15 +2517,18 @@
|
||||
"titleBlocked": "Inicialização bloqueada",
|
||||
"titleWarning": "Antes de iniciar",
|
||||
"intro": "Revise estes problemas de \"{{name}}\" antes de iniciar o navegador.",
|
||||
"fingerprintHeading": "A saída do proxy não corresponde à impressão digital",
|
||||
"vpnExtensionHeading": "Extensão VPN detectada",
|
||||
"fingerprintHeading": "A saída medida não corresponde à impressão digital",
|
||||
"vpnExtensionHeading": "Extensão de VPN ou proxy detectada",
|
||||
"vpnExtensionIntro": "Extensões neste perfil que podem redirecionar o tráfego do navegador:",
|
||||
"vpnExtensionConfirmed": "Pode alterar o proxy",
|
||||
"vpnExtensionLikely": "Talvez altere o proxy",
|
||||
"vpnExtensionConfirmed": "Ferramenta de VPN ou proxy conhecida",
|
||||
"vpnExtensionLikely": "Parece uma ferramenta de VPN ou proxy",
|
||||
"vpnExtensionCapability": "Tem a permissão de proxy",
|
||||
"vpnExtensionExplainer": "Se alguma delas redirecionar seu tráfego, a localização real do navegador deixará de corresponder ao fuso horário, ao idioma e à geolocalização com que este perfil foi criado, e o Donut não consegue detectar isso de fora.",
|
||||
"proxyCapableHeading": "Extensões que podem alterar o proxy",
|
||||
"proxyCapableIntro": "Não parecem VPNs, mas têm a permissão de proxy do Chromium, que gerenciadores de download e ferramentas de depuração também precisam. O Donut não consegue saber se alguma delas a está usando:",
|
||||
"sourceDonut": "Gerenciada pelo Donut",
|
||||
"sourceBrowser": "Instalada no perfil",
|
||||
"measurementUnreliable": "Como uma extensão VPN pode substituir o proxy, a verificação de saída pode não refletir a rota que o navegador realmente usa.",
|
||||
"measurementUnreliable": "Uma extensão neste perfil tem a permissão de proxy, então a verificação de saída pode não refletir a rota que o navegador realmente usa.",
|
||||
"scanIncompleteEncrypted": "Este perfil está criptografado, portanto só foi possível verificar as extensões gerenciadas pelo Donut.",
|
||||
"scanIncompleteEphemeral": "Este perfil ainda não tem dados, portanto só foi possível verificar as extensões gerenciadas pelo Donut.",
|
||||
"scanIncompletePartial": "A verificação de extensões foi interrompida, então algumas podem não estar listadas.",
|
||||
@@ -2535,8 +2538,8 @@
|
||||
"dontWarnExtensions": "Não avisar novamente sobre estas extensões",
|
||||
"applyToRemaining": "Aplicar esta escolha aos perfis restantes",
|
||||
"cancelledSummary": "{{cancelled}} de {{total}} inicializações canceladas",
|
||||
"cancelled": "Inicialização cancelada",
|
||||
"vpnExtensionEntry": " {{version}} — {{capability}}, {{source}}",
|
||||
"vpnExtensionEntryNoVersion": " — {{capability}}, {{source}}",
|
||||
"scanIncompleteMissing": "Este perfil ainda não foi iniciado, portanto só foi possível verificar as extensões gerenciadas pelo Donut."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2546,15 +2546,18 @@
|
||||
"titleBlocked": "Запуск заблокирован",
|
||||
"titleWarning": "Перед запуском",
|
||||
"intro": "Проверьте эти проблемы профиля «{{name}}» перед запуском браузера.",
|
||||
"fingerprintHeading": "Выходной узел прокси не совпадает с отпечатком",
|
||||
"vpnExtensionHeading": "Обнаружено VPN-расширение",
|
||||
"fingerprintHeading": "Измеренный выходной узел не совпадает с отпечатком",
|
||||
"vpnExtensionHeading": "Обнаружено VPN- или прокси-расширение",
|
||||
"vpnExtensionIntro": "Расширения в этом профиле, способные перенаправить трафик браузера:",
|
||||
"vpnExtensionConfirmed": "Может изменить прокси",
|
||||
"vpnExtensionLikely": "Возможно, изменит прокси",
|
||||
"vpnExtensionConfirmed": "Известный VPN- или прокси-инструмент",
|
||||
"vpnExtensionLikely": "Похоже на VPN- или прокси-инструмент",
|
||||
"vpnExtensionCapability": "Имеет разрешение proxy",
|
||||
"vpnExtensionExplainer": "Если одно из них направит трафик в другое место, реальное местоположение браузера перестанет совпадать с часовым поясом, языком и геолокацией, с которыми создавался профиль, а Donut не сможет это обнаружить извне.",
|
||||
"proxyCapableHeading": "Расширения, способные изменить прокси",
|
||||
"proxyCapableIntro": "Это не похоже на VPN, но у них есть разрешение proxy в Chromium, которое нужно и менеджерам загрузок, и инструментам отладки. Donut не может определить, использует ли его кто-то из них:",
|
||||
"sourceDonut": "Управляется Donut",
|
||||
"sourceBrowser": "Установлено в профиле",
|
||||
"measurementUnreliable": "Поскольку VPN-расширение может переопределить прокси, проверка выходного узла может не отражать реальный маршрут браузера.",
|
||||
"measurementUnreliable": "Расширение в этом профиле имеет разрешение proxy, поэтому проверка выходного узла может не отражать реальный маршрут браузера.",
|
||||
"scanIncompleteEncrypted": "Профиль зашифрован, поэтому удалось проверить только расширения, управляемые Donut.",
|
||||
"scanIncompleteEphemeral": "В профиле ещё нет данных, поэтому удалось проверить только расширения, управляемые Donut.",
|
||||
"scanIncompletePartial": "Проверка расширений была прервана, поэтому некоторые могут отсутствовать в списке.",
|
||||
@@ -2564,8 +2567,8 @@
|
||||
"dontWarnExtensions": "Больше не предупреждать об этих расширениях",
|
||||
"applyToRemaining": "Применить этот выбор к остальным профилям",
|
||||
"cancelledSummary": "Отменено запусков: {{cancelled}} из {{total}}",
|
||||
"cancelled": "Запуск отменён",
|
||||
"vpnExtensionEntry": " {{version}} — {{capability}}, {{source}}",
|
||||
"vpnExtensionEntryNoVersion": " — {{capability}}, {{source}}",
|
||||
"scanIncompleteMissing": "Профиль ещё ни разу не запускался, поэтому удалось проверить только расширения, управляемые Donut."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2488,15 +2488,18 @@
|
||||
"titleBlocked": "Başlatma engellendi",
|
||||
"titleWarning": "Başlatmadan önce",
|
||||
"intro": "Tarayıcıyı başlatmadan önce \"{{name}}\" ile ilgili şu sorunları inceleyin.",
|
||||
"fingerprintHeading": "Proxy çıkışı parmak iziyle eşleşmiyor",
|
||||
"vpnExtensionHeading": "VPN uzantısı algılandı",
|
||||
"fingerprintHeading": "Ölçülen çıkış parmak iziyle eşleşmiyor",
|
||||
"vpnExtensionHeading": "VPN veya proxy uzantısı algılandı",
|
||||
"vpnExtensionIntro": "Bu profildeki, tarayıcı trafiğini yeniden yönlendirebilecek uzantılar:",
|
||||
"vpnExtensionConfirmed": "Proxy'yi değiştirebilir",
|
||||
"vpnExtensionLikely": "Proxy'yi değiştirebilir (olası)",
|
||||
"vpnExtensionConfirmed": "Bilinen VPN veya proxy aracı",
|
||||
"vpnExtensionLikely": "VPN veya proxy aracı gibi görünüyor",
|
||||
"vpnExtensionCapability": "Proxy iznine sahip",
|
||||
"vpnExtensionExplainer": "Bunlardan biri trafiğinizi başka bir yere yönlendirirse, tarayıcının gerçek konumu artık bu profilin oluşturulduğu saat dilimi, dil ve coğrafi konumla eşleşmez ve Donut bunu dışarıdan algılayamaz.",
|
||||
"proxyCapableHeading": "Proxy'yi değiştirebilen uzantılar",
|
||||
"proxyCapableIntro": "Bunlar VPN'e benzemiyor, ancak Chromium'un proxy iznine sahipler; bu izne indirme yöneticileri ve hata ayıklama araçları da ihtiyaç duyar. Donut, herhangi birinin bunu kullanıp kullanmadığını anlayamaz:",
|
||||
"sourceDonut": "Donut tarafından yönetiliyor",
|
||||
"sourceBrowser": "Profile yüklenmiş",
|
||||
"measurementUnreliable": "Bir VPN uzantısı proxy'yi geçersiz kılabileceğinden, çıkış kontrolü tarayıcının gerçekte kullandığı rotayı yansıtmayabilir.",
|
||||
"measurementUnreliable": "Bu profildeki bir uzantı proxy iznine sahip, bu nedenle çıkış kontrolü tarayıcının gerçekte kullandığı rotayı yansıtmayabilir.",
|
||||
"scanIncompleteEncrypted": "Bu profil şifreli olduğundan yalnızca Donut tarafından yönetilen uzantılar denetlenebildi.",
|
||||
"scanIncompleteEphemeral": "Bu profilde henüz veri olmadığından yalnızca Donut tarafından yönetilen uzantılar denetlenebildi.",
|
||||
"scanIncompletePartial": "Uzantı taraması yarıda kesildi, bu nedenle bazıları listelenmemiş olabilir.",
|
||||
@@ -2506,8 +2509,8 @@
|
||||
"dontWarnExtensions": "Bu uzantılar için bir daha uyarma",
|
||||
"applyToRemaining": "Bu seçimi kalan profillere uygula",
|
||||
"cancelledSummary": "{{total}} başlatmadan {{cancelled}} tanesi iptal edildi",
|
||||
"cancelled": "Başlatma iptal edildi",
|
||||
"vpnExtensionEntry": " {{version}} — {{capability}}, {{source}}",
|
||||
"vpnExtensionEntryNoVersion": " — {{capability}}, {{source}}",
|
||||
"scanIncompleteMissing": "Bu profil henüz başlatılmadığından yalnızca Donut tarafından yönetilen uzantılar denetlenebildi."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2488,15 +2488,18 @@
|
||||
"titleBlocked": "Đã chặn khởi chạy",
|
||||
"titleWarning": "Trước khi khởi chạy",
|
||||
"intro": "Hãy xem lại các vấn đề của \"{{name}}\" trước khi khởi động trình duyệt.",
|
||||
"fingerprintHeading": "Điểm ra của proxy không khớp với dấu vân tay",
|
||||
"vpnExtensionHeading": "Đã phát hiện tiện ích VPN",
|
||||
"fingerprintHeading": "Điểm ra đo được không khớp với dấu vân tay",
|
||||
"vpnExtensionHeading": "Đã phát hiện tiện ích VPN hoặc proxy",
|
||||
"vpnExtensionIntro": "Các tiện ích trong hồ sơ này có thể định tuyến lại lưu lượng của trình duyệt:",
|
||||
"vpnExtensionConfirmed": "Có thể thay đổi proxy",
|
||||
"vpnExtensionLikely": "Có khả năng thay đổi proxy",
|
||||
"vpnExtensionConfirmed": "Công cụ VPN hoặc proxy đã biết",
|
||||
"vpnExtensionLikely": "Có vẻ là công cụ VPN hoặc proxy",
|
||||
"vpnExtensionCapability": "Có quyền proxy",
|
||||
"vpnExtensionExplainer": "Nếu một trong số đó chuyển lưu lượng của bạn đi nơi khác, vị trí thực của trình duyệt sẽ không còn khớp với múi giờ, ngôn ngữ và vị trí địa lý mà hồ sơ này được tạo ra, và Donut không thể phát hiện điều đó từ bên ngoài.",
|
||||
"proxyCapableHeading": "Các tiện ích có thể thay đổi proxy",
|
||||
"proxyCapableIntro": "Chúng không giống VPN, nhưng có quyền proxy của Chromium, thứ mà trình quản lý tải xuống và công cụ gỡ lỗi cũng cần. Donut không thể biết liệu có tiện ích nào đang dùng quyền đó hay không:",
|
||||
"sourceDonut": "Do Donut quản lý",
|
||||
"sourceBrowser": "Đã cài trong hồ sơ",
|
||||
"measurementUnreliable": "Vì tiện ích VPN có thể ghi đè proxy, kết quả kiểm tra điểm ra có thể không phản ánh tuyến đường mà trình duyệt thực sự dùng.",
|
||||
"measurementUnreliable": "Một tiện ích trong hồ sơ này có quyền proxy, nên kết quả kiểm tra điểm ra có thể không phản ánh tuyến đường mà trình duyệt thực sự dùng.",
|
||||
"scanIncompleteEncrypted": "Hồ sơ này được mã hóa nên chỉ có thể kiểm tra các tiện ích do Donut quản lý.",
|
||||
"scanIncompleteEphemeral": "Hồ sơ này chưa có dữ liệu nên chỉ có thể kiểm tra các tiện ích do Donut quản lý.",
|
||||
"scanIncompletePartial": "Quá trình quét tiện ích bị ngắt giữa chừng nên có thể thiếu một số tiện ích.",
|
||||
@@ -2506,8 +2509,8 @@
|
||||
"dontWarnExtensions": "Không cảnh báo lại về các tiện ích này",
|
||||
"applyToRemaining": "Áp dụng lựa chọn này cho các hồ sơ còn lại",
|
||||
"cancelledSummary": "Đã hủy {{cancelled}} trên {{total}} lượt khởi chạy",
|
||||
"cancelled": "Đã hủy khởi chạy",
|
||||
"vpnExtensionEntry": " {{version}} — {{capability}}, {{source}}",
|
||||
"vpnExtensionEntryNoVersion": " — {{capability}}, {{source}}",
|
||||
"scanIncompleteMissing": "Hồ sơ này chưa từng được khởi chạy nên chỉ có thể kiểm tra các tiện ích do Donut quản lý."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2488,15 +2488,18 @@
|
||||
"titleBlocked": "启动已阻止",
|
||||
"titleWarning": "启动前请注意",
|
||||
"intro": "启动浏览器前,请检查“{{name}}”的以下问题。",
|
||||
"fingerprintHeading": "代理出口与指纹不匹配",
|
||||
"vpnExtensionHeading": "检测到 VPN 扩展",
|
||||
"fingerprintHeading": "测得的出口与指纹不匹配",
|
||||
"vpnExtensionHeading": "检测到 VPN 或代理扩展",
|
||||
"vpnExtensionIntro": "此配置文件中可能改变浏览器流量路径的扩展:",
|
||||
"vpnExtensionConfirmed": "可以更改代理",
|
||||
"vpnExtensionLikely": "可能会更改代理",
|
||||
"vpnExtensionConfirmed": "已知的 VPN 或代理工具",
|
||||
"vpnExtensionLikely": "疑似 VPN 或代理工具",
|
||||
"vpnExtensionCapability": "拥有代理权限",
|
||||
"vpnExtensionExplainer": "如果其中之一将流量转发到别处,浏览器的真实位置将不再与创建此配置文件时使用的时区、语言和地理位置一致,而 Donut 无法从外部察觉。",
|
||||
"proxyCapableHeading": "可以更改代理的扩展",
|
||||
"proxyCapableIntro": "它们看起来不是 VPN,但拥有 Chromium 的代理权限,下载管理器和调试工具同样需要该权限。Donut 无法判断它们是否在使用它:",
|
||||
"sourceDonut": "由 Donut 管理",
|
||||
"sourceBrowser": "已安装在配置文件中",
|
||||
"measurementUnreliable": "由于 VPN 扩展可以覆盖代理设置,出口检测结果可能并非浏览器实际使用的线路。",
|
||||
"measurementUnreliable": "此配置文件中有扩展拥有代理权限,因此出口检测结果可能并非浏览器实际使用的线路。",
|
||||
"scanIncompleteEncrypted": "此配置文件已加密,因此只能检查由 Donut 管理的扩展。",
|
||||
"scanIncompleteEphemeral": "此配置文件尚无数据,因此只能检查由 Donut 管理的扩展。",
|
||||
"scanIncompletePartial": "扩展扫描被中断,可能有部分扩展未列出。",
|
||||
@@ -2506,8 +2509,8 @@
|
||||
"dontWarnExtensions": "不再就这些扩展发出警告",
|
||||
"applyToRemaining": "将此选择应用于其余配置文件",
|
||||
"cancelledSummary": "已取消 {{total}} 次启动中的 {{cancelled}} 次",
|
||||
"cancelled": "已取消启动",
|
||||
"vpnExtensionEntry": " {{version}} — {{capability}}、{{source}}",
|
||||
"vpnExtensionEntryNoVersion": " — {{capability}}、{{source}}",
|
||||
"scanIncompleteMissing": "此配置文件尚未启动过,因此只能检查由 Donut 管理的扩展。"
|
||||
}
|
||||
}
|
||||
|
||||
+20
-4
@@ -673,7 +673,22 @@ export interface ConsistencyResult {
|
||||
mismatches: string[];
|
||||
}
|
||||
|
||||
/** A VPN/proxy extension found in a profile, which can reroute browser traffic. */
|
||||
/**
|
||||
* How strongly an extension is believed to be a VPN or proxy tool.
|
||||
* "capability" is not such a claim: it means only that the extension holds
|
||||
* Chromium's `proxy` permission, which download managers do too.
|
||||
*/
|
||||
export type VpnExtensionConfidence = "confirmed" | "likely" | "capability";
|
||||
|
||||
/** How much of a profile's extension set could be read. */
|
||||
export type ExtensionScanState =
|
||||
| "scanned"
|
||||
| "partial"
|
||||
| "encrypted"
|
||||
| "ephemeral"
|
||||
| "missing";
|
||||
|
||||
/** An extension found in a profile that could change where the browser connects. */
|
||||
export interface DetectedVpnExtension {
|
||||
/** Acknowledgement identity: `donut:<uuid>` or `crx:<id>`. */
|
||||
key: string;
|
||||
@@ -681,15 +696,16 @@ export interface DetectedVpnExtension {
|
||||
version: string | null;
|
||||
/** "donut" (managed by Donut) or "browser" (installed in the profile). */
|
||||
source: string;
|
||||
/** "confirmed" (holds the proxy permission) or "likely". */
|
||||
confidence: string;
|
||||
confidence: VpnExtensionConfidence;
|
||||
/** Holds the `proxy` permission outright, so it can change the proxy today. */
|
||||
proxy_control: boolean;
|
||||
signals: string[];
|
||||
}
|
||||
|
||||
/** Local-only checks answered before a launch starts any worker. */
|
||||
export interface PreLaunchChecks {
|
||||
vpn_extensions: DetectedVpnExtension[];
|
||||
scan_state: string;
|
||||
scan_state: ExtensionScanState;
|
||||
consistency: ConsistencyResult;
|
||||
exit_probe_pending: boolean;
|
||||
exit_measurement_unreliable: boolean;
|
||||
|
||||
Reference in New Issue
Block a user