Files
yaak-mountain-loop/apps/yaak-proxy/lib/httpsStrip.ts
T
Gregory SchierandClaude Opus 5.5 b0425069f4 Yaak Proxy: HTTPS decryption UI, and Info and Body in the card's tabs
The strip, quiet encrypted rows, warning rows for refused handshakes, the
encrypted card with named actions, trust at the first Decrypt, the toast
with Undo and Reconnect, the pill's Decrypt everything switch, Settings'
Decryption section, the Encrypted sidebar row, and two-step setup. Both
body tabs read Body with a type badge; Info leads the request half with
the timeline folded in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 22:30:40 -07:00

108 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { DecryptListEntry, Flow } from "@yaakapp-internal/proxy-lib";
import { decryptsAnything, entryMatches, isDecrypted } from "./decrypt";
import { connectionOf } from "./encryptedCard";
import { isSystemFlow, siteOf } from "./search";
/** What the strip above the list shows */
export type StripState =
/** Not shown */
| "hidden"
/** HTTPS is encrypted, with Decrypt Everything and Choose… */
| "offer"
/** The list decrypts something and the certificate isn't trusted: Trust, however dismissed */
| "broken";
export interface StripInput {
list: DecryptListEntry[];
/** Encrypted connections in the capture */
encrypted: number;
/** Its × was clicked, remembered */
dismissed: boolean;
/** The certificate is trusted, null until read */
trusted: boolean | null;
/** The app can trust the certificate itself, as on macOS */
canTrust: boolean;
}
/**
* Shows while nothing is decrypted and some HTTPS is encrypted, until dismissed; goes once
* anything is on the list, from anywhere. A list that decrypts something while the certificate
* isn't trusted is broken, and says so whatever else holds.
*/
export function stripState({
list,
encrypted,
dismissed,
trusted,
canTrust,
}: StripInput): StripState {
const listed = decryptsAnything(list);
if (listed && canTrust && trusted === false) return "broken";
if (listed || encrypted === 0 || dismissed) return "hidden";
return "offer";
}
/** "22 connections" */
export function connectionCount(n: number): string {
return `${n.toLocaleString()} ${n === 1 ? "connection" : "connections"}`;
}
export interface ChooseApp {
name: string;
bundleId: string | null;
path: string | null;
count: number;
checked: boolean;
}
export interface ChooseSite {
site: string;
count: number;
checked: boolean;
}
/** At most this many apps, and sites, in Choose… */
const CHOOSE_MAX = 8;
/**
* Choose…'s rows: the apps and sites of the encrypted connections seen, most connections first,
* each checked while the list decrypts it. System's daemons and what a built-in exclusion keeps
* out are left out, as decrypting those is deliberate, from the card or Settings.
*/
export function chooseOptions(
flows: Pick<Flow, "url" | "tunnelled" | "clientName" | "clientBundleId" | "clientPath">[],
list: DecryptListEntry[],
): { apps: ChooseApp[]; sites: ChooseSite[] } {
const builtIns = list.filter((e) => e.builtIn && e.entry.startsWith("!"));
const apps = new Map<string, ChooseApp>();
const sites = new Map<string, ChooseSite>();
for (const flow of flows) {
if (!flow.tunnelled || isSystemFlow(flow)) continue;
const c = connectionOf(flow);
const q = { host: c.host, port: c.port, app: c.app };
if (builtIns.some((e) => entryMatches(e.entry, q))) continue;
if (c.app != null) {
const app = apps.get(c.app.name) ?? {
name: c.app.name,
bundleId: c.app.bundleId,
path: flow.clientPath,
count: 0,
checked: isDecrypted(list, { kind: "app", ...c.app }),
};
app.count++;
apps.set(app.name, app);
}
const name = siteOf(c.label);
const site = sites.get(name) ?? {
site: name,
count: 0,
checked: isDecrypted(list, { kind: "site", site: name }),
};
site.count++;
sites.set(name, site);
}
const top = <T extends { count: number }>(rows: Iterable<T>) =>
[...rows].sort((a, b) => b.count - a.count).slice(0, CHOOSE_MAX);
return { apps: top(apps.values()), sites: top(sites.values()) };
}