Yaak Proxy: code-styled names and an info button in the encrypted card's why line

Hosts, apps and entries named in the why and opened-before lines render
as InlineCode, an info button beside the why line explains decryption in
a tooltip, and the "New connections only" line is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Gregory SchierandClaude Opus 5.5 authored and Stetta task tk_mwb56agvne2hw21h committed 2026-11-10 22:30:40 -07:00
1 parent ae666adfb7
commit f2555bdcd5
3 files changed
+90 -54

No files matched your search

+37 -8
View File
@@ -9,6 +9,8 @@ import {
IconButton,
type IconProps,
InlineCode,
Tooltip,
CONTROL_TOOLTIP_DELAY,
} from "@yaakapp-internal/ui";
import classNames from "classnames";
import { type ReactNode, useEffect, useState } from "react";
@@ -22,6 +24,7 @@ import {
encryptedCard,
isOpen,
isRejection,
type Line,
} from "../lib/encryptedCard";
import { useFlowActions } from "../lib/flowActions";
import { utcTimestamp } from "../lib/flowViews";
@@ -122,17 +125,22 @@ export function EncryptedCard({ flow }: { flow: Flow }) {
<div className="mx-3 border-t border-border-subtle overflow-auto">
<div className="mx-auto max-w-[27rem] py-10 grid gap-2">
<div className="text-sm text-text mb-1">
{model.why}
{model.whyEntry != null && (
<>
{" "}
<InlineCode>{model.whyEntry}</InlineCode>
</>
)}
<LineText line={model.why} />
{model.whySub != null && <span className="text-text-subtle"> · {model.whySub}</span>}
<Tooltip wrap content={ABOUT} delay={CONTROL_TOOLTIP_DELAY}>
<IconButton
size="2xs"
icon="info"
iconColor="secondary"
title="About decryption"
className="ml-1 align-middle inline-flex!"
/>
</Tooltip>
</div>
{model.fact != null && (
<div className="text-sm text-text-subtle -mt-1 mb-1">{model.fact}</div>
<div className="text-sm text-text-subtle -mt-1 mb-1">
<LineText line={model.fact} />
</div>
)}
{model.actions.map((action) => (
<ActionButton
@@ -165,6 +173,27 @@ export function EncryptedCard({ flow }: { flow: Flow }) {
);
}
/** What the info button beside the why line says */
const ABOUT =
"HTTPS is encrypted between the app and the server. Yaak Proxy shows requests only for hosts and apps you decrypt, from their next connection on.";
/** A why or fact line, its hosts, apps and entries shown as code */
function LineText({ line }: { line: Line }) {
return (
<>
{line.map((part, i) =>
typeof part === "string" ? (
// oxlint-disable-next-line react/no-array-index-key -- the parts of one fixed line
<span key={i}>{part}</span>
) : (
// oxlint-disable-next-line react/no-array-index-key -- the parts of one fixed line
<InlineCode key={i}>{part.code}</InlineCode>
),
)}
</>
);
}
const ICONS: Record<Exclude<CardAction["icon"], "app">, IconProps["icon"]> = {
host: "globe",
everything: "layers",
+28 -22
View File
@@ -4,6 +4,7 @@ import {
connectionOf,
type EncryptedCardInput,
encryptedCard,
lineText,
stopDecryptingConnection,
} from "./encryptedCard";
@@ -67,7 +68,7 @@ describe("connectionOf", () => {
describe("the card of an encrypted connection", () => {
test("nothing decrypted yet: the host, the app and everything, each with what it writes", () => {
const c = card();
expect(c.why).toBe("Nothing is decrypted yet");
expect(lineText(c.why)).toBe("Nothing is decrypted yet");
expect(summary(c)).toEqual([
[
"Decrypt www.google.com",
@@ -77,21 +78,19 @@ describe("the card of an encrypted connection", () => {
["Decrypt Dia", "app:Dia", { add: ["app:Dia"], remove: [] }],
["Decrypt Everything", "*", { add: ["*"], remove: [] }],
]);
expect(c.foot).toEqual(["New connections only"]);
// Nothing under the buttons while the certificate is trusted
expect(c.foot).toEqual([]);
expect(c.fact).toBeNull();
});
test("asks for the password once while the certificate isn't trusted, where it can be", () => {
expect(card({ trusted: false }).foot).toEqual([
"New connections only",
"Asks for your password once",
]);
expect(card({ trusted: false, canTrust: false }).foot).toEqual(["New connections only"]);
expect(card({ trusted: false }).foot).toEqual(["Asks for your password once"]);
expect(card({ trusted: false, canTrust: false }).foot).toEqual([]);
});
test("something else listed: not on the list, with the same choices", () => {
const c = card({ list: [...user("host:api.stripe.com"), ...BUILT_INS] });
expect(c.why).toBe("Decryption not configured for www.google.com");
expect(lineText(c.why)).toBe("Decryption not configured for `www.google.com`");
expect(c.actions.map((a) => a.label)).toEqual([
"Decrypt www.google.com",
"Decrypt Dia",
@@ -108,11 +107,11 @@ describe("the card of an encrypted connection", () => {
test("names the host nothing on the list covers, its port only when not 443", () => {
const list = [...user("host:api.stripe.com"), ...BUILT_INS];
expect(card({ list, flow: { ...tunnel, url: "t.yaak.app:443" } }).why).toBe(
"Decryption not configured for t.yaak.app",
expect(lineText(card({ list, flow: { ...tunnel, url: "t.yaak.app:443" } }).why)).toBe(
"Decryption not configured for `t.yaak.app`",
);
expect(card({ list, flow: { ...tunnel, url: "home.ts.net:8317" } }).why).toBe(
"Decryption not configured for home.ts.net:8317",
expect(lineText(card({ list, flow: { ...tunnel, url: "home.ts.net:8317" } }).why)).toBe(
"Decryption not configured for `home.ts.net:8317`",
);
});
@@ -139,7 +138,10 @@ describe("the card of an encrypted connection", () => {
tunnelExcludedBuiltIn: true,
},
});
expect([pinned.why, pinned.whySub]).toEqual(["Excluded by default", "pins its certificate"]);
expect([lineText(pinned.why), pinned.whySub]).toEqual([
"Excluded by default",
"pins its certificate",
]);
expect(summary(pinned)).toEqual([
[
"Decrypt my.1password.com",
@@ -171,7 +173,7 @@ describe("the card of an encrypted connection", () => {
list,
flow: { ...tunnel, tunnelReason: "excluded", tunnelExcludedBy: "!host:*.google.com" },
});
expect([c.why, c.whyEntry]).toEqual(["Excluded by", "!host:*.google.com"]);
expect(lineText(c.why)).toBe("Excluded by `!host:*.google.com`");
// Nothing added wins over the person's own exclusion, so taking it off is the way in
expect(summary(c)).toEqual([
[
@@ -204,8 +206,10 @@ describe("the card of an encrypted connection", () => {
test("once the list decrypts it: opened before, the opposite, and Reconnect while open", () => {
const list = [...user("host:www.google.com"), ...BUILT_INS];
const closed = card({ list });
expect(closed.why).toBe("Not on the decrypt list");
expect(closed.fact).toBe("Opened before www.google.com was decrypted");
expect(lineText(closed.why)).toBe("Not on the decrypt list");
expect(closed.fact && lineText(closed.fact)).toBe(
"Opened before `www.google.com` was decrypted",
);
expect(summary(closed)).toEqual([
[
"Stop Decrypting www.google.com",
@@ -219,7 +223,9 @@ describe("the card of an encrypted connection", () => {
expect(open.actions.map((a) => a.kind)).toEqual(["reconnect", "stop"]);
const everything = card({ list: [...user("*"), ...BUILT_INS] });
expect(everything.fact).toBe("Opened before everything was decrypted");
expect(everything.fact && lineText(everything.fact)).toBe(
"Opened before everything was decrypted",
);
expect(everything.actions[0]?.label).toBe("Stop Decrypting Everything");
});
});
@@ -240,7 +246,7 @@ describe("the card of a refused handshake", () => {
test("an app: likely pinned, Stop Decrypting first, Try Again while passed through", () => {
const c = card({ flow: refused, list: listed, passthrough: ["sync.acme.dev:443"] });
expect(c.rejected).toBe(true);
expect([c.why, c.whySub]).toEqual(["Acme rejected the certificate", "likely pinned"]);
expect([lineText(c.why), c.whySub]).toEqual(["Acme rejected the certificate", "likely pinned"]);
expect(summary(c)).toEqual([
["Stop Decrypting Acme", "!app:Acme", { add: ["!app:Acme"], remove: [] }],
["Try Again", "next connection", null],
@@ -264,7 +270,7 @@ describe("the card of a refused handshake", () => {
clientPath: "/opt/homebrew/bin/python3",
};
const c = card({ flow: python, list: listed, passthrough: ["pypi.org:443"] });
expect([c.why, c.whySub]).toEqual([
expect([lineText(c.why), c.whySub]).toEqual([
"python3 rejected the certificate",
"uses its own trust store",
]);
@@ -281,7 +287,7 @@ describe("the card of a refused handshake", () => {
flow: { ...refused, clientName: null, clientBundleId: null, clientPath: null },
list: listed,
});
expect(c.why).toBe("A client rejected the certificate");
expect(lineText(c.why)).toBe("A client rejected the certificate");
expect(summary(c)[0]).toEqual([
"Stop Decrypting sync.acme.dev",
"!host:sync.acme.dev",
@@ -296,7 +302,7 @@ describe("the card of a refused handshake", () => {
trusted: false,
passthrough: ["sync.acme.dev:443"],
});
expect(c.why).toBe("Certificate not trusted");
expect(lineText(c.why)).toBe("Certificate not trusted");
expect(c.actions.map((a) => a.kind)).toEqual(["trust", "stop"]);
expect(c.foot).toEqual(["Passed through until trusted"]);
});
@@ -312,7 +318,7 @@ describe("the card of a refused handshake", () => {
list: listed,
rejectedBy: { clientName: "Acme", clientBundleId: "dev.acme", clientPath: null },
});
expect(c.why).toBe("Acme rejected the certificate for this host");
expect(lineText(c.why)).toBe("Acme rejected the certificate for this host");
// Stop Decrypting acts on this connection, so on Dia's own
expect(c.actions[0]?.hint).toBe("!app:Dia");
});
+25 -24
View File
@@ -78,16 +78,22 @@ export interface CardAction {
icon: "host" | "app" | "everything" | "lock" | "lock_open" | "refresh" | "shield" | "terminal";
}
/** A line of text, the hosts, apps and entries in it shown as code, as Yaak shows literal values */
export type Line = (string | { code: string })[];
/** A line as plain text, its code in backticks */
export function lineText(line: Line): string {
return line.map((part) => (typeof part === "string" ? part : `\`${part.code}\``)).join("");
}
export interface EncryptedCardModel {
rejected: boolean;
/** Why the connection is encrypted, in one line */
why: string;
why: Line;
/** Quieter, after it */
whySub: string | null;
/** The exclusion that kept it out, when the person's own, shown as code after "Excluded by" */
whyEntry: string | null;
/** A fact about the connection since the list changed: when it opened */
fact: string | null;
fact: Line | null;
actions: CardAction[];
/** Small lines under the buttons */
foot: string[];
@@ -142,8 +148,8 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
const c = connectionOf(flow);
const q = queryOf(c);
const decryptsNow = decrypts(list, q);
const { why, whySub, whyEntry } = whyLine(flow, list, c, decryptsNow);
const foot = ["New connections only"];
const { why, whySub } = whyLine(flow, list, c, decryptsNow);
const foot: string[] = [];
if (decryptsNow) {
// Decrypted from the next connection on: the entries that do it, each to take back.
@@ -155,14 +161,13 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
actions.unshift({ kind: "reconnect", label: "Reconnect", hint: null, icon: "refresh" });
}
const first = decrypting[0]?.entry;
const what =
first == null ? c.label : first === EVERYTHING ? "everything" : entrySubject(first);
const what: Line[number] =
first === EVERYTHING ? "everything" : { code: first == null ? c.label : entrySubject(first) };
return {
rejected: false,
why,
whySub,
whyEntry,
fact: `Opened before ${what} was decrypted`,
fact: ["Opened before ", what, " was decrypted"],
actions,
foot: [],
};
@@ -230,7 +235,6 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
rejected: false,
why,
whySub,
whyEntry,
fact: null,
actions,
foot: actions.length === 0 ? [] : foot,
@@ -246,24 +250,23 @@ function whyLine(
list: DecryptListEntry[],
c: Connection,
decryptsNow: boolean,
): { why: string; whySub: string | null; whyEntry: string | null } {
): { why: Line; whySub: string | null } {
if (flow.tunnelReason === "excluded" && flow.tunnelExcludedBy != null) {
if (flow.tunnelExcludedBuiltIn) {
const kind = list.find((e) => e.entry === flow.tunnelExcludedBy)?.kind;
const whySub =
kind === "apple" ? "Apple service" : kind === "pinned" ? "pins its certificate" : null;
return { why: "Excluded by default", whySub, whyEntry: null };
return { why: ["Excluded by default"], whySub };
}
return { why: "Excluded by", whySub: null, whyEntry: flow.tunnelExcludedBy };
return { why: ["Excluded by ", { code: flow.tunnelExcludedBy }], whySub: null };
}
return {
why: !decryptsAnything(list)
? "Nothing is decrypted yet"
? ["Nothing is decrypted yet"]
: decryptsNow
? "Not on the decrypt list"
: `Decryption not configured for ${c.label}`,
? ["Not on the decrypt list"]
: ["Decryption not configured for ", { code: c.label }],
whySub: null,
whyEntry: null,
};
}
@@ -362,9 +365,8 @@ function rejectedCard(input: EncryptedCardInput): EncryptedCardModel {
if (canTrust && trusted === false && decryptsAnything(list)) {
return {
rejected: true,
why: "Certificate not trusted",
why: ["Certificate not trusted"],
whySub: null,
whyEntry: null,
fact: null,
actions: [
{ kind: "trust", label: "Trust Certificate", hint: null, primary: true, icon: "shield" },
@@ -378,9 +380,8 @@ function rejectedCard(input: EncryptedCardInput): EncryptedCardModel {
if (refuser.kind === "program") {
return {
rejected: true,
why: `${refuser.name} rejected the certificate${forHost}`,
why: [`${refuser.name} rejected the certificate${forHost}`],
whySub: "uses its own trust store",
whyEntry: null,
fact: null,
actions: [
{
@@ -398,12 +399,12 @@ function rejectedCard(input: EncryptedCardInput): EncryptedCardModel {
}
return {
rejected: true,
why:
why: [
refuser.kind === "app"
? `${refuser.name} rejected the certificate${forHost}`
: "A client rejected the certificate",
],
whySub: refuser.kind === "app" ? "likely pinned" : null,
whyEntry: null,
fact: null,
actions: [
...(stop == null ? [] : [{ ...stop, primary: true }]),