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 }]),