mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
Encrypt Yaak Proxy's databases at rest with SQLCipher (#823)
This commit is contained in:
1 parent
cc382b9226
commit
91696c2d1f
42 files changed
+3366
-286
No files matched your search
@@ -1,14 +1,16 @@
|
||||
import { Dropdown, type DropdownItem, IconButton } from "@yaakapp-internal/ui";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { clearFlowsAtom, flowTotalAtom } from "../lib/store";
|
||||
import { clearFlowsAtom, flowTotalAtom, lockedAtom } from "../lib/store";
|
||||
|
||||
/**
|
||||
* Clears every flow, once confirmed in its menu, which its hotkey opens too. The list empties at
|
||||
* once; the flows are deleted in the background.
|
||||
* once; the flows are deleted in the background. Disabled with none to clear, or while captured
|
||||
* traffic is locked.
|
||||
*/
|
||||
export function ClearFlowsButton() {
|
||||
const total = useAtomValue(flowTotalAtom);
|
||||
const locked = useAtomValue(lockedAtom);
|
||||
const clear = useSetAtom(clearFlowsAtom);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -45,6 +47,20 @@ export function ClearFlowsButton() {
|
||||
},
|
||||
];
|
||||
|
||||
// Nothing to clear, or nothing that can be read while traffic is locked: no menu, nor hotkey.
|
||||
if (locked != null || total === 0) {
|
||||
return (
|
||||
<IconButton
|
||||
className="pointer-events-auto"
|
||||
icon="trash"
|
||||
title={locked != null ? "Clear flows (traffic is locked)" : "Clear flows (none yet)"}
|
||||
size="sm"
|
||||
iconColor="secondary"
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown items={items} hotKeyAction="flows_clear">
|
||||
<IconButton
|
||||
|
||||
@@ -3,6 +3,7 @@ import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import type { ReactNode } from "react";
|
||||
import { useRpcMutation } from "../hooks/useRpcMutation";
|
||||
import { encryptingPercent } from "../lib/encrypting";
|
||||
import { proxyStateAtom } from "../lib/store";
|
||||
import { useCopyEnvironment, useProxySetup, useSystemProxySwitch } from "./ProxyStatus";
|
||||
|
||||
@@ -47,7 +48,9 @@ export function FirstRun() {
|
||||
? "next"
|
||||
: "todo";
|
||||
const ready = trusted && (systemOn || !systemSupported);
|
||||
const trafficState: StepState = !running && !otherOwner ? "fail" : ready ? "wait" : "todo";
|
||||
const encrypting = !running && !otherOwner ? encryptingPercent(proxyState) : null;
|
||||
const trafficState: StepState =
|
||||
encrypting != null ? "wait" : !running && !otherOwner ? "fail" : ready ? "wait" : "todo";
|
||||
|
||||
return (
|
||||
<div className="grid place-items-center h-full px-10 pb-16 pt-6">
|
||||
@@ -113,9 +116,17 @@ export function FirstRun() {
|
||||
<Step
|
||||
n={systemSupported ? 3 : 2}
|
||||
state={trafficState}
|
||||
title={trafficState === "wait" ? "Waiting for traffic" : "See traffic"}
|
||||
title={
|
||||
encrypting != null
|
||||
? `Encrypting stored traffic, ${encrypting}`
|
||||
: trafficState === "wait"
|
||||
? "Waiting for traffic"
|
||||
: "See traffic"
|
||||
}
|
||||
description={
|
||||
trafficState === "fail"
|
||||
encrypting != null
|
||||
? "Traffic captured before this version is being encrypted on disk. Recording starts when it's done."
|
||||
: trafficState === "fail"
|
||||
? (retry.error?.message ?? proxyState?.startError ?? "The proxy has not started.")
|
||||
: !recording
|
||||
? "Recording is paused, so traffic passes through without being stored."
|
||||
@@ -161,6 +172,10 @@ export function FirstRun() {
|
||||
Working with an agent? <InlineCode>yaak mcp</InlineCode> gives it the proxy's flows and
|
||||
actions.
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-text-subtle">
|
||||
Captured traffic is encrypted on disk with a key kept in your keychain, so macOS may ask
|
||||
to let Yaak Proxy use it.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ProxyLock } from "@yaakapp-internal/proxy-lib";
|
||||
import { Button, Icon, LoadingIcon } from "@yaakapp-internal/ui";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useRpcMutation } from "../hooks/useRpcMutation";
|
||||
import { encryptingPercent } from "../lib/encrypting";
|
||||
import { lockedLine } from "../lib/locked";
|
||||
import { proxyStateAtom } from "../lib/store";
|
||||
import { getOsType } from "../lib/tauri";
|
||||
|
||||
/**
|
||||
* Over the list and the detail pane while captured traffic can't be read: its key is
|
||||
* unavailable, or traffic from before encryption is being encrypted. Retry reads the key again
|
||||
* and opens it.
|
||||
*/
|
||||
export function LockedState({ lock }: { lock: ProxyLock }) {
|
||||
const proxyState = useAtomValue(proxyStateAtom);
|
||||
const retry = useRpcMutation("take_over");
|
||||
const encrypting = encryptingPercent(proxyState);
|
||||
|
||||
return (
|
||||
<div className="grid place-items-center h-full px-10 pb-16">
|
||||
<div className="flex flex-col items-center text-center gap-2 max-w-[28rem]">
|
||||
{encrypting != null ? (
|
||||
<LoadingIcon size="lg" className="text-text-subtle mb-1" />
|
||||
) : (
|
||||
<Icon icon="lock" size="lg" className="text-text-subtle mb-1" />
|
||||
)}
|
||||
<div className="text-lg font-semibold">
|
||||
{encrypting != null ? `Encrypting stored traffic, ${encrypting}` : "Traffic is locked"}
|
||||
</div>
|
||||
<div className="text-sm text-text-subtle">
|
||||
{encrypting != null
|
||||
? "Traffic captured before this version is being encrypted on disk. It shows here when it's done."
|
||||
: lockedLine(lock, getOsType())}
|
||||
</div>
|
||||
{encrypting == null && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="border"
|
||||
className="mt-2"
|
||||
isLoading={retry.isPending}
|
||||
onClick={() => retry.mutate({})}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
{encrypting == null && retry.error != null && (
|
||||
<div className="text-xs text-danger">{retry.error.message}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,12 +12,13 @@ import { useState } from "react";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import { flowLayoutAtom, settingsRequestedAtom } from "../lib/atoms";
|
||||
import { setupIncomplete } from "../lib/firstRun";
|
||||
import { noFlowsAtom, systemProxyEverOnAtom } from "../lib/store";
|
||||
import { lockedAtom, noFlowsAtom, systemProxyEverOnAtom } from "../lib/store";
|
||||
import { getOsType } from "../lib/tauri";
|
||||
import { ClearFlowsButton } from "./ClearFlowsButton";
|
||||
import { FirstRun } from "./FirstRun";
|
||||
import { FlowDetail } from "./FlowDetail";
|
||||
import { FlowList } from "./FlowList";
|
||||
import { LockedState } from "./LockedState";
|
||||
import { ProxyStatus, useProxySetup } from "./ProxyStatus";
|
||||
import { HelperDialog } from "./HelperDialog";
|
||||
import { SaveDialog } from "./SaveDialog";
|
||||
@@ -157,10 +158,12 @@ export function ProxyLayout() {
|
||||
/**
|
||||
* The flow list beside or above the detail pane, or, while setup is incomplete and there are no
|
||||
* flows, the first-run card across the whole area. Once setup is done an empty list says so
|
||||
* itself, after a Clear too.
|
||||
* itself, after a Clear too. While captured traffic is locked, the locked state takes the whole
|
||||
* area, or the first-run card does, saying why, while setup is incomplete too.
|
||||
*/
|
||||
function FlowsOrFirstRun({ flowLayout }: { flowLayout: SplitLayoutLayout }) {
|
||||
const noFlows = useAtomValue(noFlowsAtom);
|
||||
const locked = useAtomValue(lockedAtom);
|
||||
const noFlows = useAtomValue(noFlowsAtom) || locked != null;
|
||||
const { caStatus, systemProxy } = useProxySetup();
|
||||
const systemProxyEverOn = useAtomValue(systemProxyEverOnAtom);
|
||||
const incomplete = setupIncomplete({
|
||||
@@ -169,6 +172,7 @@ function FlowsOrFirstRun({ flowLayout }: { flowLayout: SplitLayoutLayout }) {
|
||||
systemProxyEverOn,
|
||||
});
|
||||
if (noFlows && incomplete) return <FirstRun />;
|
||||
if (locked != null) return <LockedState lock={locked} />;
|
||||
return (
|
||||
<SplitLayout
|
||||
storageKey="proxy_detail"
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type DropdownItem,
|
||||
type DropdownRef,
|
||||
Icon,
|
||||
LoadingIcon,
|
||||
} from "@yaakapp-internal/ui";
|
||||
import classNames from "classnames";
|
||||
import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
|
||||
@@ -16,6 +17,7 @@ import { useRpcEvent } from "../hooks/useRpcEvent";
|
||||
import { useRpcMutation } from "../hooks/useRpcMutation";
|
||||
import { useRpcQuery } from "../hooks/useRpcQuery";
|
||||
import { helperDialogOpenAtom, helperRegisteredAtAtom } from "../lib/atoms";
|
||||
import { encryptingPercent } from "../lib/encrypting";
|
||||
import { environmentExports } from "../lib/env";
|
||||
import { proxyStateAtom, refreshProxyState, systemProxyEverOnAtom } from "../lib/store";
|
||||
import {
|
||||
@@ -51,9 +53,17 @@ export function ProxyStatus() {
|
||||
const other = proxyState?.otherOwner ?? null;
|
||||
const port = proxyState?.port ?? other?.proxyPort ?? null;
|
||||
const untrusted = caStatus.data != null && !caStatus.data.trusted;
|
||||
// Traffic stored before encryption is being encrypted; the engine starts once it is.
|
||||
const encrypting = !running && other == null ? encryptingPercent(proxyState) : null;
|
||||
|
||||
const stateRow =
|
||||
other != null ? (
|
||||
encrypting != null ? (
|
||||
<MenuRow
|
||||
icon={<LoadingIcon size="sm" className="text-text-subtle" />}
|
||||
title={`Encrypting stored traffic, ${encrypting}`}
|
||||
description="Traffic captured before this version is being encrypted on disk. Recording starts when it's done."
|
||||
/>
|
||||
) : other != null ? (
|
||||
<MenuRow
|
||||
icon={<StateDot state="cli" />}
|
||||
title="Running from the CLI"
|
||||
@@ -252,8 +262,20 @@ export function ProxyStatus() {
|
||||
return (
|
||||
<>
|
||||
<Dropdown ref={dropdownRef} items={items}>
|
||||
<Pill color={!running && other == null && proxyState != null ? "warning" : undefined}>
|
||||
{other != null ? (
|
||||
<Pill
|
||||
color={
|
||||
!running && other == null && encrypting == null && proxyState != null
|
||||
? "warning"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{encrypting != null ? (
|
||||
<>
|
||||
<LoadingIcon size="xs" />
|
||||
<span className="text-text">Encrypting</span>
|
||||
<span className="font-mono text-text-subtle">{encrypting}</span>
|
||||
</>
|
||||
) : other != null ? (
|
||||
<>
|
||||
<StateDot state="cli" />
|
||||
<span className="text-text">CLI</span>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
|
||||
import { memo, type RefObject, useEffect, useRef } from "react";
|
||||
import {
|
||||
allTrafficTotalAtom,
|
||||
lockedAtom,
|
||||
searchTermsAtom,
|
||||
searchTextAtom,
|
||||
searchTextVersionAtom,
|
||||
@@ -24,6 +25,8 @@ export const SearchBar = memo(function SearchBar() {
|
||||
const setTerms = useSetAtom(searchTermsAtom);
|
||||
const setSearchText = useSetAtom(setSearchTextAtom);
|
||||
const version = useAtomValue(searchTextVersionAtom);
|
||||
// Nothing to search while captured traffic is locked.
|
||||
const locked = useAtomValue(lockedAtom) != null;
|
||||
// Read, not subscribed: `PlaceholderCount` keeps it current without re-rendering the field.
|
||||
const total = useStore().get(allTrafficTotalAtom);
|
||||
// PlainInput hands back its <input> as the ref
|
||||
@@ -59,7 +62,8 @@ export const SearchBar = memo(function SearchBar() {
|
||||
hideLabel
|
||||
size="sm"
|
||||
className="font-mono text-xs"
|
||||
placeholder={placeholderFor(total)}
|
||||
placeholder={locked ? "Traffic is locked" : placeholderFor(total)}
|
||||
disabled={locked}
|
||||
defaultValue={text}
|
||||
forceUpdateKey={String(version)}
|
||||
onChange={setText}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { HotkeyProvider } from "@yaakapp-internal/ui";
|
||||
import type { GetProxyStateResponse } from "@yaakapp-internal/proxy-lib";
|
||||
import { useStore } from "jotai";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { ProxyLayout } from "../components/ProxyLayout";
|
||||
import { useHotkeyContextValue } from "../lib/hotkeys";
|
||||
import { syncFlows, syncProxyState, syncSidebarSelection } from "../lib/store";
|
||||
|
||||
const LOCKED =
|
||||
"Traffic is locked: Yaak Proxy stores traffic encrypted and couldn't read its key from the " +
|
||||
"keychain: Encryption error: User canceled the operation. Allow it access, then retry.";
|
||||
|
||||
const STATE: GetProxyStateResponse = {
|
||||
state: "stopped",
|
||||
port: null,
|
||||
configuredPort: 9090,
|
||||
recording: true,
|
||||
systemProxyAtLaunch: true,
|
||||
httpVersion: "auto",
|
||||
startError: LOCKED.slice("Traffic is locked: ".length),
|
||||
encrypting: null,
|
||||
locked: { message: LOCKED, keychain: true },
|
||||
otherOwner: null,
|
||||
};
|
||||
|
||||
/** What the backend answers while the keychain refuses: the state, and every read refused */
|
||||
function answer(cmd: string): unknown {
|
||||
switch (cmd) {
|
||||
case "get_proxy_state":
|
||||
return STATE;
|
||||
case "ca_status":
|
||||
return { trusted: true, installed: true, certPath: "/tmp/yaak-proxy-ca.pem" };
|
||||
case "system_proxy_status":
|
||||
return {
|
||||
enabled: false,
|
||||
port: 9090,
|
||||
services: [],
|
||||
backend: "helper",
|
||||
helper: "enabled",
|
||||
supported: true,
|
||||
};
|
||||
case "list_actions":
|
||||
return { actions: [] };
|
||||
default:
|
||||
throw new Error(LOCKED);
|
||||
}
|
||||
}
|
||||
|
||||
/** Stands in for Tauri's IPC, answering as a locked proxy does */
|
||||
export function installLockedBackend() {
|
||||
const internals = (window as unknown as { __TAURI_INTERNALS__: Record<string, unknown> })
|
||||
.__TAURI_INTERNALS__;
|
||||
internals.invoke = async (cmd: string, args?: { cmd?: string }) => {
|
||||
if (cmd === "rpc") return answer(args?.cmd ?? "");
|
||||
if (cmd.startsWith("plugin:event|")) return 0;
|
||||
throw new Error(`Not in the harness: ${cmd}`);
|
||||
};
|
||||
(window as unknown as { __TAURI_OS_PLUGIN_INTERNALS__: Record<string, string> })
|
||||
.__TAURI_OS_PLUGIN_INTERNALS__ = {
|
||||
os_type: "macos",
|
||||
platform: "macos",
|
||||
family: "unix",
|
||||
version: "15.0",
|
||||
eol: "\n",
|
||||
};
|
||||
// Setup was done before, as for anyone whose traffic is locked after an upgrade.
|
||||
localStorage.setItem("system_proxy_ever_on", "true");
|
||||
}
|
||||
|
||||
/** The whole window, as the app renders it, while captured traffic is locked */
|
||||
export function LockedHarness() {
|
||||
const store = useStore();
|
||||
const [synced, setSynced] = useState(false);
|
||||
useEffect(() => {
|
||||
syncProxyState(store);
|
||||
syncFlows(store);
|
||||
syncSidebarSelection(store);
|
||||
setSynced(true);
|
||||
}, [store]);
|
||||
if (!synced) return null;
|
||||
return (
|
||||
<Hotkeys>
|
||||
<ProxyLayout />
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
function Hotkeys({ children }: { children: ReactNode }) {
|
||||
return <HotkeyProvider value={useHotkeyContextValue()}>{children}</HotkeyProvider>;
|
||||
}
|
||||
@@ -4,18 +4,26 @@ import { createStore, Provider } from "jotai";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { FlowPanesHarness } from "./FlowPanesHarness";
|
||||
import { installLockedBackend, LockedHarness } from "./LockedHarness";
|
||||
import { MessagesHarness } from "./MessagesHarness";
|
||||
import "../main.css";
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const dark = params.get("theme") !== "light";
|
||||
applyThemeToDocument(dark ? defaultDarkTheme : defaultLightTheme);
|
||||
if (params.has("locked")) installLockedBackend();
|
||||
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<Provider store={createStore()}>
|
||||
{params.has("stream") ? <MessagesHarness /> : <FlowPanesHarness />}
|
||||
{params.has("locked") ? (
|
||||
<LockedHarness />
|
||||
) : params.has("stream") ? (
|
||||
<MessagesHarness />
|
||||
) : (
|
||||
<FlowPanesHarness />
|
||||
)}
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { encryptingPercent } from "./encrypting";
|
||||
|
||||
describe("encryptingPercent", () => {
|
||||
const state = (encrypting: number | null) =>
|
||||
({ encrypting }) as Parameters<typeof encryptingPercent>[0];
|
||||
|
||||
test("is null when nothing is being encrypted", () => {
|
||||
expect(encryptingPercent(null)).toBeNull();
|
||||
expect(encryptingPercent(state(null))).toBeNull();
|
||||
});
|
||||
|
||||
test("rounds down and stays within 0 and 100", () => {
|
||||
expect(encryptingPercent(state(0))).toBe("0%");
|
||||
expect(encryptingPercent(state(0.426))).toBe("42%");
|
||||
expect(encryptingPercent(state(0.999))).toBe("99%");
|
||||
expect(encryptingPercent(state(1))).toBe("100%");
|
||||
expect(encryptingPercent(state(1.2))).toBe("100%");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { GetProxyStateResponse } from "@yaakapp-internal/proxy-lib";
|
||||
|
||||
/**
|
||||
* How far encrypting traffic stored before encryption has got, as "42%", while it runs; null
|
||||
* otherwise. Rounded down, so it reads 100% only once it is done.
|
||||
*/
|
||||
export function encryptingPercent(state: GetProxyStateResponse | null | undefined): string | null {
|
||||
const fraction = state?.encrypting;
|
||||
if (fraction == null) return null;
|
||||
return `${Math.floor(Math.min(Math.max(fraction, 0), 1) * 100)}%`;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { useCallback } from "react";
|
||||
import { base64ToBytes, bodyFileName } from "./body";
|
||||
import { curlBodyText, toCurl } from "./curl";
|
||||
import { rpc } from "./rpc";
|
||||
import { activeQueryAtom } from "./store";
|
||||
import { activeQueryAtom, lockedAtom } from "./store";
|
||||
import { useShowToast } from "./toast";
|
||||
|
||||
/** The flow the save dialog is open for */
|
||||
@@ -68,10 +68,13 @@ export async function exportHar(
|
||||
export function useExportShownHar(): () => DropdownItem {
|
||||
const showToast = useShowToast();
|
||||
const query = useAtomValue(activeQueryAtom);
|
||||
// Nothing can be read to export while captured traffic is locked.
|
||||
const locked = useAtomValue(lockedAtom) != null;
|
||||
return useCallback(
|
||||
() => ({
|
||||
label: query == null ? "Export All Flows as HAR" : "Export Matching Flows as HAR",
|
||||
icon: "download",
|
||||
disabled: locked,
|
||||
onSelect: async () => {
|
||||
try {
|
||||
const written = await exportHar(query == null ? {} : { filter: query }, "flows.har");
|
||||
@@ -86,7 +89,7 @@ export function useExportShownHar(): () => DropdownItem {
|
||||
}
|
||||
},
|
||||
}),
|
||||
[query, showToast],
|
||||
[query, locked, showToast],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { lockedLine } from "./locked";
|
||||
|
||||
describe("lockedLine", () => {
|
||||
test("points at the platform's key store when it refused", () => {
|
||||
const lock = { keychain: true, message: "Traffic is locked: anything" };
|
||||
expect(lockedLine(lock, "macos")).toBe(
|
||||
"Allow Yaak Proxy to use its key in Keychain Access, then retry.",
|
||||
);
|
||||
expect(lockedLine(lock, "windows")).toContain("Credential Manager");
|
||||
expect(lockedLine(lock, "linux")).toContain("your keyring");
|
||||
});
|
||||
|
||||
test("gives any other reason without the title's words", () => {
|
||||
const lock = {
|
||||
keychain: false,
|
||||
message: "Traffic is locked: the proxy is already running in the yaak CLI (pid 4).",
|
||||
};
|
||||
expect(lockedLine(lock, "macos")).toBe("The proxy is already running in the yaak CLI (pid 4).");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ProxyLock } from "@yaakapp-internal/proxy-lib";
|
||||
|
||||
/** What every refused read starts with, before its reason */
|
||||
const LOCKED_PREFIX = "Traffic is locked: ";
|
||||
|
||||
/** Where each platform keeps the key, by the name its users know it by */
|
||||
function keyStoreName(os: string): string {
|
||||
switch (os) {
|
||||
case "macos":
|
||||
return "Keychain Access";
|
||||
case "windows":
|
||||
return "Credential Manager";
|
||||
default:
|
||||
return "your keyring";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The locked state's one line: how to let Yaak Proxy read its key when the key store is what
|
||||
* refused, or else the reason itself, without the "Traffic is locked" its title already says.
|
||||
*/
|
||||
export function lockedLine(lock: ProxyLock, os: string): string {
|
||||
if (lock.keychain) {
|
||||
return `Allow Yaak Proxy to use its key in ${keyStoreName(os)}, then retry.`;
|
||||
}
|
||||
const reason = lock.message.startsWith(LOCKED_PREFIX)
|
||||
? lock.message.slice(LOCKED_PREFIX.length)
|
||||
: lock.message;
|
||||
return reason.charAt(0).toUpperCase() + reason.slice(1);
|
||||
}
|
||||
@@ -185,6 +185,9 @@ export const followAtom = atom(
|
||||
/** The engine's state, kept current from its events, and polled while another process has it */
|
||||
export const proxyStateAtom = atom<GetProxyStateResponse | null>(null);
|
||||
|
||||
/** Why captured traffic can't be read, while it can't: nothing that reads it is offered then */
|
||||
export const lockedAtom = atom((get) => get(proxyStateAtom)?.locked ?? null);
|
||||
|
||||
/** When recording was seen to pause, in this session; null while recording or not known */
|
||||
export const pausedAtAtom = atom<number | null>(null);
|
||||
|
||||
|
||||
Reference in new issue
Block a user