From f2555bdcd551ee008635f33efcc142068b46a671 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Nov 2026 15:57:04 -0700 Subject: [PATCH] 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 --- apps/yaak-proxy/components/EncryptedCard.tsx | 45 ++++++++++++++---- apps/yaak-proxy/lib/encryptedCard.test.ts | 50 +++++++++++--------- apps/yaak-proxy/lib/encryptedCard.ts | 49 +++++++++---------- 3 files changed, 90 insertions(+), 54 deletions(-) diff --git a/apps/yaak-proxy/components/EncryptedCard.tsx b/apps/yaak-proxy/components/EncryptedCard.tsx index 86bc6e8a7..96db399d3 100644 --- a/apps/yaak-proxy/components/EncryptedCard.tsx +++ b/apps/yaak-proxy/components/EncryptedCard.tsx @@ -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 }) {
- {model.why} - {model.whyEntry != null && ( - <> - {" "} - {model.whyEntry} - - )} + {model.whySub != null && ยท {model.whySub}} + + +
{model.fact != null && ( -
{model.fact}
+
+ +
)} {model.actions.map((action) => ( + {line.map((part, i) => + typeof part === "string" ? ( + // oxlint-disable-next-line react/no-array-index-key -- the parts of one fixed line + {part} + ) : ( + // oxlint-disable-next-line react/no-array-index-key -- the parts of one fixed line + {part.code} + ), + )} + + ); +} + const ICONS: Record, IconProps["icon"]> = { host: "globe", everything: "layers", diff --git a/apps/yaak-proxy/lib/encryptedCard.test.ts b/apps/yaak-proxy/lib/encryptedCard.test.ts index ee4b6edb4..69ea1f72a 100644 --- a/apps/yaak-proxy/lib/encryptedCard.test.ts +++ b/apps/yaak-proxy/lib/encryptedCard.test.ts @@ -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"); }); diff --git a/apps/yaak-proxy/lib/encryptedCard.ts b/apps/yaak-proxy/lib/encryptedCard.ts index 0223fc3d3..dd3027a9d 100644 --- a/apps/yaak-proxy/lib/encryptedCard.ts +++ b/apps/yaak-proxy/lib/encryptedCard.ts @@ -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 }]),