diff --git a/apps/extension/src/entrypoints/popup/App.test.tsx b/apps/extension/src/entrypoints/popup/App.test.tsx index 503173411..15966ac00 100644 --- a/apps/extension/src/entrypoints/popup/App.test.tsx +++ b/apps/extension/src/entrypoints/popup/App.test.tsx @@ -67,6 +67,25 @@ describe("App", () => { vi.unstubAllGlobals(); }); + it("keeps an unpaired connection enabled and displays a nonblocking warning", async () => { + mockUseConnectionState.mockReturnValue({ + snapshot: { + ...baseSnapshot, + state: "connected", + localAuth: { state: "unverified_local", reason: "unpaired" }, + }, + statusState: "connected", + setLabel, + setConnectionEnabled, + }); + render(); + expect(screen.getByText("已连接")).toBeTruthy(); + expect(screen.getByText("未配对")).toBeTruthy(); + expect(screen.getByRole("link", { name: "为什么建议配对?" })).toBeTruthy(); + expect(screen.queryByRole("alertdialog")).toBeNull(); + expect(setConnectionEnabled).not.toHaveBeenCalled(); + }); + it("adds only a current-task section and keeps existing home controls accessible", async () => { mockUseConnectionState.mockReturnValue({ snapshot: { ...baseSnapshot, state: "connected" }, diff --git a/apps/extension/src/entrypoints/popup/App.tsx b/apps/extension/src/entrypoints/popup/App.tsx index 83fc7dca8..25917c1c3 100644 --- a/apps/extension/src/entrypoints/popup/App.tsx +++ b/apps/extension/src/entrypoints/popup/App.tsx @@ -203,12 +203,49 @@ export function App() { >
- + - {t(STATE_LABEL_KEYS[statusState])} + {t( + statusState !== "disabled" && snapshot.localAuth?.state === "pairing_required" + ? "popup.localAuth.required" + : STATE_LABEL_KEYS[statusState], + )}
+ {connectionLive && snapshot.localAuth === null && ( + + {t("popup.connectionRemote")} + + )} + {connectionLive && + snapshot.localAuth && + ["verified", "unverified_local"].includes(snapshot.localAuth.state) && ( + + {t( + snapshot.localAuth.state === "verified" + ? "popup.localAuth.verified" + : snapshot.localAuth.reason === "unpaired" + ? "popup.localAuth.unpaired" + : "popup.localAuth.unverified", + )} + + )} {isSkewed && ( diff --git a/apps/extension/src/entrypoints/popup/connection-settings.test.tsx b/apps/extension/src/entrypoints/popup/connection-settings.test.tsx index 29fff9619..4432d6d06 100644 --- a/apps/extension/src/entrypoints/popup/connection-settings.test.tsx +++ b/apps/extension/src/entrypoints/popup/connection-settings.test.tsx @@ -3,14 +3,25 @@ import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, expect, it, vi } from "vitest"; import { STORAGE_KEYS } from "@/lib/instance-id"; import type { RemoteEndpoint } from "@/transport/remote-endpoint"; -import { readRemoteConnection } from "@/transport/remote-storage"; +import { readRemoteConnection, readSavedRemoteConnection } from "@/transport/remote-storage"; import { ConnectionSettings } from "./connection-settings"; vi.mock("@/transport/remote-storage", () => ({ REMOTE_CONNECTION_REVISION: "revision", REMOTE_CONNECTION_MODE: "mode", readRemoteConnection: vi.fn(), + readSavedRemoteConnection: vi.fn(), })); +vi.mock("./local-pairing", async (original) => ({ + ...(await original()), + readLocalPairingInfo: vi.fn(), +})); + +import type { LocalAuthStatus } from "@/transport/local-auth"; +import v from "../../../../../crates/bsk-protocol/tests/fixtures/local-auth-v1.json"; +import { readLocalPairingInfo } from "./local-pairing"; + +const localLink = `ws://127.0.0.1:52800/#bsk-local.v1.${v.daemon_id}.${v.device_id}.${v.key}`; const endpoint: RemoteEndpoint = { url: "wss://browser.example/extension", token: "a".repeat(43), @@ -34,6 +45,15 @@ beforeEach(() => { portWriteFails = false; store = { [STORAGE_KEYS.DAEMON_PORT]: 52800 }; listeners = new Set(); + vi.mocked(readLocalPairingInfo) + .mockReset() + .mockResolvedValue({ + policy: { skip: false, everPaired: false, legacyCompat: false }, + devices: [], + }); + vi.mocked(readSavedRemoteConnection) + .mockReset() + .mockImplementation(async () => remote); vi.mocked(readRemoteConnection) .mockReset() .mockImplementation(async () => { @@ -205,7 +225,7 @@ it("keeps the remote grant when the proposed local port is invalid or cannot be render(); fireEvent.click(screen.getByText("连接设置")); await choose("本机"); - expect(screen.getByText(/再次连接远程需要新的配对链接/)).toBeTruthy(); + expect(screen.getByText(/远程配对会保留/)).toBeTruthy(); fireEvent.change(screen.getByLabelText("本机端口"), { target: { value: "65536" } }); submit(); expect(await screen.findByText("请输入 1 到 65535 之间的端口号。")).toBeTruthy(); @@ -278,3 +298,155 @@ it("does not declare an unconfirmed rotation invalid based on its old local expi expect(await screen.findByText(/此前续期可能已成功/)).toBeTruthy(); expect(screen.queryByText("授权已过期,请使用新的配对链接重新连接。")).toBeNull(); }); + +it("pair: opens local pairing immediately and recognizes local links from either tab", async () => { + render( + , + ); + await choose("远程"); + fireEvent.change(screen.getByLabelText("配对链接"), { target: { value: localLink } }); + expect(screen.getByRole("button", { name: "本机" }).getAttribute("aria-pressed")).toBe("true"); + expect(document.querySelector("details")?.open).toBe(true); + vi.mocked(chrome.runtime.sendMessage).mockResolvedValueOnce({ ok: true }); + submit(); + await screen.findByText("配对成功。"); + expect(chrome.runtime.sendMessage).toHaveBeenCalledExactlyOnceWith({ + kind: "bsk-local-pairing", + action: "pair", + link: localLink, + }); + expect(document.body.textContent).not.toContain(v.key); +}); +function pairedInfo(skip = false) { + vi.mocked(readLocalPairingInfo).mockResolvedValue({ + policy: { skip, everPaired: true, legacyCompat: false }, + devices: [{ daemonId: v.daemon_id, deviceId: v.device_id, endpoint: "ws://127.0.0.1:52800/" }], + }); +} +it("connected/settings: paired details and advanced settings stay collapsed until opened", async () => { + pairedInfo(); + render( + , + ); + await screen.findByText("使用新的配对链接"); + expect(document.querySelector("details")?.open).toBe(false); + expect(document.querySelector('[data-slot="local-advanced"]')?.open).toBe( + false, + ); + expect(screen.queryByLabelText("配对链接")).toBeNull(); + fireEvent.click(screen.getByText("使用新的配对链接")); + expect(screen.getByLabelText("配对链接")).toBeTruthy(); +}); +it.each([ + ["already_paired", /bsk daemon pair --replace a1b2c3d4/], + ["replace_mismatch", /bsk daemon revoke a1b2c3d4/], +] as const)("%s: replacement requires confirmation and a rejected link preserves the current grant", async (error, guidance) => { + pairedInfo(); + render( + , + ); + fireEvent.click(await screen.findByText("使用新的配对链接")); + fireEvent.change(screen.getByLabelText("配对链接"), { target: { value: localLink } }); + submit(); + expect(screen.getByRole("alertdialog").textContent).toContain("替换成功后当前任务会结束"); + expect(chrome.runtime.sendMessage).not.toHaveBeenCalled(); + vi.mocked(chrome.runtime.sendMessage).mockResolvedValueOnce({ error, browserId: "a1b2c3d4" }); + fireEvent.click(screen.getByText("继续配对")); + expect(await screen.findByText(guidance)).toBeTruthy(); + expect((screen.getByLabelText("配对链接") as HTMLInputElement).value).toBe(localLink); + expect(document.querySelector('[data-slot="local-paired-info"]')).toBeTruthy(); + expect(chrome.runtime.sendMessage).toHaveBeenCalledTimes(1); +}); +it("skip: requires confirmation and keeps restore guidance visible outside collapsed settings", async () => { + pairedInfo(); + const { rerender } = render(); + await screen.findByText("使用新的配对链接"); + fireEvent.click(screen.getByRole("switch", { name: "跳过本机验证" })); + expect(screen.getByRole("alertdialog").textContent).toContain("bsk daemon trust-local on"); + expect(chrome.runtime.sendMessage).not.toHaveBeenCalled(); + vi.mocked(chrome.runtime.sendMessage).mockResolvedValueOnce({ ok: true }); + pairedInfo(true); + fireEvent.click(screen.getByText("确认跳过")); + await screen.findByText("恢复验证"); + expect(chrome.runtime.sendMessage).toHaveBeenCalledExactlyOnceWith({ + kind: "bsk-local-pairing", + action: "skip", + value: true, + }); + rerender( + , + ); + expect( + document.querySelector('[data-slot="local-pairing-notice"]')?.closest("details"), + ).toBeNull(); + pairedInfo(false); + vi.mocked(chrome.runtime.sendMessage).mockResolvedValueOnce({ ok: true }); + fireEvent.click(screen.getByText("恢复验证")); + expect(await screen.findByText(/bsk daemon trust-local off/)).toBeTruthy(); +}); +it.each([ + [{ state: "unverified_local", reason: "unpaired" }, /连接身份尚未验证/], + [{ state: "server_unverified" }, /服务未通过验证/], + [{ state: "daemon_too_old" }, /本机 bsk 版本过旧/], +] satisfies [ + LocalAuthStatus, + RegExp, +][])("renders the %j state without revealing credentials", async (status, message) => { + render(); + await screen.findByText(message); + if (["server_unverified", "daemon_too_old"].includes(status.state)) { + vi.mocked(chrome.runtime.sendMessage).mockResolvedValueOnce({ ok: true }); + fireEvent.click(screen.getByText("重试")); + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + kind: "bsk-local-pairing", + action: "retry", + }); + } + expect(document.body.textContent).not.toContain(v.key); +}); +it("unpaired: keeps settings collapsed and offers optional pairing and official security guidance", async () => { + render( + , + ); + const help = await screen.findByRole("link", { name: "为什么建议配对?" }); + expect(help.getAttribute("href")).toBe("https://browserskill.weixin.qq.com/#security"); + expect(help.getAttribute("target")).toBe("_blank"); + const settings = document.querySelector( + '[data-slot="popup-connection-settings"]', + ); + expect(settings?.open).toBe(false); + expect(chrome.runtime.sendMessage).not.toHaveBeenCalled(); + fireEvent.click(screen.getByRole("button", { name: "去配对" })); + expect(settings?.open).toBe(true); + expect(chrome.runtime.sendMessage).not.toHaveBeenCalled(); +}); +it("remote: resumes retained authorization before expiry without asking for a new link", async () => { + vi.mocked(readSavedRemoteConnection).mockResolvedValue(endpoint); + render(); + await choose("远程"); + expect(screen.getByText(/授权到期时间/)).toBeTruthy(); + vi.mocked(chrome.runtime.sendMessage).mockResolvedValueOnce({ url: endpoint.url }); + fireEvent.click(screen.getByText("切回远程")); + await screen.findByText("配对成功。"); + expect(chrome.runtime.sendMessage).toHaveBeenCalledExactlyOnceWith({ + kind: "bsk-remote-authorization", + resume: true, + }); +}); diff --git a/apps/extension/src/entrypoints/popup/connection-settings.tsx b/apps/extension/src/entrypoints/popup/connection-settings.tsx index 777f41626..fb6623a70 100644 --- a/apps/extension/src/entrypoints/popup/connection-settings.tsx +++ b/apps/extension/src/entrypoints/popup/connection-settings.tsx @@ -2,12 +2,22 @@ import { useTranslation } from "@browser-skill/i18n/react"; import { Button, Input, Label } from "@browser-skill/ui"; import { useCallback, useEffect, useRef, useState } from "react"; import { DEFAULT_DAEMON_PORT, resolveDaemonWsUrl } from "@/transport/daemon-endpoint"; +import type { LocalAuthStatus } from "@/transport/local-auth"; +import { parseLocalPairing } from "@/transport/local-credentials"; import { parseRemoteEndpoint, remoteAuthorizationStatus } from "@/transport/remote-endpoint"; import { REMOTE_CONNECTION_MODE, REMOTE_CONNECTION_REVISION, readRemoteConnection, + readSavedRemoteConnection, } from "@/transport/remote-storage"; +import { + type LocalPairingInfo, + LocalPairingNotice, + LocalVerificationControls, + localPairingRequest, + readLocalPairingInfo, +} from "./local-pairing"; import { SettingInfo } from "./setting-info"; import { useDaemonPort } from "./use-daemon-port"; @@ -17,56 +27,71 @@ type Connection = { expiresAt?: string; status: ReturnType; }; - +function parseLink(input: string) { + try { + const local = parseLocalPairing(input); + return { mode: "local" as const, local, url: local.endpoint }; + } catch { + return { mode: "remote" as const, url: parseRemoteEndpoint(input).url }; + } +} export function ConnectionSettings({ connectionEnabled, disconnected = false, + localAuth, + instanceId = "", }: { connectionEnabled: boolean; disconnected?: boolean; + localAuth?: LocalAuthStatus | null; + instanceId?: string; }) { const { t } = useTranslation("extension"); const port = useDaemonPort(); const [connection, setConnection] = useState(null); + const [savedRemote, setSavedRemote] = useState(null); + const [local, setLocal] = useState(null); const [ready, setReady] = useState(false); const [storageError, setStorageError] = useState(false); - // Selecting a form does not change the persisted connection. const [selection, setSelection] = useState(null); const [draft, setDraft] = useState(""); + const [newLink, setNewLink] = useState(false); + const [open, setOpen] = useState(false); const [busy, setBusy] = useState(false); - const [error, setError] = useState<"format" | "pairing" | "local" | null>(null); + const [confirm, setConfirm] = useState(false); + const [restored, setRestored] = useState(false); + const [error, setError] = useState(null); + const [rejection, setRejection] = useState(null); const [success, setSuccess] = useState<"pairing" | "local" | "port" | null>(null); const saving = useRef(false); const mounted = useRef(false); const revision = useRef(0); const timer = useRef | undefined>(undefined); - const read = useCallback(async () => { const current = ++revision.current; clearTimeout(timer.current); - try { - const remote = await readRemoteConnection(); - if (!mounted.current || current !== revision.current) return; - setConnection( - remote - ? { - url: remote.url, - expiresAt: remote.expiresAt, - status: remoteAuthorizationStatus(remote), - } - : null, - ); - setStorageError(false); - setReady(true); - if (remote) timer.current = setTimeout(() => void read(), 30_000); - } catch { - if (!mounted.current || current !== revision.current) return; - setStorageError(true); - setReady(true); - timer.current = setTimeout(() => void read(), 30_000); - } + const [remote, saved, info] = await Promise.allSettled([ + readRemoteConnection(), + readSavedRemoteConnection(), + readLocalPairingInfo(), + ]); + if (!mounted.current || current !== revision.current) return; + const publicConnection = ( + endpoint: NonNullable>>, + ): Connection => ({ + url: endpoint.url, + expiresAt: endpoint.expiresAt, + status: remoteAuthorizationStatus(endpoint), + }); + setStorageError(remote.status === "rejected"); + if (remote.status === "fulfilled") + setConnection(remote.value ? publicConnection(remote.value) : null); + if (saved.status === "fulfilled") + setSavedRemote(saved.value ? publicConnection(saved.value) : null); + if (info.status === "fulfilled") setLocal(info.value); + setReady(true); + timer.current = setTimeout(() => void read(), 30_000); }, []); - useEffect(() => { mounted.current = true; const changed = (changes: Record, area: string) => { @@ -78,122 +103,194 @@ export function ConnectionSettings({ void read(); } }; - if (typeof chrome === "undefined" || !chrome.storage?.local) { - return () => { - mounted.current = false; - }; + if (typeof chrome !== "undefined" && chrome.storage?.local) { + chrome.storage.onChanged.addListener(changed); + void read(); } - chrome.storage.onChanged.addListener(changed); - void read(); return () => { mounted.current = false; revision.current += 1; clearTimeout(timer.current); - chrome.storage.onChanged.removeListener(changed); + if (typeof chrome !== "undefined") chrome.storage?.onChanged.removeListener(changed); }; }, [read]); - + useEffect(() => { + if (localAuth?.state === "pairing_required") { + setOpen(true); + setSelection("local"); + setNewLink(true); + } + if (mounted.current && localAuth?.state === "verified") void read(); + }, [localAuth?.state, read]); const activeMode: Mode = connection || storageError ? "remote" : "local"; const mode = selection ?? activeMode; const switchingToLocal = mode === "local" && activeMode !== "local"; - const dirty = mode !== activeMode || (mode === "local" ? port.dirty : !!draft.trim()); - let destination: string | null = null; + const paired = local?.devices.find( + (device) => + device.daemonId === localAuth?.daemonId || + Number(new URL(device.endpoint).port || "80") === port.savedPort, + ); + const skip = local?.policy.skip ?? localAuth?.reason === "skip"; + let parsed: ReturnType | null = null; try { - if (draft.trim()) destination = parseRemoteEndpoint(draft).url; + if (draft.trim()) parsed = parseLink(draft); } catch { - // Validate on submit; an incomplete draft must not replace the current address. + /* Validate when submitted. */ } + const resumeRemote = + mode === "remote" && + activeMode === "local" && + !!savedRemote && + !["expired", "rejected"].includes(savedRemote.status) && + !draft.trim(); + const dirty = mode !== activeMode || port.dirty || !!draft.trim(); + const showInput = mode === "remote" || newLink || !paired; + const statusKey = connection + ? ( + { + active: null, + renewing: "remoteRenewing", + unavailable: "remoteRenewalFailed", + rejected: "remoteRejected", + expired: "remoteExpired", + unconfirmed: "remoteUnconfirmed", + } as const + )[connection.status] + : null; + const needsAttention = + storageError || + error || + rejection || + statusKey || + skip || + ["pairing_required", "server_unverified", "daemon_too_old"].includes(localAuth?.state ?? ""); + const address = !ready + ? t("popup.connectionLoading") + : storageError + ? t("popup.remoteUnknown") + : t("popup.connectionCurrent", { + mode: t(activeMode === "local" ? "popup.connectionLocal" : "popup.connectionRemote"), + address: + connection?.url ?? (port.savedPort === null ? "—" : resolveDaemonWsUrl(port.savedPort)), + }); + const saveDisabled = + !ready || + busy || + (!draft.trim() && + (mode === "remote" ? !resumeRemote : !switchingToLocal && (!port.loaded || !port.dirty))); - async function save() { + async function save(confirmed = false) { if (!ready || saving.current) return; setError(null); + setRejection(null); setSuccess(null); - if (mode === "remote" && !destination) { + if ((draft.trim() && !parsed) || (mode === "remote" && !parsed && !resumeRemote)) { setError("format"); return; } + if ( + parsed?.mode === "local" && + local?.devices.some((d) => d.daemonId === parsed.local.daemonId) && + !confirmed + ) { + setConfirm(true); + return; + } saving.current = true; setBusy(true); try { - if (mode === "local") { - // Reuse the existing port writer. Failed validation or persistence must - // not discard a remote grant. Explicit local recovery also works when - // the port preference cannot be read; no unread port is overwritten. + if (parsed?.mode === "local") { + await localPairingRequest({ action: "pair", link: draft }); + } else if (mode === "remote") { + const reply = await chrome.runtime.sendMessage({ + kind: "bsk-remote-authorization", + ...(resumeRemote ? { resume: true } : { pairing: draft }), + }); + if (!reply || reply.error) throw new Error("pairing"); + } else { if (port.loaded) { if (!(await port.commit())) return; - } else if (!switchingToLocal) { - return; - } + } else if (!switchingToLocal) return; if (switchingToLocal) { const reply = await chrome.runtime.sendMessage({ kind: "bsk-remote-authorization", pairing: null, }); - if (!reply || reply.error) throw new Error("Local selection failed"); - await read(); + if (!reply || reply.error) throw new Error("local"); } - } else { - const reply = await chrome.runtime.sendMessage({ - kind: "bsk-remote-authorization", - pairing: draft, - }); - if (!reply || reply.error) throw new Error("Pairing failed"); - await read(); } + await read(); if (!mounted.current) return; setDraft(""); setSelection(null); - setSuccess(mode === "remote" ? "pairing" : switchingToLocal ? "local" : "port"); - } catch { - if (mounted.current) setError(mode === "remote" ? "pairing" : "local"); + setConfirm(false); + setNewLink(false); + setSuccess(parsed || resumeRemote ? "pairing" : switchingToLocal ? "local" : "port"); + } catch (cause) { + if (!mounted.current) return; + if (cause instanceof Error && ["already_paired", "replace_mismatch"].includes(cause.message)) + setRejection({ + state: cause.message as "already_paired" | "replace_mismatch", + browserId: (cause as Error & { browserId?: string }).browserId, + }); + else setError(parsed?.mode === "local" ? "pairing" : mode === "remote" ? "pairing" : "local"); + setConfirm(false); } finally { saving.current = false; if (mounted.current) setBusy(false); } } - - function select(mode: Mode) { - setSelection(mode); + async function changeSkip(value: boolean) { + await localPairingRequest({ action: "skip", value }); + await read(); + setRestored(!value); + } + async function retry() { + try { + await localPairingRequest({ action: "retry" }); + } catch { + setError("local"); + } + } + function edit(value: string) { + setDraft(value); setError(null); + setRejection(null); setSuccess(null); + setConfirm(false); + try { + setSelection(parseLink(value).mode); + } catch { + /* Keep current tab while typing. */ + } } - - const statusKey = - connection && !storageError - ? ( - { - active: null, - renewing: "remoteRenewing", - unavailable: "remoteRenewalFailed", - rejected: "remoteRejected", - expired: "remoteExpired", - unconfirmed: "remoteUnconfirmed", - } as const - )[connection.status] - : null; - const needsAttention = - storageError || error || (connection && !["active", "renewing"].includes(connection.status)); - const localAddress = port.savedPort === null ? "—" : resolveDaemonWsUrl(port.savedPort); - const address = !ready - ? t("popup.connectionLoading") - : storageError - ? t("popup.remoteUnknown") - : t("popup.connectionCurrent", { - mode: t(activeMode === "local" ? "popup.connectionLocal" : "popup.connectionRemote"), - address: connection?.url ?? localAddress, - }); - const saveDisabled = - !ready || - busy || - (mode === "remote" ? !draft.trim() : !switchingToLocal && (!port.loaded || !port.dirty)); - + const notice = + rejection ?? + (activeMode === "local" + ? skip + ? { state: "unverified_local" as const, reason: "skip" as const } + : localAuth + : null); return ( <> - {disconnected && !storageError && !statusKey && ( -

+ { + setOpen(true); + setSelection("local"); + setNewLink(true); + }} + onRetry={() => void retry()} + onRestore={() => void changeSkip(false).catch(() => setError("local"))} + /> + {restored && ( +

+ {t("popup.localAuth.restoreHint")} +

+ )} + {disconnected && !notice && !storageError && !statusKey && ( +

{t(activeMode === "remote" ? "popup.remoteUnreachable" : "popup.daemonUnreachable")}

)} @@ -208,6 +305,8 @@ export function ConnectionSettings({

)}
setOpen(event.currentTarget.open)} className="mt-3 border-t border-border/70 pt-2.5" data-slot="popup-connection-settings" > @@ -216,10 +315,11 @@ export function ConnectionSettings({ {ready && !storageError && ( · {t(activeMode === "local" ? "popup.connectionLocal" : "popup.connectionRemote")} + {paired && activeMode === "local" ? ` · ${t("popup.localAuth.paired")}` : ""} )} {needsAttention && ( - {t("popup.remoteNeedsAttention")} + {t("popup.remoteNeedsAttention")} )}
@@ -229,9 +329,11 @@ export function ConnectionSettings({ > {address}

- {connection?.expiresAt && !storageError && ( + {(connection ?? (mode === "remote" ? savedRemote : null))?.expiresAt && (

- {t("popup.remoteExpires", { date: new Date(connection.expiresAt).toLocaleString() })} + {t("popup.remoteExpires", { + date: new Date((connection ?? savedRemote)!.expiresAt!).toLocaleString(), + })}

)}
select(value)} + onClick={() => { + setSelection(value); + setDraft(""); + setConfirm(false); + setError(null); + setSuccess(null); + }} className={`h-8 min-w-0 flex-1 rounded-md text-xs ${mode === value ? "bg-background shadow-sm" : "text-muted-foreground"}`} > {t(value === "local" ? "popup.connectionLocal" : "popup.connectionRemote")} @@ -257,13 +365,34 @@ export function ConnectionSettings({
{ event.preventDefault(); - if (!saveDisabled) void save(); + if (!saveDisabled && !confirm) void save(); }} className="space-y-2" aria-busy={busy} > - {mode === "local" ? ( + {mode === "local" && ( <> + {paired && ( +
+

+ {t("popup.localAuth.pairedInfo", { + daemon: paired.daemonId.slice(0, 8), + device: paired.deviceId.slice(0, 8), + })} +

+ {!newLink && ( + + )} +
+ )}
diff --git a/apps/extension/src/entrypoints/popup/local-pairing.tsx b/apps/extension/src/entrypoints/popup/local-pairing.tsx new file mode 100644 index 000000000..f41365108 --- /dev/null +++ b/apps/extension/src/entrypoints/popup/local-pairing.tsx @@ -0,0 +1,172 @@ +import { useTranslation } from "@browser-skill/i18n/react"; +import { Button } from "@browser-skill/ui"; +import { RiErrorWarningLine } from "@remixicon/react"; +import { useState } from "react"; +import type { LocalAuthStatus } from "@/transport/local-auth"; +import { Switch } from "./switch"; + +export interface LocalPairingInfo { + policy: { skip: boolean; everPaired: boolean; legacyCompat: boolean }; + devices: { daemonId: string; deviceId: string; endpoint: string }[]; +} +export async function localPairingRequest(request: Record) { + const reply = await chrome.runtime.sendMessage({ kind: "bsk-local-pairing", ...request }); + if (!reply || reply.error) + throw Object.assign(new Error(reply?.error ?? "pairing_failed"), { + browserId: reply?.browserId, + }); + return reply; +} +export async function readLocalPairingInfo(): Promise { + return localPairingRequest({ action: "info" }); +} +export function LocalPairingNotice({ + status, + instanceId = "", + onPair, + onRetry, + onRestore, +}: { + status?: LocalAuthStatus | null; + instanceId?: string; + onPair: () => void; + onRetry: () => void; + onRestore: () => void; +}) { + const { t } = useTranslation("extension"); + if (!status || status.state === "verified" || status.state === "verifying") return null; + const key = status.state === "unverified_local" ? (status.reason ?? "unpaired") : status.state; + return ( +
+
+

+ {t(`popup.localAuth.message.${key}`, { + browser: status.browserId ?? instanceId, + instance: instanceId, + })} +

+ {status.state === "unverified_local" && ( + + + )} +
+ {key === "skip" && ( + + )} + {key === "unpaired" && ( + + )} + {["server_unverified", "daemon_too_old", "busy"].includes(key) && ( + + )} +
+ ); +} +export function LocalVerificationControls({ + skip, + disabled, + onChange, +}: { + skip: boolean; + disabled: boolean; + onChange: (skip: boolean) => Promise; +}) { + const { t } = useTranslation("extension"); + const [confirm, setConfirm] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(false); + async function save(next: boolean) { + setSaving(true); + setError(false); + try { + await onChange(next); + setConfirm(false); + } catch { + setError(true); + } finally { + setSaving(false); + } + } + return ( + <> +
+ + {t("popup.localAuth.advanced")} + +
+ {t("popup.localAuth.skipLabel")} + (next ? setConfirm(true) : void save(false))} + /> +
+ {confirm && ( +
+

{t("popup.localAuth.skipConfirm")}

+

bsk daemon trust-local on

+
+ + +
+
+ )} + {error && ( +

+ {t("popup.localAuth.settingsFailed")} +

+ )} +
+ + ); +} diff --git a/packages/i18n/src/locales/de-DE/extension.json b/packages/i18n/src/locales/de-DE/extension.json index cedb201c3..2bbc792d7 100644 --- a/packages/i18n/src/locales/de-DE/extension.json +++ b/packages/i18n/src/locales/de-DE/extension.json @@ -137,7 +137,7 @@ "connectionDestination": "Verbinden mit: {{address}}", "connectionPortHint": "Leer lassen, um den Standardport {{port}} wiederherzustellen.", "connectionSaveWarning": "Das Speichern beendet laufende Aufgaben und stellt die Verbindung neu her, wenn der Verbindungsschalter aktiviert ist.", - "connectionLocalWarning": "Der Wechsel zu lokal beendet laufende Aufgaben und entfernt die gespeicherten Remote-Anmeldedaten. Für die Rückkehr zu Remote ist ein neuer Kopplungslink erforderlich.", + "connectionLocalWarning": "Der Wechsel zu lokal beendet laufende Aufgaben. Die Fernkopplung bleibt bis zum Ablauf der Autorisierung nutzbar. Im lokalen Modus wird sie nicht verlängert.", "connectionCancel": "Änderungen abbrechen", "connectionLocalError": "Wechsel zu lokal fehlgeschlagen. Bitte versuchen Sie es erneut.", "connectionLocalSaved": "Zum lokalen Dienst gewechselt.", @@ -171,6 +171,44 @@ "notificationMessage": "Browseraufgaben sind nicht verfügbar. Aktualisieren Sie bsk auf dem Computer, auf dem der Browserdienst läuft, und stellen Sie sicher, dass auch der laufende Dienst aktualisiert wurde.", "toolbarTitle": "BrowserSkill: bsk-Browserdienst aktualisieren", "notificationButton": "Update-Anleitung anzeigen" + }, + "localAuth": { + "required": "Kopplung erforderlich", + "verified": "Verifiziert", + "unverified": "Nicht verifiziert", + "paired": "Gekoppelt", + "pairedInfo": "Gekoppelt: bsk {{daemon}} · Gerät {{device}}", + "newLink": "Neuen Kopplungslink verwenden", + "pairAction": "Browser koppeln", + "unpaired": "Nicht gekoppelt", + "whyPair": "Warum wird die Kopplung empfohlen?", + "retry": "Erneut versuchen", + "restore": "Verifizierung wiederherstellen", + "restoreHint": "Verifizierung wiederhergestellt. Führen Sie im Terminal bsk daemon trust-local off aus, um auch die Prüfung durch bsk wiederherzustellen.", + "advanced": "Erweitert", + "skipLabel": "Lokale Verifizierung überspringen", + "skipConfirmTitle": "Verifizierung überspringen?", + "skipConfirm": "Lokale Programme könnten sich als bsk ausgeben und diesen Browser steuern. Auch bsk muss unverifizierte Verbindungen ausdrücklich erlauben.", + "confirmSkip": "Überspringen bestätigen", + "settingsFailed": "Einstellung konnte nicht gespeichert werden. Erneut versuchen.", + "pairHint": "Führen Sie bsk daemon pair aus und fügen Sie den Link hier ein. Lokale und entfernte Links werden automatisch erkannt.", + "pairPreserves": "Die aktuelle Verbindung bleibt verfügbar, bis die neue Kopplung erfolgreich ist.", + "replaceTitle": "Kopplung ersetzen?", + "replaceConfirm": "Dieser bsk ist bereits gekoppelt. Ein erfolgreicher Ersatz beendet laufende Aufgaben. Bei Ablehnung bleibt die aktuelle Kopplung gültig.", + "confirmReplace": "Kopplung fortsetzen", + "resumeRemote": "Zurück zur Fernverbindung", + "message": { + "unpaired": "Die Identität der Verbindung ist nicht bestätigt. Lokale Schadsoftware könnte einen Verbindungspartner imitieren. Eine Kopplung wird empfohlen; die Nutzung ist auch ohne sie möglich.", + "pairing_required": "Führen Sie bsk daemon pair im Terminal aus und fügen Sie den Link unten ein.", + "already_paired": "Dieser Browser ist bereits gekoppelt. Zum Ersetzen bsk daemon pair --replace {{browser}} ausführen und den neuen Link einfügen.", + "replace_mismatch": "Dieser Browser ist {{instance}}; das Ziel ist {{browser}}. Keine Änderungen. Für ein neues Profil bsk daemon pair verwenden; ungenutzte Geräte mit bsk daemon revoke {{browser}} widerrufen.", + "skip": "Lokale Verifizierung wird übersprungen. Lokale Programme könnten sich als bsk ausgeben und diesen Browser steuern.", + "migration_old": "Diese Verbindung ist nicht verifiziert; bsk ist für die Kopplung zu alt. Mit bsk update aktualisieren, dann koppeln.", + "migration_pair": "Diese Verbindung ist nicht verifiziert. bsk wird bald eine Kopplung voraussetzen. Bitte jetzt einmalig koppeln.", + "server_unverified": "Verifizierung fehlgeschlagen. Ein anderes Programm könnte den Port belegen oder die Kopplung wurde zurückgesetzt. Mit bsk doctor prüfen.", + "daemon_too_old": "bsk ist für eine sichere Verbindung zu alt. Führen Sie bsk update aus und versuchen Sie es erneut.", + "busy": "Eine andere Verbindung verifiziert dieses Gerät. Bitte kurz warten." + } } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/en-US/extension.json b/packages/i18n/src/locales/en-US/extension.json index 6cf643486..2a73b25b8 100644 --- a/packages/i18n/src/locales/en-US/extension.json +++ b/packages/i18n/src/locales/en-US/extension.json @@ -137,7 +137,7 @@ "connectionDestination": "Connect to: {{address}}", "connectionPortHint": "Leave blank to restore the default port, {{port}}.", "connectionSaveWarning": "Saving ends current tasks and reconnects if the connection switch is on.", - "connectionLocalWarning": "Switching to local ends current tasks and removes the saved remote credential. Returning to remote requires a new pairing link.", + "connectionLocalWarning": "Switching to local ends current tasks. Remote pairing is kept, so you can return before authorization expires. It does not renew while local mode is selected.", "connectionCancel": "Cancel changes", "connectionLocalError": "Could not switch to local. Please try again.", "connectionLocalSaved": "Switched to the local service.", @@ -171,6 +171,44 @@ "notificationMessage": "Browser tasks are unavailable. Update bsk on the computer running the browser service and make sure the running service is updated too.", "toolbarTitle": "BrowserSkill: update the bsk browser service", "notificationButton": "View update steps" + }, + "localAuth": { + "required": "Pairing required", + "verified": "Verified", + "unverified": "Unverified", + "paired": "Paired", + "pairedInfo": "Paired bsk {{daemon}} · Device {{device}}", + "newLink": "Use a new pairing link", + "pairAction": "Pair this browser", + "unpaired": "Unpaired", + "whyPair": "Why is pairing recommended?", + "retry": "Retry", + "restore": "Restore verification", + "restoreHint": "Verification restored. Run bsk daemon trust-local off in your terminal to restore daemon verification too.", + "advanced": "Advanced", + "skipLabel": "Skip local verification", + "skipConfirmTitle": "Skip verification?", + "skipConfirm": "Any local program could impersonate bsk and control this browser. The daemon must also explicitly allow unverified connections.", + "confirmSkip": "Confirm skip", + "settingsFailed": "Could not save this setting. Try again.", + "pairHint": "Run bsk daemon pair and paste its link here. Local and remote links are detected automatically.", + "pairPreserves": "The current connection stays available until the new pairing succeeds.", + "replaceTitle": "Replace pairing?", + "replaceConfirm": "This bsk is already paired. A successful replacement will end current tasks. If the link is rejected, the current pairing stays valid.", + "confirmReplace": "Continue pairing", + "resumeRemote": "Return to remote", + "message": { + "unpaired": "Connection identity is not verified. Malicious local software could impersonate a peer. Pairing is recommended; you can keep using this connection without it.", + "pairing_required": "Run bsk daemon pair in your terminal and paste the link below to pair this browser.", + "already_paired": "This browser is already paired with this bsk. To replace it, run bsk daemon pair --replace {{browser}} and paste the new link.", + "replace_mismatch": "This browser is {{instance}}; the replacement targets {{browser}}. No changes were made. Use bsk daemon pair for a new browser profile; revoke an unused device separately with bsk daemon revoke {{browser}}.", + "skip": "Local verification is skipped. Any local program could impersonate bsk and control this browser.", + "migration_old": "This connection is unverified and the local bsk is too old to pair. Run bsk update, then pair this browser.", + "migration_pair": "This connection is unverified. bsk will soon require pairing; complete it once now.", + "server_unverified": "The service failed verification. Another program may be using the port, or this bsk’s pairing may have been reset. Run bsk doctor to check.", + "daemon_too_old": "This bsk is too old for a secure connection. Run bsk update, then retry.", + "busy": "Another connection is verifying this device. Please retry shortly." + } } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/es-ES/extension.json b/packages/i18n/src/locales/es-ES/extension.json index 58f10c55f..6aae9704d 100644 --- a/packages/i18n/src/locales/es-ES/extension.json +++ b/packages/i18n/src/locales/es-ES/extension.json @@ -137,7 +137,7 @@ "connectionDestination": "Conectar a: {{address}}", "connectionPortHint": "Déjalo en blanco para restaurar el puerto predeterminado, {{port}}.", "connectionSaveWarning": "Guardar finaliza las tareas actuales y vuelve a conectar si el interruptor de conexión está activado.", - "connectionLocalWarning": "Cambiar a local finaliza las tareas actuales y elimina la credencial remota guardada. Volver a remoto requiere un nuevo enlace de emparejamiento.", + "connectionLocalWarning": "Cambiar a local finaliza las tareas actuales. La vinculación remota se conserva hasta que caduque la autorización y no se renueva en modo local.", "connectionCancel": "Cancelar cambios", "connectionLocalError": "No se pudo cambiar a local. Inténtalo de nuevo.", "connectionLocalSaved": "Se cambió al servicio local.", @@ -171,6 +171,44 @@ "notificationMessage": "Las tareas del navegador no están disponibles. Actualiza bsk en el equipo que ejecuta el servicio de navegador y comprueba que el servicio en ejecución también esté actualizado.", "toolbarTitle": "BrowserSkill: actualiza el servicio de navegador bsk", "notificationButton": "Ver cómo actualizar" + }, + "localAuth": { + "required": "Vinculación necesaria", + "verified": "Verificado", + "unverified": "Sin verificar", + "paired": "Vinculado", + "pairedInfo": "bsk {{daemon}} vinculado · Dispositivo {{device}}", + "newLink": "Usar un nuevo enlace", + "pairAction": "Vincular navegador", + "unpaired": "Sin vincular", + "whyPair": "¿Por qué se recomienda vincular?", + "retry": "Reintentar", + "restore": "Restaurar verificación", + "restoreHint": "Verificación restaurada. Ejecuta bsk daemon trust-local off en el terminal para restaurar también la verificación de bsk.", + "advanced": "Avanzado", + "skipLabel": "Omitir verificación local", + "skipConfirmTitle": "¿Omitir la verificación?", + "skipConfirm": "Cualquier programa local podría suplantar a bsk y controlar este navegador. bsk también debe permitir explícitamente las conexiones sin verificar.", + "confirmSkip": "Confirmar omisión", + "settingsFailed": "No se pudo guardar el ajuste. Inténtalo de nuevo.", + "pairHint": "Ejecuta bsk daemon pair y pega el enlace aquí. Los enlaces locales y remotos se detectan automáticamente.", + "pairPreserves": "La conexión actual sigue disponible hasta que la nueva vinculación tenga éxito.", + "replaceTitle": "¿Reemplazar vinculación?", + "replaceConfirm": "Este bsk ya está vinculado. Si el reemplazo tiene éxito, las tareas actuales finalizarán. Si se rechaza el enlace, la vinculación actual seguirá siendo válida.", + "confirmReplace": "Continuar vinculación", + "resumeRemote": "Volver al servidor remoto", + "message": { + "unpaired": "La identidad de la conexión no está verificada. Un programa local malicioso podría suplantar a una de las partes. Se recomienda vincular, pero puedes seguir sin hacerlo.", + "pairing_required": "Ejecuta bsk daemon pair en el terminal y pega el enlace a continuación.", + "already_paired": "Este navegador ya está vinculado. Para reemplazarlo, ejecuta bsk daemon pair --replace {{browser}} y pega el nuevo enlace.", + "replace_mismatch": "Este navegador es {{instance}}; el destino es {{browser}}. No se realizaron cambios. Usa bsk daemon pair para un perfil nuevo y bsk daemon revoke {{browser}} para revocar un dispositivo sin uso.", + "skip": "Se ha omitido la verificación local. Cualquier programa local podría suplantar a bsk y controlar este navegador.", + "migration_old": "Esta conexión no está verificada y bsk es demasiado antiguo para vincularse. Ejecuta bsk update y luego vincula el navegador.", + "migration_pair": "Esta conexión no está verificada. Pronto bsk exigirá vinculación; complétala una vez ahora.", + "server_unverified": "El servicio no superó la verificación. Otro programa podría estar usando el puerto o se restableció la vinculación. Compruébalo con bsk doctor.", + "daemon_too_old": "bsk es demasiado antiguo para una conexión segura. Ejecuta bsk update y reintenta.", + "busy": "Otra conexión está verificando este dispositivo. Reintenta en un momento." + } } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/fr-FR/extension.json b/packages/i18n/src/locales/fr-FR/extension.json index 238070e69..4078a2d73 100644 --- a/packages/i18n/src/locales/fr-FR/extension.json +++ b/packages/i18n/src/locales/fr-FR/extension.json @@ -137,7 +137,7 @@ "connectionDestination": "Se connecter à : {{address}}", "connectionPortHint": "Laissez vide pour restaurer le port par défaut, {{port}}.", "connectionSaveWarning": "L'enregistrement met fin aux tâches en cours et reconnecte si le commutateur de connexion est activé.", - "connectionLocalWarning": "Passer en local met fin aux tâches en cours et supprime l'identifiant distant enregistré. Revenir en distant nécessite un nouveau lien de couplage.", + "connectionLocalWarning": "Le passage en local termine les tâches en cours. L’association distante est conservée jusqu’à l’expiration de l’autorisation, sans renouvellement en mode local.", "connectionCancel": "Annuler les modifications", "connectionLocalError": "Impossible de passer en local. Veuillez réessayer.", "connectionLocalSaved": "Basculé vers le service local.", @@ -171,6 +171,44 @@ "notificationMessage": "Les tâches de navigateur sont indisponibles. Mettez à jour bsk sur l’ordinateur qui exécute le service et vérifiez que le service en cours d’exécution est également à jour.", "toolbarTitle": "BrowserSkill : mise à jour du service de navigateur bsk requise", "notificationButton": "Voir les étapes de mise à jour" + }, + "localAuth": { + "required": "Association requise", + "verified": "Vérifié", + "unverified": "Non vérifié", + "paired": "Associé", + "pairedInfo": "bsk {{daemon}} associé · Appareil {{device}}", + "newLink": "Utiliser un nouveau lien", + "pairAction": "Associer ce navigateur", + "unpaired": "Non associé", + "whyPair": "Pourquoi associer les appareils ?", + "retry": "Réessayer", + "restore": "Rétablir la vérification", + "restoreHint": "Vérification rétablie. Exécutez bsk daemon trust-local off dans le terminal pour rétablir aussi la vérification côté bsk.", + "advanced": "Avancé", + "skipLabel": "Ignorer la vérification locale", + "skipConfirmTitle": "Ignorer la vérification ?", + "skipConfirm": "Un programme local pourrait se faire passer pour bsk et contrôler ce navigateur. bsk doit aussi autoriser explicitement les connexions non vérifiées.", + "confirmSkip": "Confirmer", + "settingsFailed": "Impossible d’enregistrer ce réglage. Réessayez.", + "pairHint": "Exécutez bsk daemon pair et collez le lien ici. Les liens locaux et distants sont reconnus automatiquement.", + "pairPreserves": "La connexion actuelle reste disponible jusqu’à la réussite de la nouvelle association.", + "replaceTitle": "Remplacer l’association ?", + "replaceConfirm": "Ce bsk est déjà associé. Un remplacement réussi mettra fin aux tâches en cours. Si le lien est refusé, l’association actuelle reste valide.", + "confirmReplace": "Continuer l’association", + "resumeRemote": "Revenir au serveur distant", + "message": { + "unpaired": "L’identité de la connexion n’est pas vérifiée. Un logiciel local malveillant pourrait usurper celle d’un participant. L’association est recommandée, mais vous pouvez continuer sans elle.", + "pairing_required": "Exécutez bsk daemon pair dans le terminal et collez le lien ci-dessous.", + "already_paired": "Ce navigateur est déjà associé. Pour remplacer l’association, exécutez bsk daemon pair --replace {{browser}} et collez le nouveau lien.", + "replace_mismatch": "Ce navigateur est {{instance}} ; la cible est {{browser}}. Aucune modification. Pour un nouveau profil, utilisez bsk daemon pair ; révoquez un appareil inutilisé avec bsk daemon revoke {{browser}}.", + "skip": "La vérification locale est ignorée. Un programme local pourrait se faire passer pour bsk et contrôler ce navigateur.", + "migration_old": "Connexion non vérifiée : bsk est trop ancien pour l’association. Exécutez bsk update, puis associez le navigateur.", + "migration_pair": "Cette connexion n’est pas vérifiée. bsk exigera bientôt une association ; effectuez-la une fois maintenant.", + "server_unverified": "Échec de la vérification. Un autre programme utilise peut-être le port ou l’association a été réinitialisée. Vérifiez avec bsk doctor.", + "daemon_too_old": "bsk est trop ancien pour une connexion sécurisée. Exécutez bsk update et réessayez.", + "busy": "Une autre connexion vérifie cet appareil. Réessayez dans un instant." + } } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/it-IT/extension.json b/packages/i18n/src/locales/it-IT/extension.json index 2a618f969..7552b72ea 100644 --- a/packages/i18n/src/locales/it-IT/extension.json +++ b/packages/i18n/src/locales/it-IT/extension.json @@ -137,7 +137,7 @@ "connectionDestination": "Connetti a: {{address}}", "connectionPortHint": "Lascia vuoto per ripristinare la porta predefinita, {{port}}.", "connectionSaveWarning": "Il salvataggio termina le attività correnti e si riconnette se l'interruttore di connessione è attivo.", - "connectionLocalWarning": "Passare a locale termina le attività correnti e rimuove la credenziale remota salvata. Per tornare a remoto è necessario un nuovo link di associazione.", + "connectionLocalWarning": "Il passaggio alla modalità locale termina le attività correnti. L’associazione remota resta disponibile fino alla scadenza e non viene rinnovata in modalità locale.", "connectionCancel": "Annulla modifiche", "connectionLocalError": "Impossibile passare a locale. Riprova.", "connectionLocalSaved": "Passato al servizio locale.", @@ -171,6 +171,44 @@ "notificationMessage": "Le attività del browser non sono disponibili. Aggiorna bsk sul computer che esegue il servizio browser e verifica che anche il servizio in esecuzione sia aggiornato.", "toolbarTitle": "BrowserSkill: aggiorna il servizio browser bsk", "notificationButton": "Visualizza come aggiornare" + }, + "localAuth": { + "required": "Associazione necessaria", + "verified": "Verificato", + "unverified": "Non verificato", + "paired": "Associato", + "pairedInfo": "bsk {{daemon}} associato · Dispositivo {{device}}", + "newLink": "Usa un nuovo link", + "pairAction": "Associa il browser", + "unpaired": "Non associato", + "whyPair": "Perché è consigliata l’associazione?", + "retry": "Riprova", + "restore": "Ripristina verifica", + "restoreHint": "Verifica ripristinata. Esegui bsk daemon trust-local off nel terminale per ripristinare anche la verifica di bsk.", + "advanced": "Avanzate", + "skipLabel": "Salta verifica locale", + "skipConfirmTitle": "Saltare la verifica?", + "skipConfirm": "Un programma locale potrebbe impersonare bsk e controllare questo browser. Anche bsk deve consentire esplicitamente connessioni non verificate.", + "confirmSkip": "Conferma", + "settingsFailed": "Impossibile salvare questa impostazione. Riprova.", + "pairHint": "Esegui bsk daemon pair e incolla qui il link. I link locali e remoti vengono riconosciuti automaticamente.", + "pairPreserves": "La connessione attuale rimane disponibile finché la nuova associazione non riesce.", + "replaceTitle": "Sostituire l’associazione?", + "replaceConfirm": "Questo bsk è già associato. La sostituzione riuscita terminerà le attività correnti. Se il link viene rifiutato, l’associazione attuale rimane valida.", + "confirmReplace": "Continua associazione", + "resumeRemote": "Torna al server remoto", + "message": { + "unpaired": "L’identità della connessione non è verificata. Un software locale dannoso potrebbe impersonare una delle parti. L’associazione è consigliata, ma puoi continuare senza eseguirla.", + "pairing_required": "Esegui bsk daemon pair nel terminale e incolla il link qui sotto.", + "already_paired": "Questo browser è già associato. Per sostituirlo, esegui bsk daemon pair --replace {{browser}} e incolla il nuovo link.", + "replace_mismatch": "Questo browser è {{instance}}; la destinazione è {{browser}}. Nessuna modifica. Usa bsk daemon pair per un nuovo profilo e bsk daemon revoke {{browser}} per revocare un dispositivo inutilizzato.", + "skip": "La verifica locale è saltata. Un programma locale potrebbe impersonare bsk e controllare questo browser.", + "migration_old": "Questa connessione non è verificata e bsk è troppo vecchio per l’associazione. Esegui bsk update, poi associa il browser.", + "migration_pair": "Questa connessione non è verificata. bsk richiederà presto l’associazione; completala una volta ora.", + "server_unverified": "Verifica del servizio fallita. Un altro programma potrebbe usare la porta o l’associazione è stata reimpostata. Controlla con bsk doctor.", + "daemon_too_old": "bsk è troppo vecchio per una connessione sicura. Esegui bsk update e riprova.", + "busy": "Un’altra connessione sta verificando questo dispositivo. Riprova tra poco." + } } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/ja-JP/extension.json b/packages/i18n/src/locales/ja-JP/extension.json index 55d028d03..6628a12fa 100644 --- a/packages/i18n/src/locales/ja-JP/extension.json +++ b/packages/i18n/src/locales/ja-JP/extension.json @@ -137,7 +137,7 @@ "connectionDestination": "接続先:{{address}}", "connectionPortHint": "空欄のままにすると、既定のポート {{port}} に戻ります。", "connectionSaveWarning": "保存すると現在のタスクが終了し、接続スイッチがオンの場合は再接続します。", - "connectionLocalWarning": "ローカルに切り替えると現在のタスクが終了し、保存されているリモートの認証情報が削除されます。リモートに戻るには新しいペアリングリンクが必要です。", + "connectionLocalWarning": "ローカルに切り替えると現在のタスクは終了します。リモートのペアリングは保持され、有効期限前なら戻せます。ローカル利用中はリモート認可を更新しません。", "connectionCancel": "変更をキャンセル", "connectionLocalError": "ローカルへの切り替えに失敗しました。もう一度お試しください。", "connectionLocalSaved": "ローカルサービスに切り替えました。", @@ -171,6 +171,44 @@ "notificationMessage": "ブラウザータスクを実行できません。ブラウザーサービスを実行しているコンピューターで bsk を更新し、実行中のサービスも更新されていることを確認してください。", "toolbarTitle": "BrowserSkill:bsk ブラウザーサービスの更新が必要です", "notificationButton": "更新手順を見る" + }, + "localAuth": { + "required": "ペアリングが必要", + "verified": "検証済み", + "unverified": "未検証", + "paired": "ペアリング済み", + "pairedInfo": "bsk {{daemon}} · デバイス {{device}} とペアリング済み", + "newLink": "新しいペアリングリンクを使う", + "pairAction": "ペアリングする", + "unpaired": "未ペアリング", + "whyPair": "ペアリングを勧める理由", + "retry": "再試行", + "restore": "検証を復元", + "restoreHint": "検証を復元しました。端末で bsk daemon trust-local off を実行して bsk 側の検証も復元してください。", + "advanced": "詳細設定", + "skipLabel": "ローカル検証をスキップ", + "skipConfirmTitle": "検証をスキップしますか?", + "skipConfirm": "ローカルのプログラムが bsk を装ってブラウザーを操作する可能性があります。bsk 側でも未検証の接続を明示的に許可する必要があります。", + "confirmSkip": "スキップを確認", + "settingsFailed": "設定を保存できませんでした。再試行してください。", + "pairHint": "bsk daemon pair を実行し、リンクを貼り付けてください。ローカルとリモートを自動判別します。", + "pairPreserves": "新しいペアリングが成功するまで現在の接続を維持します。", + "replaceTitle": "ペアリングを置き換えますか?", + "replaceConfirm": "この bsk はペアリング済みです。置き換えに成功すると現在のタスクは終了します。リンクが拒否された場合、現在のペアリングは有効なままです。", + "confirmReplace": "ペアリングを続行", + "resumeRemote": "リモートに戻る", + "message": { + "unpaired": "接続先の身元は未確認です。ローカルの悪意あるソフトウェアが相手になりすます可能性があります。ペアリングを推奨しますが、未設定でも利用できます。", + "pairing_required": "端末で bsk daemon pair を実行し、リンクを下に貼り付けてください。", + "already_paired": "このブラウザーはすでにペアリング済みです。置き換えるには bsk daemon pair --replace {{browser}} を実行し、新しいリンクを貼り付けてください。", + "replace_mismatch": "現在のブラウザーは {{instance}}、置き換え対象は {{browser}} です。変更はありません。新しいプロファイルには bsk daemon pair を使い、不要なデバイスは bsk daemon revoke {{browser}} で解除してください。", + "skip": "ローカル検証をスキップ中です。ローカルプログラムが bsk を装ってブラウザーを操作する可能性があります。", + "migration_old": "この接続は未検証で、bsk が古くペアリングに対応していません。bsk update で更新してからペアリングしてください。", + "migration_pair": "この接続は未検証です。bsk はまもなくペアリングを必須にします。一度だけ設定してください。", + "server_unverified": "サービスの検証に失敗しました。別のプログラムがポートを使用しているか、ペアリングがリセットされた可能性があります。bsk doctor で確認してください。", + "daemon_too_old": "bsk が古いため安全に接続できません。bsk update の実行後に再試行してください。", + "busy": "別の接続がこのデバイスを検証中です。しばらくして再試行してください。" + } } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/ko-KR/extension.json b/packages/i18n/src/locales/ko-KR/extension.json index 6454a002d..5fb566d4a 100644 --- a/packages/i18n/src/locales/ko-KR/extension.json +++ b/packages/i18n/src/locales/ko-KR/extension.json @@ -137,7 +137,7 @@ "connectionDestination": "연결 대상: {{address}}", "connectionPortHint": "비워 두면 기본 포트 {{port}}을 사용합니다.", "connectionSaveWarning": "저장하면 현재 작업이 종료됩니다. 연결 스위치가 켜져 있으면 다시 연결합니다.", - "connectionLocalWarning": "로컬로 전환하면 현재 작업이 종료되고 저장된 원격 자격 증명이 삭제됩니다. 다시 원격으로 연결하려면 새 페어링 링크가 필요합니다.", + "connectionLocalWarning": "로컬로 전환하면 현재 작업이 종료됩니다. 원격 페어링은 유지되며 승인 만료 전에는 바로 돌아갈 수 있습니다. 로컬 모드에서는 원격 승인을 갱신하지 않습니다.", "connectionCancel": "변경 취소", "connectionLocalError": "로컬로 전환하지 못했습니다. 다시 시도해 주세요.", "connectionLocalSaved": "로컬 서비스로 전환했습니다.", @@ -171,6 +171,44 @@ "notificationMessage": "브라우저 작업을 사용할 수 없습니다. 브라우저 서비스가 실행 중인 컴퓨터에서 bsk를 업데이트하고 실행 중인 서비스도 업데이트되었는지 확인하세요.", "toolbarTitle": "BrowserSkill: bsk 브라우저 서비스 업데이트 필요", "notificationButton": "업데이트 방법 보기" + }, + "localAuth": { + "required": "페어링 필요", + "verified": "검증됨", + "unverified": "미검증", + "paired": "페어링됨", + "pairedInfo": "페어링된 bsk {{daemon}} · 기기 {{device}}", + "newLink": "새 페어링 링크 사용", + "pairAction": "페어링하기", + "unpaired": "페어링 안 됨", + "whyPair": "페어링을 권장하는 이유", + "retry": "다시 시도", + "restore": "검증 복원", + "restoreHint": "검증이 복원되었습니다. 터미널에서 bsk daemon trust-local off를 실행하여 bsk 측 검증도 복원하세요.", + "advanced": "고급", + "skipLabel": "로컬 검증 건너뛰기", + "skipConfirmTitle": "검증을 건너뛸까요?", + "skipConfirm": "로컬 프로그램이 bsk를 사칭하여 이 브라우저를 제어할 수 있습니다. bsk 측에서도 미검증 연결을 명시적으로 허용해야 합니다.", + "confirmSkip": "건너뛰기 확인", + "settingsFailed": "설정을 저장할 수 없습니다. 다시 시도하세요.", + "pairHint": "bsk daemon pair를 실행하고 링크를 붙여 넣으세요. 로컬 및 원격 링크를 자동으로 구분합니다.", + "pairPreserves": "새 페어링이 성공할 때까지 현재 연결이 유지됩니다.", + "replaceTitle": "현재 페어링을 교체할까요?", + "replaceConfirm": "이 bsk는 이미 페어링되어 있습니다. 교체가 성공하면 현재 작업이 종료됩니다. 링크가 거부되면 기존 페어링이 유지됩니다.", + "confirmReplace": "페어링 계속", + "resumeRemote": "원격으로 돌아가기", + "message": { + "unpaired": "연결 상대의 신원이 확인되지 않았습니다. 로컬 악성 프로그램이 상대를 사칭할 수 있습니다. 페어링을 권장하지만 하지 않아도 계속 사용할 수 있습니다.", + "pairing_required": "터미널에서 bsk daemon pair를 실행하고 아래에 링크를 붙여 넣으세요.", + "already_paired": "이 브라우저는 이미 페어링되어 있습니다. 교체하려면 bsk daemon pair --replace {{browser}}를 실행하고 새 링크를 붙여 넣으세요.", + "replace_mismatch": "현재 브라우저는 {{instance}}이고 교체 대상은 {{browser}}입니다. 변경 사항이 없습니다. 새 프로필에는 bsk daemon pair를 사용하고, 사용하지 않는 기기는 bsk daemon revoke {{browser}}로 취소하세요.", + "skip": "로컬 검증을 건너뛰고 있습니다. 로컬 프로그램이 bsk를 사칭하여 브라우저를 제어할 수 있습니다.", + "migration_old": "연결이 미검증 상태이며 bsk가 너무 오래되어 페어링을 지원하지 않습니다. bsk update로 업데이트한 뒤 페어링하세요.", + "migration_pair": "이 연결은 미검증 상태입니다. 곧 bsk에 페어링이 필수가 되니 지금 한 번 설정하세요.", + "server_unverified": "서비스 검증에 실패했습니다. 다른 프로그램이 포트를 사용하거나 페어링이 초기화되었을 수 있습니다. bsk doctor로 확인하세요.", + "daemon_too_old": "bsk 버전이 오래되어 안전하게 연결할 수 없습니다. bsk update 실행 후 다시 시도하세요.", + "busy": "다른 연결이 이 기기를 검증 중입니다. 잠시 후 다시 시도하세요." + } } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/pt-BR/extension.json b/packages/i18n/src/locales/pt-BR/extension.json index 52491ab67..b6eda2129 100644 --- a/packages/i18n/src/locales/pt-BR/extension.json +++ b/packages/i18n/src/locales/pt-BR/extension.json @@ -137,7 +137,7 @@ "connectionDestination": "Conectar a: {{address}}", "connectionPortHint": "Deixe em branco para restaurar a porta padrão, {{port}}.", "connectionSaveWarning": "Salvar encerra as tarefas atuais e reconecta se o interruptor de conexão estiver ativado.", - "connectionLocalWarning": "Mudar para local encerra as tarefas atuais e remove a credencial remota salva. Voltar para remoto requer um novo link de pareamento.", + "connectionLocalWarning": "Mudar para local encerra as tarefas atuais. O pareamento remoto é mantido até a autorização expirar, sem renovação no modo local.", "connectionCancel": "Cancelar alterações", "connectionLocalError": "Não foi possível mudar para local. Tente novamente.", "connectionLocalSaved": "Mudou para o serviço local.", @@ -171,6 +171,44 @@ "notificationMessage": "As tarefas do navegador estão indisponíveis. Atualize o bsk no computador que executa o serviço de navegador e verifique se o serviço em execução também foi atualizado.", "toolbarTitle": "BrowserSkill: atualize o serviço de navegador bsk", "notificationButton": "Ver como atualizar" + }, + "localAuth": { + "required": "Pareamento necessário", + "verified": "Verificado", + "unverified": "Não verificado", + "paired": "Pareado", + "pairedInfo": "bsk {{daemon}} pareado · Dispositivo {{device}}", + "newLink": "Usar novo link de pareamento", + "pairAction": "Parear navegador", + "unpaired": "Não pareado", + "whyPair": "Por que o pareamento é recomendado?", + "retry": "Tentar novamente", + "restore": "Restaurar verificação", + "restoreHint": "Verificação restaurada. Execute bsk daemon trust-local off no terminal para restaurar também a verificação do bsk.", + "advanced": "Avançado", + "skipLabel": "Ignorar verificação local", + "skipConfirmTitle": "Ignorar a verificação?", + "skipConfirm": "Qualquer programa local pode se passar pelo bsk e controlar este navegador. O bsk também deve permitir explicitamente conexões não verificadas.", + "confirmSkip": "Confirmar", + "settingsFailed": "Não foi possível salvar esta configuração. Tente novamente.", + "pairHint": "Execute bsk daemon pair e cole o link aqui. Links locais e remotos são reconhecidos automaticamente.", + "pairPreserves": "A conexão atual permanece disponível até que o novo pareamento seja concluído.", + "replaceTitle": "Substituir pareamento?", + "replaceConfirm": "Este bsk já está pareado. Uma substituição bem-sucedida encerrará as tarefas atuais. Se o link for rejeitado, o pareamento atual continuará válido.", + "confirmReplace": "Continuar pareamento", + "resumeRemote": "Voltar ao servidor remoto", + "message": { + "unpaired": "A identidade da conexão não foi verificada. Um programa local malicioso pode se passar por uma das partes. O pareamento é recomendado, mas você pode continuar sem ele.", + "pairing_required": "Execute bsk daemon pair no terminal e cole o link abaixo.", + "already_paired": "Este navegador já está pareado. Para substituí-lo, execute bsk daemon pair --replace {{browser}} e cole o novo link.", + "replace_mismatch": "Este navegador é {{instance}}; o destino é {{browser}}. Nada foi alterado. Use bsk daemon pair para um novo perfil e bsk daemon revoke {{browser}} para revogar um dispositivo sem uso.", + "skip": "A verificação local foi ignorada. Qualquer programa local pode se passar pelo bsk e controlar este navegador.", + "migration_old": "Esta conexão não foi verificada e o bsk é antigo demais para parear. Execute bsk update e depois faça o pareamento.", + "migration_pair": "Esta conexão não foi verificada. Em breve o bsk exigirá pareamento; conclua-o uma vez agora.", + "server_unverified": "Falha na verificação do serviço. Outro programa pode estar usando a porta ou o pareamento foi redefinido. Verifique com bsk doctor.", + "daemon_too_old": "O bsk é antigo demais para uma conexão segura. Execute bsk update e tente novamente.", + "busy": "Outra conexão está verificando este dispositivo. Tente novamente em instantes." + } } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/zh-CN/extension.json b/packages/i18n/src/locales/zh-CN/extension.json index 694ce197f..65f8c96d2 100644 --- a/packages/i18n/src/locales/zh-CN/extension.json +++ b/packages/i18n/src/locales/zh-CN/extension.json @@ -137,7 +137,7 @@ "connectionDestination": "将连接到:{{address}}", "connectionPortHint": "留空恢复默认端口 {{port}}。", "connectionSaveWarning": "保存后会结束当前任务,并在连接开启时重新连接。", - "connectionLocalWarning": "切回本机将结束当前任务,并移除保存的远程凭据。再次连接远程需要新的配对链接。", + "connectionLocalWarning": "切回本机将结束当前任务。远程配对会保留,授权到期前可以直接切回;本机模式下不会续期远程授权。", "connectionCancel": "取消修改", "connectionLocalError": "切换到本机失败,请重试。", "connectionLocalSaved": "已切换到本机服务。", @@ -171,6 +171,44 @@ "notificationMessage": "浏览器任务暂不可用。请在运行 bsk 浏览器服务的电脑上升级 bsk,并确认正在运行的服务也已更新。", "toolbarTitle": "BrowserSkill:需要升级 bsk 浏览器服务", "notificationButton": "查看升级步骤" + }, + "localAuth": { + "required": "需要配对", + "verified": "已验证", + "unverified": "未验证", + "paired": "已配对", + "pairedInfo": "已配对 bsk {{daemon}} · 设备 {{device}}", + "newLink": "使用新的配对链接", + "pairAction": "去配对", + "unpaired": "未配对", + "whyPair": "为什么建议配对?", + "retry": "重试", + "restore": "恢复验证", + "restoreHint": "已恢复验证。请在终端运行 bsk daemon trust-local off,同时恢复 bsk 端验证。", + "advanced": "高级", + "skipLabel": "跳过本机验证", + "skipConfirmTitle": "确认跳过验证?", + "skipConfirm": "本机任何程序都可能冒充 bsk 控制此浏览器。bsk 端也需要明确允许未验证连接。", + "confirmSkip": "确认跳过", + "settingsFailed": "设置保存失败,请重试。", + "pairHint": "运行 bsk daemon pair,将链接粘贴到这里。支持自动识别本机与远程链接。", + "pairPreserves": "新配对成功前,当前连接继续可用。", + "replaceTitle": "替换当前配对?", + "replaceConfirm": "这台 bsk 已配对。替换成功后当前任务会结束;如果链接被拒绝,原配对继续有效。", + "confirmReplace": "继续配对", + "resumeRemote": "切回远程", + "message": { + "unpaired": "连接身份尚未验证,可能被本机恶意程序冒充。建议配对使用,未配对也可继续使用。", + "pairing_required": "在终端运行 bsk daemon pair,将输出的链接粘贴到下方,完成本机配对。", + "already_paired": "这个浏览器已与这台 bsk 配对,无需重新配对。如需替换,运行 bsk daemon pair --replace {{browser}},再粘贴新链接。", + "replace_mismatch": "当前浏览器是 {{instance}},替换目标是 {{browser}},未做任何改动。新浏览器配置请使用 bsk daemon pair;不用的设备可用 bsk daemon revoke {{browser}} 单独撤销。", + "skip": "已跳过本机验证:本机任何程序都可能冒充 bsk 控制此浏览器。", + "migration_old": "当前连接未验证,本机 bsk 版本还不支持配对。请运行 bsk update 升级,再完成配对。", + "migration_pair": "当前连接未验证。bsk 即将要求配对,建议现在完成,只需一次。", + "server_unverified": "服务未通过验证。可能是其它程序占用了端口,也可能是这台 bsk 的配对已被重置。请运行 bsk doctor 检查。", + "daemon_too_old": "本机 bsk 版本过旧,不支持安全连接。请运行 bsk update 后重试。", + "busy": "另一个连接正在验证此设备,请稍后重试。" + } } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/zh-TW/extension.json b/packages/i18n/src/locales/zh-TW/extension.json index 89712a0d7..5f36835bd 100644 --- a/packages/i18n/src/locales/zh-TW/extension.json +++ b/packages/i18n/src/locales/zh-TW/extension.json @@ -137,7 +137,7 @@ "connectionDestination": "將連線至:{{address}}", "connectionPortHint": "留空以還原預設連接埠 {{port}}。", "connectionSaveWarning": "儲存後會結束目前的工作,並在連線開關開啟時重新連線。", - "connectionLocalWarning": "切回本機將結束目前的工作,並移除已儲存的遠端憑證。再次連線遠端需要新的配對連結。", + "connectionLocalWarning": "切回本機將結束目前工作。遠端配對會保留,授權到期前可直接切回;本機模式不會續期遠端授權。", "connectionCancel": "取消變更", "connectionLocalError": "切換至本機失敗,請再試一次。", "connectionLocalSaved": "已切換至本機服務。", @@ -171,6 +171,44 @@ "notificationMessage": "暫時無法執行瀏覽器任務。請在執行 bsk 瀏覽器服務的電腦上升級 bsk,並確認正在執行的服務也已更新。", "toolbarTitle": "BrowserSkill:需要升級 bsk 瀏覽器服務", "notificationButton": "查看升級步驟" + }, + "localAuth": { + "required": "需要配對", + "verified": "已驗證", + "unverified": "未驗證", + "paired": "已配對", + "pairedInfo": "已配對 bsk {{daemon}} · 裝置 {{device}}", + "newLink": "使用新的配對連結", + "pairAction": "前往配對", + "unpaired": "未配對", + "whyPair": "為什麼建議配對?", + "retry": "重試", + "restore": "恢復驗證", + "restoreHint": "已恢復驗證。請在終端機執行 bsk daemon trust-local off,同時恢復 bsk 端驗證。", + "advanced": "進階", + "skipLabel": "略過本機驗證", + "skipConfirmTitle": "確認略過驗證?", + "skipConfirm": "本機任何程式都可能冒充 bsk 控制此瀏覽器。bsk 端也需要明確允許未驗證連線。", + "confirmSkip": "確認略過", + "settingsFailed": "無法儲存設定,請重試。", + "pairHint": "執行 bsk daemon pair,將連結貼到這裡。會自動辨識本機與遠端連結。", + "pairPreserves": "新配對成功前,目前連線仍可使用。", + "replaceTitle": "取代目前配對?", + "replaceConfirm": "這台 bsk 已配對。取代成功後目前工作將結束;若連結遭拒,原配對仍然有效。", + "confirmReplace": "繼續配對", + "resumeRemote": "切回遠端", + "message": { + "unpaired": "連線身分尚未驗證,可能遭本機惡意程式冒充。建議配對使用;未配對仍可繼續使用。", + "pairing_required": "在終端機執行 bsk daemon pair,將輸出的連結貼到下方,完成本機配對。", + "already_paired": "此瀏覽器已與這台 bsk 配對。如需取代,執行 bsk daemon pair --replace {{browser}},再貼上新連結。", + "replace_mismatch": "目前瀏覽器是 {{instance}},取代目標是 {{browser}},未做任何變更。新設定檔請用 bsk daemon pair;不用的裝置可用 bsk daemon revoke {{browser}} 撤銷。", + "skip": "已略過本機驗證:本機任何程式都可能冒充 bsk 控制此瀏覽器。", + "migration_old": "此連線尚未驗證,本機 bsk 版本不支援配對。請執行 bsk update 升級後配對。", + "migration_pair": "此連線尚未驗證。bsk 即將要求配對,建議現在完成一次配對。", + "server_unverified": "服務未通過驗證。可能是其他程式占用連接埠,或 bsk 配對已重設。請執行 bsk doctor 檢查。", + "daemon_too_old": "本機 bsk 版本過舊,不支援安全連線。請執行 bsk update 後重試。", + "busy": "另一條連線正在驗證此裝置,請稍後重試。" + } } }, "controlOverlay": {