Compare commits

..
Author SHA1 Message Date
Gregory SchierandClaude Fable 5.2 6a34d0a46d Yaak Proxy: the rejected card claims decryption only when the list decrypts
Once an address is no longer passed through, the foot said the next
connection will be decrypted even when the list excludes it or the
certificate is untrusted. It now says so only when the list still
decrypts the connection, and keeps the trust line while untrusted.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 08:47:57 -07:00
Gregory SchierandClaude Fable 5.2 35058cc3be Yaak Proxy: prose pass over the UI copy
Apply the approved wording table: labels stay short, explanations are one
full sentence about the user's traffic, and detail moves behind an info
icon on the first-run card, the body banners, the Settings Helper row and
the rejected card. One word per thing: traffic and flows, recorded,
truncated, domains, searches, the CLI, Retry.

Backend messages that reach the window change with them, with the tests
and harness fixtures that assert on the old text. The two working files
for the pass are removed from the branch.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 08:41:49 -07:00
Gregory Schier 1f0541f379 Proxy prose pass 2026-11-11 08:13:49 -07:00
Gregory Schier f0e767d77e Proxy prose pass 2026-11-11 08:13:49 -07:00
Gregory Schier 075fcaf2c4 Proxy prose pass 2026-11-11 08:13:49 -07:00
69 changed files with 561 additions and 589 deletions

No files matched your search

Generated
+122 -140
View File
@@ -109,7 +109,7 @@ checksum = "f6f39be698127218cca460cb624878c9aa4e2b47dba3b277963d2bf00bad263b"
dependencies = [
"android_log-sys",
"env_filter 0.1.3",
"log 0.4.35",
"log 0.4.34",
]
[[package]]
@@ -211,7 +211,7 @@ checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917"
dependencies = [
"clipboard-win",
"image",
"log 0.4.35",
"log 0.4.34",
"objc2 0.6.5",
"objc2-app-kit",
"objc2-core-foundation",
@@ -304,9 +304,9 @@ dependencies = [
[[package]]
name = "async-compression"
version = "0.4.52"
version = "0.4.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ad47a740fb906fffdd891e8b6d5fde2511600f7bfaebd68939fa08a14999459"
checksum = "9d5755640120b24961f8231e05892e4a4f7e0d04e8aeca161508c9c397946970"
dependencies = [
"compression-codecs",
"compression-core",
@@ -676,9 +676,9 @@ dependencies = [
[[package]]
name = "block2"
version = "0.6.2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
dependencies = [
"objc2 0.6.5",
]
@@ -1186,9 +1186,9 @@ dependencies = [
[[package]]
name = "compression-codecs"
version = "0.4.47"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84f840cff84d10085260e460023934d0af699e71212b09b2a41452e8bd9cda43"
checksum = "cae36683101b2899f48bd31f8db9839437356188e3b1ff23096e73dab09ba8fe"
dependencies = [
"brotli 9.1.1",
"compression-core",
@@ -1334,7 +1334,7 @@ dependencies = [
"document-features",
"idna",
"indexmap 2.14.2",
"log 0.4.35",
"log 0.4.34",
"serde",
"serde_derive",
"serde_json",
@@ -1441,12 +1441,6 @@ dependencies = [
"libc",
]
[[package]]
name = "core_detect"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
[[package]]
name = "countme"
version = "3.0.1"
@@ -2013,7 +2007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.11.0",
"block2 0.6.2",
"block2 0.6.1",
"libc",
"objc2 0.6.5",
]
@@ -2223,16 +2217,11 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "encoding_rs"
version = "0.8.44"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89253175e704ab869b04f9db224d38630e0211a53841a79181deae0889e169f5"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
"core_detect",
"multiversion_no_op",
"rustversion",
"scopeguard",
"simdutf8",
]
[[package]]
@@ -2274,7 +2263,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"regex 1.13.1",
]
@@ -2284,7 +2273,7 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"regex 1.13.1",
]
@@ -2298,7 +2287,7 @@ dependencies = [
"anstyle",
"env_filter 2.0.0",
"jiff",
"log 0.4.35",
"log 0.4.34",
]
[[package]]
@@ -2359,7 +2348,7 @@ name = "eventsource-client"
version = "0.14.0"
source = "git+https://github.com/yaakapp/rust-eventsource-client#60e0e3ac5038149c4778dc4979b09b152214f9a8"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"pin-project",
]
@@ -2450,7 +2439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
dependencies = [
"colored",
"log 0.4.35",
"log 0.4.34",
]
[[package]]
@@ -2495,9 +2484,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.11"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626d4928326b646e70f65ed36fc7cf9f206bdb68a0019ee7c6b4709da4e084c4"
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
dependencies = [
"crc32fast",
"miniz_oxide 0.9.1",
@@ -2979,7 +2968,7 @@ dependencies = [
"bitflags 2.11.0",
"libc",
"libgit2-sys",
"log 0.4.35",
"log 0.4.34",
"openssl-sys",
]
@@ -3237,7 +3226,7 @@ version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"mac",
"markup5ever 0.14.1",
"match_token",
@@ -3249,7 +3238,7 @@ version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"markup5ever 0.38.0",
]
@@ -3421,7 +3410,7 @@ dependencies = [
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log 0.4.35",
"log 0.4.34",
"wasm-bindgen",
"windows-core 0.61.2",
]
@@ -3831,7 +3820,7 @@ dependencies = [
"defmt",
"jiff-core",
"jiff-static",
"log 0.4.35",
"log 0.4.34",
"portable-atomic",
"portable-atomic-util",
"serde_core",
@@ -3844,7 +3833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
dependencies = [
"defmt",
"log 0.4.35",
"log 0.4.34",
]
[[package]]
@@ -3869,7 +3858,7 @@ dependencies = [
"cfg-if",
"combine",
"jni-sys 0.3.0",
"log 0.4.35",
"log 0.4.34",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
@@ -3885,7 +3874,7 @@ dependencies = [
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log 0.4.35",
"log 0.4.34",
"simd_cesu8",
"thiserror 2.0.21",
"walkdir",
@@ -4014,7 +4003,7 @@ checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
dependencies = [
"byteorder",
"dbus-secret-service",
"log 0.4.35",
"log 0.4.34",
"security-framework 2.11.1",
"security-framework 3.7.0",
"windows-sys 0.60.2",
@@ -4075,7 +4064,7 @@ dependencies = [
"gtk",
"gtk-sys",
"libappindicator-sys",
"log 0.4.35",
"log 0.4.34",
]
[[package]]
@@ -4224,14 +4213,14 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
dependencies = [
"log 0.4.35",
"log 0.4.34",
]
[[package]]
name = "log"
version = "0.4.35"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lzma-rust2"
@@ -4359,7 +4348,7 @@ version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"phf 0.11.3",
"phf_codegen 0.11.3",
"string_cache 0.8.9",
@@ -4373,7 +4362,7 @@ version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"tendril 0.5.0",
"web_atoms",
]
@@ -4403,9 +4392,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "md5"
version = "0.8.1"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c"
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
[[package]]
name = "memchr"
@@ -4486,7 +4475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log 0.4.35",
"log 0.4.34",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
@@ -4498,7 +4487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
dependencies = [
"libc",
"log 0.4.35",
"log 0.4.34",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.61.2",
]
@@ -4524,12 +4513,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "multiversion_no_op"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
[[package]]
name = "nanoid"
version = "0.5.0"
@@ -4546,7 +4529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
dependencies = [
"libc",
"log 0.4.35",
"log 0.4.34",
"openssl",
"openssl-probe",
"openssl-sys",
@@ -4564,7 +4547,7 @@ checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
dependencies = [
"bitflags 2.11.0",
"jni-sys 0.3.0",
"log 0.4.35",
"log 0.4.34",
"ndk-sys",
"num_enum",
"raw-window-handle",
@@ -4688,7 +4671,7 @@ dependencies = [
"inotify",
"kqueue",
"libc",
"log 0.4.35",
"log 0.4.34",
"mio 1.2.4",
"notify-types",
"objc2-core-foundation",
@@ -4819,7 +4802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.11.0",
"block2 0.6.2",
"block2 0.6.1",
"libc",
"objc2 0.6.5",
"objc2-cloud-kit",
@@ -4968,7 +4951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.11.0",
"block2 0.6.2",
"block2 0.6.1",
"libc",
"objc2 0.6.5",
"objc2-core-foundation",
@@ -5074,7 +5057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
dependencies = [
"bitflags 2.11.0",
"block2 0.6.2",
"block2 0.6.1",
"objc2 0.6.5",
"objc2-cloud-kit",
"objc2-core-data",
@@ -5104,7 +5087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad"
dependencies = [
"bitflags 2.11.0",
"block2 0.6.2",
"block2 0.6.1",
"objc2 0.6.5",
"objc2-app-kit",
"objc2-core-foundation",
@@ -5179,9 +5162,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.118"
version = "0.9.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19c413d6ec9ebaf5e1c829d355cb3e9f783778d700f135be0921d52fce3c60ae"
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
dependencies = [
"cc",
"libc",
@@ -5231,7 +5214,7 @@ version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fc863e2ca13dc2d5c34fb22ea4a588248ac14db929616ba65c45f21744b1e9"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"serde",
"windows-sys 0.52.0",
]
@@ -6613,7 +6596,7 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"parking_lot",
"scheduled-thread-pool",
]
@@ -6935,7 +6918,7 @@ dependencies = [
"hyper-tls",
"hyper-util",
"js-sys",
"log 0.4.35",
"log 0.4.34",
"mime",
"mime_guess",
"native-tls",
@@ -6969,13 +6952,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"ashpd",
"block2 0.6.2",
"block2 0.6.1",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log 0.4.35",
"log 0.4.34",
"objc2 0.6.5",
"objc2-app-kit",
"objc2-core-foundation",
@@ -7014,9 +6997,9 @@ dependencies = [
[[package]]
name = "rmcp"
version = "3.9.0"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24a7e92f4bcaa36b97fa93c4fa7ca8f3e669eced949345b3462dba333c7f909"
checksum = "f8be15815186873ea063bf005011d16c356804973489e40c39116ae3a2878d1f"
dependencies = [
"async-trait",
"base64 0.23.1",
@@ -7035,7 +7018,6 @@ dependencies = [
"schemars 1.2.1",
"serde",
"serde_json",
"sha2 0.11.1",
"sse-stream",
"thiserror 2.0.21",
"tokio",
@@ -7048,9 +7030,9 @@ dependencies = [
[[package]]
name = "rmcp-macros"
version = "3.9.0"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3140f24a766f80f9ec1607bde85680959a69b83d46fa9ce87dadab7e67ab029"
checksum = "8b0748e054a5f5598b3213cf72099ae6b2745ba4d996c59ccf455150a7dbb839"
dependencies = [
"darling 0.24.1",
"proc-macro2",
@@ -7145,7 +7127,7 @@ dependencies = [
"inotify",
"kqueue",
"libc",
"log 0.4.35",
"log 0.4.34",
"mio 1.2.4",
"rolldown-notify-types",
"walkdir",
@@ -7158,7 +7140,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69128ae9ebf6d65638157e79dc9430e9341179aed9d85dde315ce6a702dba7e1"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"rolldown-file-id",
"rolldown-notify",
"rolldown-notify-types",
@@ -7579,7 +7561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
dependencies = [
"aws-lc-rs",
"log 0.4.35",
"log 0.4.34",
"once_cell",
"ring",
"rustls-pki-types",
@@ -7627,7 +7609,7 @@ dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni 0.22.4",
"log 0.4.35",
"log 0.4.34",
"once_cell",
"rustls",
"rustls-native-certs",
@@ -7889,7 +7871,7 @@ dependencies = [
"cssparser 0.29.6",
"derive_more 0.99.20",
"fxhash",
"log 0.4.35",
"log 0.4.34",
"phf 0.8.0",
"phf_codegen 0.8.0",
"precomputed-hash",
@@ -7906,7 +7888,7 @@ dependencies = [
"bitflags 2.11.0",
"cssparser 0.36.0",
"derive_more 2.1.1",
"log 0.4.35",
"log 0.4.34",
"new_debug_unreachable",
"phf 0.13.1",
"phf_codegen 0.13.1",
@@ -8388,7 +8370,7 @@ dependencies = [
"core-graphics 0.24.0",
"foreign-types 0.5.0",
"js-sys",
"log 0.4.35",
"log 0.4.34",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-quartz-core 0.2.2",
@@ -8714,7 +8696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4"
dependencies = [
"bitflags 2.11.0",
"block2 0.6.2",
"block2 0.6.1",
"core-foundation 0.10.1",
"core-graphics 0.25.0",
"crossbeam-channel",
@@ -8727,7 +8709,7 @@ dependencies = [
"gtk",
"jni 0.21.1",
"libc",
"log 0.4.35",
"log 0.4.34",
"ndk",
"ndk-sys",
"objc2 0.6.5",
@@ -8794,7 +8776,7 @@ dependencies = [
"http-range",
"jni 0.21.1",
"libc",
"log 0.4.35",
"log 0.4.34",
"mime",
"muda",
"objc2 0.6.5",
@@ -8909,7 +8891,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4136fb69d967753d000423d7e5f863f89bf949efbdfbecb43a580426a01a0194"
dependencies = [
"arboard",
"log 0.4.35",
"log 0.4.34",
"serde",
"serde_json",
"tauri",
@@ -8944,7 +8926,7 @@ version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61854a36651aa48381e5e209f69a01273b77f3f9f91f0c430b1b98d33bd47229"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"raw-window-handle",
"rfd",
"serde",
@@ -8965,7 +8947,7 @@ dependencies = [
"anyhow",
"dunce",
"glob",
"log 0.4.35",
"log 0.4.34",
"objc2-foundation 0.3.2",
"percent-encoding",
"schemars 0.8.22",
@@ -8988,7 +8970,7 @@ checksum = "86f2e3c40b77fd1eb7f8e1b6900fb036aee2da1ea2d3783c91255a82c636649d"
dependencies = [
"android_logger",
"fern",
"log 0.4.35",
"log 0.4.34",
"objc2 0.6.5",
"objc2-foundation 0.3.2",
"serde",
@@ -9030,7 +9012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997"
dependencies = [
"gethostname 1.0.2",
"log 0.4.35",
"log 0.4.34",
"os_info",
"serde",
"serde_json",
@@ -9048,7 +9030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8548af174c5516e4f71f142acea4d02e00316296ea9aafa58798851481003e3c"
dependencies = [
"encoding_rs",
"log 0.4.35",
"log 0.4.34",
"open",
"os_pipe",
"regex 1.13.1",
@@ -9091,7 +9073,7 @@ dependencies = [
"futures-util",
"http",
"infer",
"log 0.4.35",
"log 0.4.34",
"minisign-verify",
"osakit",
"percent-encoding",
@@ -9147,7 +9129,7 @@ dependencies = [
"html5ever 0.29.1",
"http",
"kuchikiki",
"log 0.4.35",
"log 0.4.34",
"objc2 0.6.5",
"objc2-app-kit",
"objc2-foundation 0.3.2",
@@ -9170,7 +9152,7 @@ dependencies = [
"gtk",
"http",
"jni 0.21.1",
"log 0.4.35",
"log 0.4.34",
"objc2 0.6.5",
"objc2-app-kit",
"objc2-web-kit",
@@ -9205,7 +9187,7 @@ dependencies = [
"infer",
"json-patch",
"kuchikiki",
"log 0.4.35",
"log 0.4.34",
"memchr",
"phf 0.13.1",
"plist",
@@ -9242,7 +9224,7 @@ dependencies = [
"http",
"infer",
"json-patch",
"log 0.4.35",
"log 0.4.34",
"memchr",
"phf 0.13.1",
"plist",
@@ -9548,7 +9530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71"
dependencies = [
"futures-util",
"log 0.4.35",
"log 0.4.34",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
@@ -9813,7 +9795,7 @@ version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -9857,7 +9839,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"once_cell",
"tracing-core",
]
@@ -9970,7 +9952,7 @@ dependencies = [
"data-encoding",
"http",
"httparse",
"log 0.4.35",
"log 0.4.34",
"rand 0.10.3",
"rustls",
"rustls-pki-types",
@@ -10163,7 +10145,7 @@ dependencies = [
"base64 0.22.1",
"cookie_store",
"flate2",
"log 0.4.35",
"log 0.4.34",
"percent-encoding",
"rustls",
"rustls-pki-types",
@@ -10184,7 +10166,7 @@ dependencies = [
"base64 0.22.1",
"http",
"httparse",
"log 0.4.35",
"log 0.4.34",
]
[[package]]
@@ -10249,9 +10231,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.29.1"
version = "1.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a06f08a856b009bc7bbaf7d3e79808595d032dd19074c0c2ef26e49e03e6bdc"
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
dependencies = [
"getrandom 0.4.3",
"js-sys",
@@ -10519,7 +10501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
dependencies = [
"dlib",
"log 0.4.35",
"log 0.4.34",
"pkg-config",
]
@@ -10557,12 +10539,12 @@ dependencies = [
[[package]]
name = "webbrowser"
version = "1.2.6"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8d20d0214027baf11b8b0cc307c1da7657d8f4db9aa956f7de27a6fb1acbd5"
checksum = "d2c8ddfbbf66e501434657400268cefe51b95140a0266236415cda7627732df7"
dependencies = [
"jni 0.22.4",
"log 0.4.35",
"log 0.4.34",
"ndk-context",
"objc2 0.6.5",
"objc2-app-kit",
@@ -11306,7 +11288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "959c7ac3b8afde864f7a7448ac16f29b2b83350b7eccbf77fb781c3999aa6848"
dependencies = [
"libc",
"log 0.4.35",
"log 0.4.34",
"os_pipe",
"rustix 1.1.5",
"thiserror 2.0.21",
@@ -11330,7 +11312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"
dependencies = [
"base64 0.22.1",
"block2 0.6.2",
"block2 0.6.1",
"cookie",
"crossbeam-channel",
"dirs 6.0.0",
@@ -11428,7 +11410,7 @@ dependencies = [
"async-trait",
"base64 0.23.1",
"chrono",
"log 0.4.35",
"log 0.4.34",
"md5",
"rusqlite",
"serde_json",
@@ -11450,7 +11432,7 @@ dependencies = [
name = "yaak-api"
version = "0.1.0"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"reqwest",
"sysproxy",
"thiserror 2.0.21",
@@ -11467,7 +11449,7 @@ dependencies = [
"cookie",
"eventsource-client",
"http",
"log 0.4.35",
"log 0.4.34",
"md5",
"mime_guess",
"notify",
@@ -11534,7 +11516,7 @@ dependencies = [
name = "yaak-app-proxy"
version = "0.0.0"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"serde_json",
"tauri",
"tauri-build",
@@ -11566,7 +11548,7 @@ dependencies = [
"include_dir",
"inquire",
"keyring",
"log 0.4.35",
"log 0.4.34",
"oxc_resolver",
"predicates",
"rand 0.10.3",
@@ -11638,7 +11620,7 @@ dependencies = [
"chacha20poly1305",
"dbus-secret-service",
"keyring",
"log 0.4.35",
"log 0.4.34",
"serde",
"tempfile",
"thiserror 2.0.21",
@@ -11652,7 +11634,7 @@ dependencies = [
"chrono",
"getrandom 0.3.4",
"include_dir",
"log 0.4.35",
"log 0.4.34",
"nanoid",
"openssl-sys",
"r2d2",
@@ -11697,7 +11679,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"git2",
"log 0.4.35",
"log 0.4.34",
"serde",
"serde_json",
"serde_yaml",
@@ -11720,7 +11702,7 @@ dependencies = [
"dunce",
"hyper-rustls",
"hyper-util",
"log 0.4.35",
"log 0.4.34",
"md5",
"prost",
"prost-reflect",
@@ -11751,7 +11733,7 @@ dependencies = [
"futures-util",
"http-body",
"hyper-util",
"log 0.4.35",
"log 0.4.34",
"mime_guess",
"native-tls",
"reqwest",
@@ -11776,7 +11758,7 @@ name = "yaak-license"
version = "0.1.0"
dependencies = [
"chrono",
"log 0.4.35",
"log 0.4.34",
"reqwest",
"serde",
"serde_json",
@@ -11793,7 +11775,7 @@ dependencies = [
name = "yaak-lifecycle"
version = "0.0.0"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"yaak-models",
]
@@ -11803,7 +11785,7 @@ version = "0.1.0"
dependencies = [
"cocoa",
"csscolorparser",
"log 0.4.35",
"log 0.4.34",
"objc",
"rand 0.10.3",
"tauri",
@@ -11818,7 +11800,7 @@ dependencies = [
"http-body-util",
"hyper",
"hyper-util",
"log 0.4.35",
"log 0.4.34",
"reqwest",
"rmcp",
"serde",
@@ -11836,7 +11818,7 @@ dependencies = [
"chrono",
"hex",
"include_dir",
"log 0.4.35",
"log 0.4.34",
"nanoid",
"r2d2",
"r2d2_sqlite",
@@ -11863,7 +11845,7 @@ dependencies = [
"bytes",
"clap",
"env_logger",
"log 0.4.35",
"log 0.4.34",
"serde",
"serde_json",
"tokio",
@@ -11882,7 +11864,7 @@ dependencies = [
"futures-util",
"hex",
"keyring",
"log 0.4.35",
"log 0.4.34",
"md5",
"path-slash",
"rand 0.10.3",
@@ -11906,7 +11888,7 @@ name = "yaak-proxy"
version = "0.1.0"
dependencies = [
"base64 0.23.1",
"block2 0.6.2",
"block2 0.6.1",
"bytes",
"dispatch2",
"flate2",
@@ -11916,7 +11898,7 @@ dependencies = [
"hyper",
"hyper-util",
"libc",
"log 0.4.35",
"log 0.4.34",
"pem",
"rcgen",
"reqwest",
@@ -11959,7 +11941,7 @@ dependencies = [
"hyper-util",
"include_dir",
"libc",
"log 0.4.35",
"log 0.4.34",
"objc2 0.6.5",
"objc2-app-kit",
"objc2-foundation 0.3.2",
@@ -12012,7 +11994,7 @@ dependencies = [
name = "yaak-rpc"
version = "0.0.0"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"serde",
"serde_json",
"ts-rs",
@@ -12048,7 +12030,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"hex",
"log 0.4.35",
"log 0.4.34",
"notify",
"serde",
"serde_json",
@@ -12068,7 +12050,7 @@ version = "0.1.0"
dependencies = [
"dark-light",
"dispatch2",
"log 0.4.35",
"log 0.4.34",
"objc2-app-kit",
"objc2-foundation 0.3.2",
"tauri",
@@ -12088,7 +12070,7 @@ name = "yaak-templates"
version = "0.1.0"
dependencies = [
"base64 0.23.1",
"log 0.4.35",
"log 0.4.34",
"serde",
"serde-wasm-bindgen",
"serde_json",
@@ -12102,7 +12084,7 @@ dependencies = [
name = "yaak-tls"
version = "0.1.0"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"p12",
"pem",
"rustls",
@@ -12119,7 +12101,7 @@ dependencies = [
name = "yaak-updater"
version = "0.1.0"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"md5",
"nanoid",
"serde",
@@ -12138,7 +12120,7 @@ version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"js-sys",
"log 0.4.35",
"log 0.4.34",
"serde",
"serde-wasm-bindgen",
"serde_json",
@@ -12163,7 +12145,7 @@ dependencies = [
"clap",
"env_logger",
"futures-util",
"log 0.4.35",
"log 0.4.34",
"serde",
"serde_json",
"tempfile",
@@ -12181,7 +12163,7 @@ dependencies = [
name = "yaak-window"
version = "0.1.0"
dependencies = [
"log 0.4.35",
"log 0.4.34",
"md5",
"rand 0.10.3",
"serde",
@@ -12196,7 +12178,7 @@ version = "0.1.0"
dependencies = [
"futures-util",
"http",
"log 0.4.35",
"log 0.4.34",
"md5",
"serde",
"serde_json",
@@ -12480,7 +12462,7 @@ checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log 0.4.35",
"log 0.4.34",
"simd-adler32",
]
@@ -47,27 +47,14 @@ export function ClearFlowsButton() {
<IconButton
className="pointer-events-auto"
icon="trash"
title={
locked != null
? "Clear flows (traffic is locked)"
: total === 0
? "Clear flows (none yet)"
: hotkey == null
? "Clear flows"
: `Clear flows ${hotkey}`
}
title={action === "disabled" || hotkey == null ? "Clear flows" : `Clear flows ${hotkey}`}
tooltip
size="sm"
iconColor="secondary"
disabled={action === "disabled"}
onClick={start}
/>
<Dialog
open={confirming}
onClose={() => setConfirming(false)}
size="sm"
title="Clear traffic?"
>
<Dialog open={confirming} onClose={() => setConfirming(false)} size="sm" title="Clear Flows">
{confirming && (
<ConfirmClear
total={total}
@@ -103,8 +90,8 @@ function ConfirmClear({
<form id={formId} className="flex flex-col gap-3" onSubmit={handleSubmit}>
<p>
{total === 1
? "1 flow will be removed."
: `${total.toLocaleString()} flows will be removed.`}
? "Permanently clear 1 flow?"
: `Permanently clear ${total.toLocaleString()} flows?`}
</p>
<Checkbox checked={dontAskAgain} title="Don't ask again" onChange={setDontAskAgain} />
<DialogFooter
@@ -26,7 +26,8 @@ import {
everythingEdit,
normalizeEntry,
} from "../lib/decrypt";
import { revealPath } from "../lib/opener";
import { revealLabel, revealPath } from "../lib/opener";
import { getOsType } from "../lib/tauri";
import { useShowToast } from "../lib/toast";
/**
@@ -75,21 +76,23 @@ export function DecryptionSettings() {
<CertificateRow />
<SettingRowBoolean
title="Decrypt everything"
description="Except built-in exclusions"
description="Excluded hosts and apps stay encrypted."
checked={everything}
disabled={busy}
onChange={(on) =>
void change(
everythingEdit(on),
on ? "Decrypting everything" : "Stopped decrypting everything",
on ? "Except Apple services and apps that pin" : undefined,
on ? "Excluded hosts and apps stay encrypted." : undefined,
)
}
/>
<div className="py-4 grid gap-2">
<div className="flex items-center text-text">
Hosts and apps
<span className="ml-auto text-sm text-text-subtlest">New connections</span>
<span className="ml-auto text-sm text-text-subtlest">
Changes apply to new connections.
</span>
</div>
<EntryInput
seenRef={seenRef}
@@ -106,7 +109,7 @@ export function DecryptionSettings() {
/>
<div className="rounded-md border border-border-subtle divide-y divide-border-subtle">
{own.length === 0 && (
<div className="px-3 py-2 text-sm text-text-subtle">Nothing listed</div>
<div className="px-3 py-2 text-sm text-text-subtle">No hosts or apps</div>
)}
{own.map((e) => (
<EntryRow key={e.entry} entry={e} disabled={busy} onRemove={() => remove(e.entry)} />
@@ -156,7 +159,7 @@ function CertificateRow() {
)
}
>
Reveal
{revealLabel(getOsType())}
</Button>
)}
</div>
@@ -286,7 +289,7 @@ function EntryInput({
if (disabledRef.current) return true;
const entry = normalizeEntry(text);
if (entry == null) {
setError("Not a host or app");
setError("Invalid host or app");
return true;
}
setError(null);
@@ -299,20 +302,18 @@ function EntryInput({
}, [seenRef]);
return (
<Tooltip wrap content="Applies to new connections" delay={CONTROL_TOOLTIP_DELAY}>
<div className="grid gap-1">
<Input
label="Add entry"
hideLabel
size="sm"
stateKey={null}
language={null}
placeholder="Add host or app"
extensions={extensions}
onChange={() => setError(null)}
/>
{error != null && <div className="text-xs text-danger">{error}</div>}
</div>
</Tooltip>
<div className="grid gap-1">
<Input
label="Add entry"
hideLabel
size="sm"
stateKey={null}
language={null}
placeholder="Add host or app"
extensions={extensions}
onChange={() => setError(null)}
/>
{error != null && <div className="text-xs text-danger">{error}</div>}
</div>
);
}
+2 -8
View File
@@ -126,8 +126,7 @@ export function EncryptedCard({ flow }: { flow: Flow }) {
<div className="mx-auto max-w-[27rem] py-10 grid gap-2">
<div className="text-sm text-text mb-1">
<LineText line={model.why} />
{model.whySub != null && <span className="text-text-subtle"> · {model.whySub}</span>}
<Tooltip wrap content={ABOUT} delay={CONTROL_TOOLTIP_DELAY}>
<Tooltip wrap content={model.tooltip ?? ABOUT} delay={CONTROL_TOOLTIP_DELAY}>
<IconButton
size="2xs"
icon="info"
@@ -137,11 +136,6 @@ export function EncryptedCard({ flow }: { flow: Flow }) {
/>
</Tooltip>
</div>
{model.fact != null && (
<div className="text-sm text-text-subtle -mt-1 mb-1">
<LineText line={model.fact} />
</div>
)}
{model.actions.map((action) => (
<ActionButton
key={action.label}
@@ -162,7 +156,7 @@ export function EncryptedCard({ flow }: { flow: Flow }) {
/>
))}
{model.foot.length > 0 && (
<div className="text-xs text-text-subtlest mt-0.5">{model.foot.join(" · ")}</div>
<div className="text-xs text-text-subtlest mt-0.5">{model.foot.join(" ")}</div>
)}
{flow.error != null && flow.tlsRejected == null && (
<div className="text-xs text-danger mt-1">{flow.error}</div>
+19 -17
View File
@@ -1,4 +1,4 @@
import { Button, Icon, InlineCode, LoadingIcon } from "@yaakapp-internal/ui";
import { Button, Icon, IconTooltip, InlineCode, LoadingIcon } from "@yaakapp-internal/ui";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import type { ReactNode } from "react";
@@ -49,7 +49,7 @@ export function FirstRun() {
<div className="text-xl font-semibold mb-1.5">Set up Yaak Proxy</div>
<div className="text-text-subtle text-sm leading-relaxed mb-4">
{systemSupported
? "Two steps, once. Then every app on this Mac sends its traffic here."
? "Traffic from every app on this Mac will be recorded."
: "Traffic appears here as apps send it."}
</div>
<div className="rounded-lg border border-border-subtle bg-surface divide-y divide-border-subtle">
@@ -57,12 +57,12 @@ export function FirstRun() {
<Step
n={1}
state={systemState}
title={systemOn ? "System proxy on" : "Turn on the system proxy"}
title={systemOn ? "System proxy overridden" : "Override system proxy"}
description={
otherOwner && !systemOn
? proxyState?.otherOwner?.owner === "cli"
? "The CLI owns the proxy. Take over to turn this on from here."
: "Another Yaak Proxy window owns the engine. Quit it to use this one."
? "Take over from the CLI to change this."
: "Quit the other Yaak Proxy to use this one."
: systemSwitch.description
}
action={
@@ -73,7 +73,7 @@ export function FirstRun() {
color={systemState === "fail" ? "default" : "primary"}
onClick={() => setSystemProxy.mutate({ enabled: true })}
>
{systemState === "fail" ? "Try Again" : "Turn On"}
{systemState === "fail" ? "Retry" : "Enable"}
</Button>
) : null
}
@@ -84,20 +84,20 @@ export function FirstRun() {
state={trafficState}
title={
encrypting != null
? `Encrypting stored traffic, ${encrypting}`
? `Encrypting recorded traffic, ${encrypting}`
: trafficState === "wait"
? "Waiting for traffic"
: "See traffic"
}
description={
encrypting != null
? "Traffic captured before this version is being encrypted on disk. Recording starts when it's done."
? "Recording will start once encryption finishes."
: trafficState === "fail"
? (retry.error?.message ?? proxyState?.startError ?? "The proxy has not started.")
? (retry.error?.message ?? proxyState?.startError ?? "The proxy failed to start.")
: !recording
? "Recording is paused, so traffic passes through without being stored."
? "Recording is paused."
: trafficState === "wait"
? "Open a website or send a request, and its flow appears here."
? "Open a website or send a request to begin."
: "Flows appear here as apps send requests."
}
action={
@@ -123,7 +123,7 @@ export function FirstRun() {
/>
</div>
<div className="flex items-center gap-3 mt-4 text-sm text-text-subtle">
<span>Only need one terminal or one app? Copy the environment instead.</span>
<span>To proxy a single terminal instead, copy the environment.</span>
<Button
size="2xs"
variant="border"
@@ -135,12 +135,14 @@ export function FirstRun() {
</Button>
</div>
<div className="mt-2 text-sm text-text-subtle">
Working with an agent? <InlineCode>yaak mcp</InlineCode> gives it the proxy's flows and
actions.
Agents can connect through <InlineCode>yaak mcp</InlineCode>.
</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 className="flex items-center gap-1.5 mt-2 text-sm text-text-subtle">
Recorded traffic is encrypted on disk.
<IconTooltip
iconSize="sm"
content="The key is kept in the system keychain, so the system may ask to let Yaak Proxy use it."
/>
</div>
</div>
</div>
+11 -4
View File
@@ -10,6 +10,7 @@ import {
CsvViewer,
HexdumpViewer,
Icon,
IconTooltip,
ImageViewer,
LARGE_BODY_BYTES,
languageFromContentType,
@@ -238,14 +239,20 @@ export function FlowBodyViewer({ flow, which, form, hideCutNote = false }: Props
<div className="h-full grid grid-rows-[auto_auto_minmax(0,1fr)]">
{decodeError != null && (
<Banner color="danger" className="text-sm mb-2">
{decodeError}. Showing the body as it was on the wire.
{decodeError}. The body is shown as it was received.
</Banner>
)}
{truncated && !hideCutNote && (
<Banner color="notice" className="text-sm mb-2">
{wireTruncated || decodeError != null
? `Showing the first ${formatSize(storedSize)} of ${formatSize(wireSize)}, the capture limit. The app received all of it.`
: `Showing the first ${formatSize(bytes.length)} of the decoded body, the capture limit. The app received all of it.`}
<span className="flex items-center gap-1.5">
{wireTruncated || decodeError != null
? `The first ${formatSize(storedSize)} of ${formatSize(wireSize)} is shown.`
: `The first ${formatSize(bytes.length)} of the decoded body is shown.`}
<IconTooltip
iconSize="sm"
content="The body was truncated at the capture limit. The app received all of it."
/>
</span>
</Banner>
)}
<div className="min-h-0 row-start-3">{viewer}</div>
+3 -1
View File
@@ -323,7 +323,9 @@ function FlowResponseHalf({ flow, className }: { flow: Flow; className: string }
)}
>
<Icon icon="scissors" size="xs" />
{body.data == null ? "Body cut" : `First ${formatSize(body.data.storedSize)} kept`}
{body.data == null
? "Body truncated"
: `First ${formatSize(body.data.storedSize)} recorded`}
</span>
) : undefined
}
+4 -4
View File
@@ -65,8 +65,8 @@ export function FlowInfo({ flow }: { flow: Flow }) {
"Started",
format(utcTimestamp(flow.startedAt), "MMM d, yyyy, h:mm:ss.SSS a"),
),
row("headers", "Headers", ms(flow.elapsedHeadersMs)),
row("total", "Total", ms(flow.elapsedMs)),
row("headers", "Latency", ms(flow.elapsedHeadersMs)),
row("total", "Time", ms(flow.elapsedMs)),
]}
</Section>
<Section title="Size">
@@ -75,11 +75,11 @@ export function FlowInfo({ flow }: { flow: Flow }) {
row("response_size", "Response", formatSize(flow.responseSize)),
]}
</Section>
<Section title="Record">
<Section title="Flow">
{[
row("number", "Number", `#${flow.number}`, { copy: true }),
row("id", "ID", flow.id, { copy: true }),
row("saved", "Saved As", flow.savedRequestId, { copy: true }),
row("saved", "Yaak Request ID", flow.savedRequestId, { copy: true }),
]}
</Section>
<Timeline flow={flow} />
+6 -6
View File
@@ -914,9 +914,9 @@ function encryptedReason(flow: Flow): string {
? "Encrypted: excluded by default"
: `Encrypted: excluded by ${flow.tunnelExcludedBy}`;
case "rejected":
return "Encrypted: passed through after a client rejected the certificate";
return "Encrypted: an app rejected the certificate";
default:
return "Encrypted: not on the decrypt list";
return "Encrypted: decryption is not enabled";
}
}
@@ -929,12 +929,12 @@ function FlowMark({ flow }: { flow: Flow }) {
const open = isOpenStream(flow);
const rejected = flow.tlsRejected != null;
const states: string[] = [];
if (rejected) states.push("Rejected the proxy's certificate");
if (rejected) states.push("Certificate rejected by the app");
else if (flow.error != null) states.push(`Failed: ${flow.error}`);
if (flow.cancelled) states.push("Cancelled by the app");
if (flow.tunnelled) states.push(encryptedReason(flow));
if (open) states.push("Streaming");
if (flow.responseTruncated) states.push("Body cut at the capture limit");
if (flow.responseTruncated) states.push("Body truncated at the capture limit");
if (flow.savedRequestId != null) states.push("Saved to Yaak");
const mark = (
<span className="flex items-center justify-center">
@@ -1369,9 +1369,9 @@ function useCellActions(): (flow: Flow, column: ColumnId) => DropdownItem[] {
onSelect: async () => {
try {
await navigator.clipboard.writeText(value);
showToast({ message: "Copied value" });
showToast({ message: "Copied to clipboard" });
} catch (err) {
showToast({ message: `Failed to copy value: ${String(err)}`, color: "danger" });
showToast({ message: `Failed to copy: ${String(err)}`, color: "danger" });
}
},
});
+7 -10
View File
@@ -31,18 +31,17 @@ export function FlowListEmpty() {
<div className="text-text text-sm">{recording ? "No flows yet" : "Recording is paused"}</div>
<div className="text-sm text-text-subtle max-w-[26rem]">
{!running
? "The proxy is not running. Retry from the menu by the state."
? "Retry from the status menu to start the proxy."
: !recording
? "Traffic passes through without being stored."
: `Recording on port ${port ?? "--"}. Flows appear as apps send requests.`}
? "Traffic passes through without being recorded."
: `Traffic sent to port ${port ?? "--"} will be recorded.`}
</div>
</EmptyStateText>
);
}
/**
* The list's last row while recording is paused, where the next flow would appear: since when,
* and that nothing is stored.
* The list's last row while recording is paused, where the next flow would appear: since when.
*/
export function PausedRow({ style }: { style?: React.CSSProperties }) {
const pausedAt = useAtomValue(pausedAtAtom);
@@ -56,8 +55,7 @@ export function PausedRow({ style }: { style?: React.CSSProperties }) {
>
<Icon icon="circle_pause" size="sm" className="text-text-subtlest" />
<span className="truncate">
{pausedAt == null ? "Paused." : `Paused at ${timeOf(pausedAt)}.`} Traffic passes through
without being stored.
{pausedAt == null ? "Recording is paused." : `Recording was paused at ${timeOf(pausedAt)}.`}
</span>
<Hotkey action="recording_toggle" variant="text" className="ml-auto" />
</div>
@@ -102,11 +100,10 @@ function NoResults({ text }: { text: string }) {
className="not-italic !h-auto py-3 px-4 text-sm text-text-subtle"
>
<div className="text-center leading-relaxed">
No flows match.
{wider.length > 0 && " Found matches for "}
{wider.length === 0 ? "No results" : "No results, but found matches for "}
{wider.map((w, i) => (
<span key={w.text}>
{i > 0 && (i === wider.length - 1 ? ", or " : ", ")}
{i > 0 && " or "}
<button type="button" className="hover:underline" onClick={() => setText(w.text)}>
<InlineCode>{w.text === "" ? "all traffic" : w.text}</InlineCode>
</button>{" "}
@@ -159,7 +159,7 @@ export const FlowListHeader = memo(function FlowListHeader({
: {}),
};
}),
{ label: "Add Column…", icon: "plus", onSelect: () => openAddColumn(true) },
{ label: "Add Column", icon: "plus", onSelect: () => openAddColumn(true) },
{ type: "separator" },
{ label: "Reset Sort", disabled: sort.length === 0, onSelect: onResetSort },
{ label: "Reset Column Widths", onSelect: () => setPrefs((p) => resetWidths(p)) },
@@ -179,7 +179,7 @@ export const FlowListHeader = memo(function FlowListHeader({
{ label: "Sort Descending", onSelect: () => sortBy(true) },
{ label: "Add to Sort Ascending", onSelect: () => addTo(false) },
{ label: "Add to Sort Descending", onSelect: () => addTo(true) },
{ label: "Clear Sorting", disabled: sort.length === 0, onSelect: onResetSort },
{ label: "Reset Sort", disabled: sort.length === 0, onSelect: onResetSort },
{ type: "separator" },
{
label: "Move Left",
+3 -1
View File
@@ -120,7 +120,9 @@ export function FlowMessages({ flow }: { flow: Flow }) {
header={header}
error={error ?? (open ? null : flow.error)}
isLoading={loading && events.length === 0}
emptyMessage={open ? `Waiting for the first ${noun.toLowerCase()}` : `No ${noun}s`}
emptyMessage={
open ? `Waiting for the first ${noun.toLowerCase()}` : `No ${noun.toLowerCase()}s`
}
splitLayoutStorageKey="proxy_flow_messages"
columns={columns}
loadPayload={loadPayload}
@@ -4,15 +4,14 @@ import { describe, expect, test, vi } from "vite-plus/test";
import type { FlowBodyData } from "../lib/flowViews";
import { FlowRequestBody } from "./FlowRequestBody";
// The package's index reaches for the window as it loads; these three are all this renders.
// The package's index reaches for the window as it loads; these four are all this renders.
vi.mock("@yaakapp-internal/ui", () => ({
Banner: ({ children, color }: { children: ReactNode; color?: string }) => (
<div data-banner={color}>{children}</div>
),
EmptyStateText: ({ children }: { children: ReactNode }) => <div>{children}</div>,
LoadingIcon: () => <span>LOADING</span>,
CONTROL_TOOLTIP_DELAY: 0,
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
IconTooltip: ({ content }: { content: string }) => <span data-tooltip={content} />,
}));
const data = (truncated = false): FlowBodyData => ({
@@ -45,10 +44,13 @@ describe("FlowRequestBody", () => {
expect(render({ data: data(), isLoading: false, error: null })).toContain("EDITOR");
});
test("a cut body says how much was kept", () => {
test("a truncated body says how much is shown, and why behind its info icon", () => {
const html = render({ data: data(true), isLoading: false, error: null });
expect(html).toContain("EDITOR");
expect(html).toContain('data-banner="notice"');
expect(html).toContain("kept");
expect(html.replaceAll("<!-- -->", "")).toContain("The first 7 B of 2 KB is shown.");
expect(html).toContain(
'data-tooltip="The body was truncated at the capture limit. The app sent all of it."',
);
});
});
+8 -12
View File
@@ -1,11 +1,5 @@
import { formatSize } from "@yaakapp-internal/lib/formatSize";
import {
Banner,
CONTROL_TOOLTIP_DELAY,
EmptyStateText,
LoadingIcon,
Tooltip,
} from "@yaakapp-internal/ui";
import { Banner, EmptyStateText, IconTooltip, LoadingIcon } from "@yaakapp-internal/ui";
import type { HttpBodyResult } from "@yaakapp-internal/ui/httpTypes";
import type { ReactNode } from "react";
import type { FlowBodyData } from "../lib/flowViews";
@@ -42,11 +36,13 @@ export function FlowRequestBody({ body, viewer, requestSize }: Props) {
return (
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
<Banner color="notice" className="text-sm mb-2">
<Tooltip wrap content="The app sent all of it" delay={CONTROL_TOOLTIP_DELAY}>
<span>
First {formatSize(body.data.storedSize)} of {formatSize(requestSize)} kept
</span>
</Tooltip>
<span className="flex items-center gap-1.5">
The first {formatSize(body.data.storedSize)} of {formatSize(requestSize)} is shown.
<IconTooltip
iconSize="sm"
content="The body was truncated at the capture limit. The app sent all of it."
/>
</span>
</Banner>
<div className="min-h-0">{viewer}</div>
</div>
+2 -2
View File
@@ -48,8 +48,8 @@ export function HelperDialog() {
open={open}
onClose={close}
size="dynamic"
title="Turn on Yaak Proxy under Allow in the Background"
description="In System Settings > General > Login Items. Newer macOS calls the list Background App Activity."
title="Approve Helper"
description="Turn on Yaak Proxy in System Settings > General > Login Items."
className="w-[34rem]"
>
<div className="grid gap-3 pb-4">
+9 -9
View File
@@ -24,7 +24,7 @@ const STRIP =
/**
* Above the list while nothing is decrypted: says once that HTTPS is encrypted, and offers
* Decrypt Everything, or Choose… for apps and sites one at a time. Goes once anything is on
* Decrypt Everything, or Choose for apps and sites one at a time. Goes once anything is on
* the list, or when dismissed; comes back, with Trust, while the list decrypts something the
* certificate isn't trusted for.
*/
@@ -33,7 +33,7 @@ export function HttpsStrip() {
const flows = useAtomValue(sidebarFlowsAtom);
const caStatus = useRpcQuery("ca_status", {});
const [dismissed, setDismissed] = useLocalStorage("https_strip_dismissed", false);
// Kept while Choose… is open, so checking a row there leaves the menu where it is.
// Kept while Choose is open, so checking a row there leaves the menu where it is.
const [choosing, setChoosing] = useState(false);
const { apply } = useDecrypt();
const trusting = useTrusting();
@@ -56,7 +56,7 @@ export function HttpsStrip() {
<div className={STRIP}>
<Icon icon="shield_off" size="sm" color="warning" className="shrink-0" />
<span className="text-text shrink-0">Certificate not trusted</span>
<span className="truncate">Required to decrypt</span>
<span className="truncate">The certificate must be trusted to decrypt traffic.</span>
<Button
size="2xs"
color="primary"
@@ -93,7 +93,7 @@ export function HttpsStrip() {
});
},
})),
{ type: "separator", label: "Sites", hidden: options.sites.length === 0 },
{ type: "separator", label: "Domains", hidden: options.sites.length === 0 },
...options.sites.map((site): DropdownItem => ({
label: <span className="font-mono text-xs">*.{site.site}</span>,
keepOpenOnSelect: true,
@@ -109,7 +109,7 @@ export function HttpsStrip() {
})),
{ type: "separator" },
{
label: "Edit List in Settings…",
label: "Decryption Settings",
leftSlot: <Icon icon="settings" />,
onSelect: () => openSettings(SETTINGS_TAB_PROXY),
},
@@ -124,7 +124,7 @@ export function HttpsStrip() {
<div className="ml-auto flex items-center gap-1.5 shrink-0">
<Tooltip
wrap
content="New connections, except Apple services and apps that pin"
content="New connections will be decrypted, except for excluded hosts and apps."
delay={CONTROL_TOOLTIP_DELAY}
>
<Button
@@ -136,7 +136,7 @@ export function HttpsStrip() {
void apply({
edit: everythingEdit(true),
message: "Decrypting everything",
sub: "Except Apple services and apps that pin",
sub: "Excluded hosts and apps stay encrypted.",
})
}
>
@@ -145,14 +145,14 @@ export function HttpsStrip() {
</Tooltip>
<Dropdown items={items} onOpen={() => setChoosing(true)} onClose={() => setChoosing(false)}>
<Button size="2xs" variant="border">
Choose…
Choose
</Button>
</Dropdown>
<IconButton
size="xs"
icon="x"
iconColor="secondary"
title="Not now"
title="Dismiss"
tooltip
onClick={() => setDismissed(true)}
/>
+1 -1
View File
@@ -28,7 +28,7 @@ export function LockedState({ lock }: { lock: ProxyLock }) {
<div className="text-lg font-semibold whitespace-nowrap">
{encrypting != null ? (
<>
Encrypting stored traffic,{" "}
Encrypting recorded traffic,{" "}
{/* Sized for "100%" and right-aligned, so the centred line never changes width as
the number ticks: a changing width re-rasterizes every glyph at a new
sub-pixel offset, and the line shakes */}
+14 -17
View File
@@ -65,24 +65,20 @@ export function ProxyStatus() {
status === "encrypting" ? (
<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."
title={`Encrypting recorded traffic, ${encrypting}`}
description="Recording will start once encryption finishes."
/>
) : status === "cli" && other != null ? (
<MenuRow
icon={<StateDot state="cli" />}
title="Running from the CLI"
description={`Started by yaak proxy start, pid ${other.pid}, since ${timeOf(other.startedAt)}.${other.proxyPort == null ? "" : ` Port ${other.proxyPort}.`}`}
description={`The proxy was started by the CLI at ${timeOf(other.startedAt)}.`}
/>
) : status === "recording" || status === "paused" ? (
<MenuRow
icon={<StateDot state={status} />}
title={recording ? `Recording on port ${port}` : `Paused on port ${port}`}
description={
recording
? "Every flow is stored as it passes."
: "Traffic passes through untouched, and nothing is stored."
}
description={recording ? null : "Traffic passes through without being recorded."}
/>
) : (
<MenuRow
@@ -90,7 +86,7 @@ export function ProxyStatus() {
title="Not running"
description={
<span className="text-danger">
{retry.error?.message ?? proxyState?.startError ?? "The proxy has not started."}
{retry.error?.message ?? proxyState?.startError ?? "The proxy failed to start."}
</span>
}
action={
@@ -156,9 +152,7 @@ export function ProxyStatus() {
{systemSwitch.description}
</span>
{helperNotice && (
<span className="block text-notice">
macOS asks for your password each time until the helper is approved in Settings.
</span>
<span className="block text-notice">macOS will ask for a password each time.</span>
)}
</>
}
@@ -258,8 +252,8 @@ function DecryptEverythingRow({ broken }: { broken: boolean }) {
trusting
? "Waiting for macOS"
: on
? "Except Apple services and apps that pin"
: (listSummary(list) ?? "Nothing listed")
? "Excluded hosts and apps stay encrypted."
: (listSummary(list) ?? "No hosts or apps")
}
action={
<KeepMenuOpen>
@@ -274,7 +268,7 @@ function DecryptEverythingRow({ broken }: { broken: boolean }) {
? {
edit: everythingEdit(true),
message: "Decrypting everything",
sub: "Except Apple services and apps that pin",
sub: "Excluded hosts and apps stay encrypted.",
}
: { edit: everythingEdit(false), message: "Stopped decrypting everything" },
)
@@ -350,7 +344,7 @@ export function useCopyEnvironment() {
try {
await navigator.clipboard.writeText(environmentExports(port, caPath));
showToast({
message: "Copied the environment. Paste it into a terminal to proxy what runs there.",
message: "Copied the environment. Paste it into a terminal.",
});
} catch (err) {
showToast({ message: `Failed to copy: ${String(err)}`, color: "danger" });
@@ -429,6 +423,7 @@ function MenuRow({
}: {
icon: ReactNode;
title: string;
/** Under the title; null for a title that says it all */
description: ReactNode;
action?: ReactNode;
}) {
@@ -437,7 +432,9 @@ function MenuRow({
<span className="h-5 flex items-center justify-center">{icon}</span>
<div>
<div className="text-sm text-text">{title}</div>
<div className="text-xs text-text-subtle mt-0.5">{description}</div>
{description != null && (
<div className="text-xs text-text-subtle mt-0.5">{description}</div>
)}
</div>
<span className="self-center">{action}</span>
</div>
+20 -24
View File
@@ -40,8 +40,8 @@ export function SaveDialog() {
open
onClose={close}
size="dynamic"
title="Save as Yaak request"
description="A new request in Yaak with this flow's method, URL, headers and body."
title="Save as Yaak Request"
description="A new request will be created in Yaak."
className="w-[36rem]"
>
{workspaces.error != null ? (
@@ -103,7 +103,7 @@ function SaveForm({
message: (
<>
Saved <b className="font-semibold">{saved.name || unnamed}</b> to {workspace.name}
{folderName == null ? "." : `, in ${folderName}.`} Yaak picks it up within a second.
{folderName == null ? "" : `, in ${folderName}`}
</>
),
});
@@ -113,11 +113,7 @@ function SaveForm({
};
if (workspaces.length === 0) {
return (
<Banner color="notice">
Yaak has no workspaces yet. Create one in Yaak, then save here.
</Banner>
);
return <Banner color="notice">Create a workspace in Yaak first.</Banner>;
}
if (p == null) {
@@ -209,13 +205,10 @@ function SaveForm({
</div>
{save.error != null && <Banner color="danger">{save.error.message}</Banner>}
<DialogFooter
leftSlot={
<span className="text-xs text-text-subtlest">Yaak picks it up within a second</span>
}
actions={[
{ label: "Cancel", onClick: onClose },
{
label: "Save request",
label: "Save Request",
form: SAVE_FORM_ID,
color: "primary",
isLoading: save.isPending,
@@ -243,15 +236,19 @@ function NoYaakDialog({ flow, onClose }: { flow: Flow; onClose: () => void }) {
return (
<Dialog open onClose={onClose} size="dynamic" className="w-[32rem]">
<DialogFooter
leftSlot={<span className="text-xs text-text-subtlest">Or keep the flow as a file</span>}
leftSlot={
<span className="text-xs text-text-subtlest">
The flow can be exported as a file instead.
</span>
}
actions={[
{
label: "Export HAR",
label: "Export as HAR",
onClick: async () => {
try {
if ((await exportHar({ ids: [flow.id] }, "flow.har")) != null) {
onClose();
showToast({ message: "Exported the flow as HAR" });
showToast({ message: "Exported 1 flow as HAR" });
}
} catch (err) {
showToast({ message: `Failed to export HAR: ${String(err)}`, color: "danger" });
@@ -262,11 +259,10 @@ function NoYaakDialog({ flow, onClose }: { flow: Flow; onClose: () => void }) {
]}
/>
<div className="pt-6 pb-2">
<div className="text-lg font-semibold mb-1.5">Saving needs Yaak</div>
<div className="text-lg font-semibold mb-1.5">Yaak Required</div>
<div className="text-sm text-text-subtle leading-relaxed">
This flow becomes a request in one of Yaak's workspaces, and Yaak has not been opened on
this Mac. Yaak is free for personal use and opens the request with the same viewers you
see here.
This flow will be saved as a request in one of Yaak's workspaces, but Yaak has not been
opened on this computer.
</div>
</div>
</Dialog>
@@ -308,18 +304,18 @@ function folderOptions(folders: FolderSummary[]): { id: string; name: string }[]
function reasonLabel(reason: DropReason): string {
switch (reason) {
case "set_by_sending":
return "Yaak sets it when it sends";
return "Yaak sets it when it sends the request.";
case "hop_by_hop":
return "Only meant for the proxy";
return "It was only meant for the proxy.";
case "body_decoded":
return "The body is saved decoded";
return "The body is saved decoded.";
}
}
function droppedSummary(reasons: DropReason[]): string {
const unique = [...new Set(reasons)];
if (unique.length === 1 && unique[0] === "set_by_sending") {
return "Yaak sets these itself when it sends";
return "Yaak sets these itself when it sends the request.";
}
return "Yaak sets these itself, or they only meant something to the proxy";
return "Yaak sets these, or they were meant for the proxy.";
}
@@ -47,7 +47,7 @@ function NameDialog({ dialog, onClose }: { dialog: SavedSearchDialog; onClose: (
open
onClose={onClose}
size="dynamic"
title={dialog.mode === "save" ? "Save search" : "Rename search"}
title={dialog.mode === "save" ? "Save Search" : "Rename Search"}
className="w-[26rem]"
>
<form
+3 -3
View File
@@ -205,7 +205,7 @@ export const SearchBar = memo(function SearchBar() {
<Dropdown
items={[
{
label: "Save search",
label: "Save Search",
leftSlot: <Icon icon="bookmark" />,
onSelect: () =>
setSavedDialog({
@@ -275,7 +275,7 @@ function FilterHistory({
const items: DropdownItem[] =
history.length === 0
? [{ label: "No recent filters", disabled: true }]
? [{ label: "No recent searches", disabled: true }]
: [
...history.map((filter): DropdownItem => ({
label: (
@@ -311,7 +311,7 @@ function FilterHistory({
<IconButton
size="xs"
icon="search"
title="Recent filters"
title="Recent searches"
tooltip
iconColor="secondary"
disabled={disabled}
+1 -1
View File
@@ -235,7 +235,7 @@ function useAppInfo() {
? "Reveal in Finder"
: os === "windows"
? "Show in Explorer"
: "Show in Folder",
: "Show in File Manager",
}),
[data, os],
);
+28 -22
View File
@@ -6,6 +6,7 @@ import {
Dialog,
DialogFooter,
Icon,
IconTooltip,
RadioDropdown,
SettingRow,
SettingRowBoolean,
@@ -58,27 +59,30 @@ export function SettingsProxy() {
const elsewhere = proxyState?.otherOwner != null;
return (
<SettingsPage title="Proxy" description="Listening, decryption, traffic and terminals">
<SettingsPage
title="Proxy"
description="Configure the port, decryption, recorded traffic, and terminal environment."
>
<SettingsList className="space-y-8">
<SettingsSection title="Proxy">
<PortRow disabled={elsewhere} />
<SettingRowSelect<HttpVersion>
name="httpVersion"
title="HTTP version"
description="For new connections"
description="The change applies to new connections."
value={proxyState?.httpVersion ?? "auto"}
disabled={elsewhere}
onChange={(httpVersion) => setHttpVersion.mutate({ httpVersion })}
options={[
{ label: "Auto", value: "auto" },
{ label: "Automatic", value: "auto" },
{ label: "HTTP/1.1 only", value: "http1Only" },
]}
/>
{systemProxy.data?.supported === true && (
<>
<SettingRowBoolean
title="Override system proxy"
description="At launch"
title="Override system proxy at launch"
description="Traffic from every app will be recorded."
checked={proxyState?.systemProxyAtLaunch ?? true}
disabled={setAtLaunch.isPending}
onChange={(enabled) => setAtLaunch.mutate({ enabled })}
@@ -147,7 +151,7 @@ function PortRow({ disabled }: { disabled: boolean }) {
setPort.mutate({ port });
};
return (
<SettingRow title="Port" description="Restarts the proxy">
<SettingRow title="Port" description="Changing the port restarts the proxy.">
<PlainInput
hideLabel
size="sm"
@@ -184,7 +188,10 @@ function HelperRow({ status }: { status: SystemProxyStatus }) {
unregister.error != null ? (
<span className="text-danger">{unregister.error.message}</span>
) : (
`${capitalize(row.title.replace(/^Helper: /, ""))}. ${row.description}`
<span className="inline-flex items-center gap-1.5">
{row.description}
<IconTooltip iconSize="sm" content={row.tooltip} />
</span>
)
}
>
@@ -200,7 +207,7 @@ function HelperRow({ status }: { status: SystemProxyStatus }) {
? "Install"
: row.action === "approve"
? "Approve"
: "Unregister"}
: "Uninstall"}
</Button>
)}
</SettingRow>
@@ -217,7 +224,7 @@ function TrafficRow() {
return (
<SettingRowSelect<TrafficRetention>
name="traffic"
title="Captured traffic"
title="Recorded traffic"
selectClassName="w-56!"
value={proxyState?.traffic ?? "clearOnLaunch"}
disabled={proxyState?.otherOwner != null}
@@ -240,7 +247,7 @@ function KeepTrafficRow() {
});
const keep = proxyState?.keepTrafficBytes ?? null;
return (
<SettingRow title="Size limit" description="Oldest flows dropped past it">
<SettingRow title="Size limit" description="The oldest flows will be cleared first.">
<div className="flex items-center gap-3">
{size.data != null && (
<span className="text-sm text-text-subtle tabular-nums whitespace-nowrap">
@@ -280,21 +287,24 @@ function ResetRow() {
onSuccess: () => {
setConfirming(false);
void queryClient.invalidateQueries();
showToast({ message: "Captured data reset" });
showToast({ message: "Recorded data reset" });
},
onSettled: () => void refreshProxyState(store),
});
return (
<SettingRow title="Reset captured data" description="Deletes all flows and their keys">
<SettingRow
title="Reset recorded data"
description="All traffic and its encryption keys will be deleted."
>
<Button size="xs" variant="border" color="danger" onClick={() => setConfirming(true)}>
Reset…
Reset
</Button>
<Dialog
open={confirming}
onClose={() => setConfirming(false)}
size="sm"
title="Reset captured data?"
description="Every captured flow and the keys that encrypt them are deleted."
title="Reset Recorded Data"
description="Permanently delete all traffic and its encryption keys?"
>
{reset.error != null && <p className="text-danger text-sm">{reset.error.message}</p>}
<DialogFooter
@@ -351,8 +361,8 @@ function TerminalRow() {
className="ml-auto"
title={
os === "macos"
? "A new Terminal window with these set"
: "Not available on this platform yet"
? "A new Terminal window will open with these variables set."
: "Opening a terminal is only supported on macOS."
}
disabled={caPath == null || os !== "macos"}
isLoading={openTerminal.isPending}
@@ -363,7 +373,7 @@ function TerminalRow() {
Open Terminal
</Button>
</div>
<p className="mt-1 text-sm text-text-subtle">Proxies one terminal instead of the system</p>
<p className="mt-1 text-sm text-text-subtle">Traffic from one terminal will be recorded.</p>
{caPath != null && (
<SettingSnippet
className="mt-3"
@@ -374,7 +384,3 @@ function TerminalRow() {
</div>
);
}
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}
+4 -4
View File
@@ -175,7 +175,7 @@ export function Sidebar() {
onSelect: () => void revealPath(path, fail),
},
{
label: "Copy path",
label: "Copy Path",
leftSlot: <Icon icon="copy" />,
hidden: item.kind !== "folder",
onSelect: () => {
@@ -203,9 +203,9 @@ export function Sidebar() {
onSelect: async () => {
try {
await navigator.clipboard.writeText(copyRowsText(items));
showToast({ message: "Copied value" });
showToast({ message: "Copied to clipboard" });
} catch (err) {
showToast({ message: `Failed to copy value: ${String(err)}`, color: "danger" });
showToast({ message: `Failed to copy: ${String(err)}`, color: "danger" });
}
},
},
@@ -239,7 +239,7 @@ export function Sidebar() {
void applyDecrypt({
edit: everythingEdit(true),
message: "Decrypting everything",
sub: "Except Apple services and apps that pin",
sub: "Excluded hosts and apps stay encrypted.",
}),
},
],
@@ -29,11 +29,11 @@ export function TakeOverBanner() {
<span className="truncate">
{!cli ? (
// Stopping another app halts its engine without releasing it, so it can't be taken over.
"Another Yaak Proxy window owns the engine. Quit it to use this one."
"Quit the other copy to use this one."
) : takeOver.error != null ? (
<span className="text-danger">{takeOver.error.message}</span>
) : (
`pid ${other.pid}, since ${since}. Flows still list here, read from the capture database.`
`pid ${other.pid}, since ${since}`
)}
</span>
{cli && (
@@ -44,7 +44,7 @@ export function TakeOverBanner() {
isLoading={takeOver.isPending}
onClick={() => takeOver.mutate({})}
>
Take over
Take Over
</Button>
)}
</div>
+2 -2
View File
@@ -9,8 +9,8 @@ import { syncNavigation, syncSidebarSelection } from "../lib/navigation";
import { syncFlows, syncProxyState } 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.";
"Traffic is locked: Failed to read the encryption key from the keychain: Encryption error: " +
"User canceled the operation";
const STATE: GetProxyStateResponse = {
state: "stopped",
+1 -1
View File
@@ -157,7 +157,7 @@ function mixedScene(): Flow[] {
tunnelled: false,
tunnelReason: null,
tlsRejected: "bad_certificate",
error: "Rejected the proxy's certificate",
error: "Certificate rejected by the app",
elapsedMs: 6,
requestSize: 517,
responseSize: 0,
+1 -1
View File
@@ -46,7 +46,7 @@ export function useTrustCertificate() {
showToast({
id: "decrypt",
color: "danger",
message: `Certificate not trusted: ${err.message}`,
message: `Failed to trust certificate: ${err.message}`,
}),
});
}
+1 -1
View File
@@ -231,7 +231,7 @@ describe("flowBodyQueryOptions", () => {
describe("a body that isn't there", () => {
test("a WebSocket's note reaches the viewer, which points at its messages", () => {
const note = "A WebSocket has no body after its 101 response. Its frames are its messages.";
const note = "A WebSocket has no body";
const body = viewerBody({ data: "", truncated: false, storedSize: 0, note });
expect(body.note).toBe(note);
expect(emptyBodyText("response", body.note)).toBe("No body. See Messages.");
+1 -1
View File
@@ -194,7 +194,7 @@ describe("the sidebar menu", () => {
expect(decryptMenuAction([], [site])?.label).toBe("Decrypt *.google.com");
expect(decryptMenuAction([], [dia])?.label).toBe("Decrypt Dia");
expect(decryptMenuAction([], [site, { kind: "site", site: "x.com" }])?.label).toBe(
"Decrypt 2 Sites",
"Decrypt 2 Domains",
);
expect(decryptMenuAction([], [dia, a, site])?.label).toBe("Decrypt 3 Selected");
});
+5 -5
View File
@@ -411,8 +411,8 @@ export function subjectName(subject: DecryptSubject): string {
}
/**
* Several subjects named together: one by its name, several of a kind counted, `3 Sites` in a
* menu and `3 sites` in a toast, and mixed kinds as `3 Selected`
* Several subjects named together: one by its name, several of a kind counted, `3 Domains` in a
* menu and `3 domains` in a toast, and mixed kinds as `3 Selected`
*/
function subjectsName(subjects: DecryptSubject[]): { title: string; text: string } {
const [first] = subjects;
@@ -423,7 +423,7 @@ function subjectsName(subjects: DecryptSubject[]): { title: string; text: string
const kinds = new Set(subjects.map((s) => s.kind));
const n = subjects.length;
if (kinds.size > 1) return { title: `${n} Selected`, text: `${n} selected` };
const noun = first?.kind === "app" ? "Apps" : first?.kind === "site" ? "Sites" : "Hosts";
const noun = first?.kind === "app" ? "Apps" : first?.kind === "site" ? "Domains" : "Hosts";
return { title: `${n} ${noun}`, text: `${n} ${noun.toLowerCase()}` };
}
@@ -473,8 +473,8 @@ export function decryptFilterOptions(seen: SeenTargets | null | undefined): Filt
replaceValueOnly: true,
negation: { prefix: "!", info: "Exclude" },
fields: [
{ name: "host", values: seen?.hosts ?? [], info: "Host or *.site" },
{ name: "app", values: [...apps], info: "App name or bundle id" },
{ name: "host", values: seen?.hosts ?? [], info: "Host or *.domain" },
{ name: "app", values: [...apps], info: "App name or bundle ID" },
],
};
}
+2 -2
View File
@@ -106,10 +106,10 @@ describe("the toast after a choice", () => {
test("offers Reconnect for the entry when open connections stay encrypted", () => {
expect(choiceToast(choice, 3)).toEqual({
message: "Decrypting www.google.com",
sub: "3 open connections stay encrypted",
sub: "3 open connections stay encrypted.",
reconnect: ["host:www.google.com"],
});
expect(choiceToast(choice, 1).sub).toBe("1 open connection stays encrypted");
expect(choiceToast(choice, 1).sub).toBe("1 open connection stays encrypted.");
});
test("Reconnect for an exclusion taken off names the exclusion, as the engine reads it", () => {
+1 -1
View File
@@ -81,7 +81,7 @@ export function choiceToast(choice: DecryptChoice, stillEncrypted: number): Choi
const reconnect = reconnectEntries(choice.edit);
return {
message: choice.message,
sub: `${n} open ${stillEncrypted === 1 ? "connection stays" : "connections stay"} encrypted`,
sub: `${n} open ${stillEncrypted === 1 ? "connection stays" : "connections stay"} encrypted.`,
reconnect,
};
}
+45 -31
View File
@@ -68,7 +68,9 @@ 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(lineText(c.why)).toBe("Nothing is decrypted yet");
expect(lineText(c.why)).toBe(
"**Encrypted Content:** Enable decryption to see requests to `www.google.com`",
);
expect(summary(c)).toEqual([
[
"Decrypt www.google.com",
@@ -80,11 +82,12 @@ describe("the card of an encrypted connection", () => {
]);
// Nothing under the buttons while the certificate is trusted
expect(c.foot).toEqual([]);
expect(c.fact).toBeNull();
// The info button keeps its general line
expect(c.tooltip).toBeNull();
});
test("asks for the password once while the certificate isn't trusted, where it can be", () => {
expect(card({ trusted: false }).foot).toEqual(["Asks for your password once"]);
expect(card({ trusted: false }).foot).toEqual(["macOS will ask for a password once."]);
expect(card({ trusted: false, canTrust: false }).foot).toEqual([]);
});
@@ -140,10 +143,7 @@ describe("the card of an encrypted connection", () => {
tunnelExcludedBuiltIn: true,
},
});
expect([lineText(pinned.why), pinned.whySub]).toEqual([
"Excluded by default",
"pins its certificate",
]);
expect(lineText(pinned.why)).toBe("Apps that pin certificates are excluded by default.");
expect(summary(pinned)).toEqual([
[
"Decrypt my.1password.com",
@@ -152,7 +152,7 @@ describe("the card of an encrypted connection", () => {
],
]);
expect(pinned.actions[0]?.dashed).toBe(true);
expect(pinned.foot).toEqual(["1Password may refuse it"]);
expect(pinned.foot).toEqual(["1Password may reject the connection."]);
const apple = card({
flow: {
@@ -165,8 +165,8 @@ describe("the card of an encrypted connection", () => {
tunnelExcludedBuiltIn: true,
},
});
expect(apple.whySub).toBe("Apple service");
expect(apple.foot).toEqual(["macOS may refuse it"]);
expect(lineText(apple.why)).toBe("Apple services are excluded by default.");
expect(apple.foot).toEqual(["macOS may reject the connection."]);
});
test("excluded by the person: names it, and Stop Excluding when that alone decrypts it", () => {
@@ -175,7 +175,7 @@ describe("the card of an encrypted connection", () => {
list,
flow: { ...tunnel, tunnelReason: "excluded", tunnelExcludedBy: "!host:*.google.com" },
});
expect(lineText(c.why)).toBe("Excluded by `!host:*.google.com`");
expect(lineText(c.why)).toBe("This connection is 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([
[
@@ -208,9 +208,8 @@ 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(lineText(closed.why)).toBe("Not on the decrypt list");
expect(closed.fact && lineText(closed.fact)).toBe(
"Opened before `www.google.com` was decrypted",
expect(lineText(closed.why)).toBe(
"This connection was opened before `www.google.com` was decrypted.",
);
expect(summary(closed)).toEqual([
[
@@ -225,8 +224,8 @@ 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 && lineText(everything.fact)).toBe(
"Opened before everything was decrypted",
expect(lineText(everything.why)).toBe(
"This connection was opened before everything was decrypted.",
);
expect(everything.actions[0]?.label).toBe("Stop Decrypting Everything");
});
@@ -245,22 +244,37 @@ describe("the card of a refused handshake", () => {
};
const listed = [...user("*"), ...BUILT_INS];
test("an app: likely pinned, Stop Decrypting first, Try Again while passed through", () => {
test("an app: likely pinned, Stop Decrypting first, Retry while passed through", () => {
const c = card({ flow: refused, list: listed, passthrough: ["sync.acme.dev:443"] });
expect(c.rejected).toBe(true);
expect([lineText(c.why), c.whySub]).toEqual(["Acme rejected the certificate", "likely pinned"]);
expect([lineText(c.why), c.tooltip]).toEqual([
"Acme rejected the certificate.",
"Acme likely pins its certificate, so it cannot be decrypted.",
]);
expect(summary(c)).toEqual([
["Stop Decrypting Acme", "!app:Acme", { add: ["!app:Acme"], remove: [] }],
["Try Again", "next connection", null],
["Retry", "next connection", null],
]);
expect(c.actions[0]?.primary).toBe(true);
expect(c.foot).toEqual(["Passed through until relaunch"]);
expect(c.foot).toEqual(["This host stays encrypted until Yaak Proxy is relaunched."]);
});
test("once no longer passed through, Try Again goes and the foot says so", () => {
test("once no longer passed through, Retry goes and the foot says so", () => {
const c = card({ flow: refused, list: listed, passthrough: [] });
expect(c.actions.map((a) => a.kind)).toEqual(["stop"]);
expect(c.foot).toEqual(["No longer passed through"]);
expect(c.foot).toEqual(["The next connection will be decrypted."]);
});
test("no longer passed through but excluded since: no claim that it will be decrypted", () => {
const excluded = [...user("*", "!app:Acme"), ...BUILT_INS];
const c = card({ flow: refused, list: excluded, passthrough: [] });
expect(c.actions).toEqual([]);
expect(c.foot).toEqual([]);
});
test("an untrusted certificate keeps its line once no longer passed through", () => {
const c = card({ flow: refused, list: listed, trusted: false, passthrough: [] });
expect(c.foot).toEqual(["This host stays encrypted until the certificate is trusted."]);
});
test("a program: its own trust store, Copy Environment first", () => {
@@ -272,16 +286,16 @@ describe("the card of a refused handshake", () => {
clientPath: "/opt/homebrew/bin/python3",
};
const c = card({ flow: python, list: listed, passthrough: ["pypi.org:443"] });
expect([lineText(c.why), c.whySub]).toEqual([
"python3 rejected the certificate",
"uses its own trust store",
expect([lineText(c.why), c.tooltip]).toEqual([
"python3 rejected the certificate.",
"python3 uses its own trust store, so it does not trust the certificate.",
]);
expect(c.actions.map((a) => [a.kind, a.label])).toEqual([
["copy_environment", "Copy Environment"],
["stop", "Stop Decrypting python3"],
["try_again", "Try Again"],
["try_again", "Retry"],
]);
expect(c.foot).toEqual(["Then rerun python3"]);
expect(c.foot).toEqual(["Paste it into a terminal, then rerun python3."]);
});
test("an unknown client: the host", () => {
@@ -289,7 +303,7 @@ describe("the card of a refused handshake", () => {
flow: { ...refused, clientName: null, clientBundleId: null, clientPath: null },
list: listed,
});
expect(lineText(c.why)).toBe("A client rejected the certificate");
expect(lineText(c.why)).toBe("An app rejected the certificate.");
expect(summary(c)[0]).toEqual([
"Stop Decrypting sync.acme.dev",
"!host:sync.acme.dev",
@@ -304,9 +318,9 @@ describe("the card of a refused handshake", () => {
trusted: false,
passthrough: ["sync.acme.dev:443"],
});
expect(lineText(c.why)).toBe("Certificate not trusted");
expect(lineText(c.why)).toBe("The certificate is not trusted.");
expect(c.actions.map((a) => a.kind)).toEqual(["trust", "stop"]);
expect(c.foot).toEqual(["Passed through until trusted"]);
expect(c.foot).toEqual(["This host stays encrypted until the certificate is trusted."]);
});
test("a tunnel after a refusal names the client that refused", () => {
@@ -320,7 +334,7 @@ describe("the card of a refused handshake", () => {
list: listed,
rejectedBy: { clientName: "Acme", clientBundleId: "dev.acme", clientPath: null },
});
expect(lineText(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");
});
+52 -55
View File
@@ -95,12 +95,10 @@ export function lineText(line: Line): string {
export interface EncryptedCardModel {
rejected: boolean;
/** Why the connection is encrypted, in one line */
/** Why the connection is encrypted, in one sentence */
why: Line;
/** Quieter, after it */
whySub: string | null;
/** A fact about the connection since the list changed: when it opened */
fact: Line | null;
/** What the info button beside it says, in place of the general line; null for that line */
tooltip: string | null;
actions: CardAction[];
/** Small lines under the buttons */
foot: string[];
@@ -155,7 +153,6 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
const c = connectionOf(flow);
const q = queryOf(c);
const decryptsNow = decrypts(list, q);
const { why, whySub } = whyLine(flow, list, c, decryptsNow);
const foot: string[] = [];
if (decryptsNow) {
@@ -172,9 +169,8 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
first === EVERYTHING ? "everything" : { code: first == null ? c.label : entrySubject(first) };
return {
rejected: false,
why,
whySub,
fact: ["Opened before ", what, " was decrypted"],
why: ["This connection was opened before ", what, " was decrypted."],
tooltip: null,
actions,
foot: [],
};
@@ -193,8 +189,8 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
foot.length = 0;
foot.push(
builtInBlock.kind === "apple" || c.app == null
? "macOS may refuse it"
: `${c.app.name} may refuse it`,
? "macOS may reject the connection."
: `${c.app.name} may reject the connection.`,
);
}
} else {
@@ -209,7 +205,7 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
label: "Decrypt Everything",
hint: EVERYTHING,
edit: { add: [EVERYTHING], remove: [] },
toast: { message: "Decrypting everything", sub: "Except Apple services and apps that pin" },
toast: { message: "Decrypting everything", sub: "Excluded hosts and apps stay encrypted." },
icon: "everything",
});
}
@@ -237,48 +233,41 @@ function tunnelCard({ flow, list, trusted, canTrust }: EncryptedCardInput): Encr
}
}
}
if (actions.length > 0 && canTrust && trusted === false) foot.push("Asks for your password once");
if (actions.length > 0 && canTrust && trusted === false) {
foot.push("macOS will ask for a password once.");
}
return {
rejected: false,
why,
whySub,
fact: null,
why: whyLine(flow, list, c),
tooltip: null,
actions,
foot: actions.length === 0 ? [] : foot,
};
}
/**
* The why line, from the reason the engine recorded as the tunnel opened. One that nothing on
* the list covers names its host; once the list covers it, it opened before that.
* The why line of a connection the list does not decrypt, from the reason the engine recorded
* as the tunnel opened: what excluded it, or, with nothing to exclude it, its host.
*/
function whyLine(
flow: EncryptedCardInput["flow"],
list: DecryptListEntry[],
c: Connection,
decryptsNow: boolean,
): { why: Line; whySub: string | null } {
function whyLine(flow: EncryptedCardInput["flow"], list: DecryptListEntry[], c: Connection): Line {
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 };
return [
kind === "apple"
? "Apple services are excluded by default."
: kind === "pinned"
? "Apps that pin certificates are excluded by default."
: "This connection is excluded by default.",
];
}
return { why: ["Excluded by ", { code: flow.tunnelExcludedBy }], whySub: null };
return ["This connection is excluded by ", { code: flow.tunnelExcludedBy }, "."];
}
return {
why: !decryptsAnything(list)
? ["Nothing is decrypted yet"]
: decryptsNow
? ["Not on the decrypt list"]
: [
{ bold: "Encrypted Content:" },
" Enable decryption to see requests to ",
{ code: c.label },
],
whySub: null,
};
return [
{ bold: "Encrypted Content:" },
" Enable decryption to see requests to ",
{ code: c.label },
];
}
function decryptSubject(
@@ -370,20 +359,27 @@ function rejectedCard(input: EncryptedCardInput): EncryptedCardModel {
const tryAgain: CardAction | null =
passedThrough === false
? null
: { kind: "try_again", label: "Try Again", hint: "next connection", icon: "refresh" };
const until = passedThrough === false ? "No longer passed through" : null;
: { kind: "try_again", label: "Retry", hint: "next connection", icon: "refresh" };
// No longer passed through: the next connection is decrypted while the list still decrypts
// it. Excluded since, it stays encrypted, and there is nothing to add.
const until: string[] | null =
passedThrough === false
? stop == null
? []
: ["The next connection will be decrypted."]
: null;
if (canTrust && trusted === false && decryptsAnything(list)) {
return {
rejected: true,
why: ["Certificate not trusted"],
whySub: null,
fact: null,
why: ["The certificate is not trusted."],
tooltip: null,
actions: [
{ kind: "trust", label: "Trust Certificate", hint: null, primary: true, icon: "shield" },
...(stop == null ? [] : [stop]),
],
foot: [until ?? "Passed through until trusted"],
// Whether or not it is still passed through: untrusted, the next one is rejected too.
foot: ["This host stays encrypted until the certificate is trusted."],
};
}
@@ -391,9 +387,8 @@ function rejectedCard(input: EncryptedCardInput): EncryptedCardModel {
if (refuser.kind === "program") {
return {
rejected: true,
why: [`${refuser.name} rejected the certificate${forHost}`],
whySub: "uses its own trust store",
fact: null,
why: [`${refuser.name} rejected the certificate${forHost}.`],
tooltip: `${refuser.name} uses its own trust store, so it does not trust the certificate.`,
actions: [
{
kind: "copy_environment",
@@ -405,22 +400,24 @@ function rejectedCard(input: EncryptedCardInput): EncryptedCardModel {
...(stop == null ? [] : [stop]),
...(tryAgain == null ? [] : [tryAgain]),
],
foot: until == null ? [`Then rerun ${refuser.name}`] : [until],
foot: until ?? [`Paste it into a terminal, then rerun ${refuser.name}.`],
};
}
return {
rejected: true,
why: [
refuser.kind === "app"
? `${refuser.name} rejected the certificate${forHost}`
: "A client rejected the certificate",
? `${refuser.name} rejected the certificate${forHost}.`
: "An app rejected the certificate.",
],
whySub: refuser.kind === "app" ? "likely pinned" : null,
fact: null,
tooltip:
refuser.kind === "app"
? `${refuser.name} likely pins its certificate, so it cannot be decrypted.`
: null,
actions: [
...(stop == null ? [] : [{ ...stop, primary: true }]),
...(tryAgain == null ? [] : [tryAgain]),
],
foot: [until ?? "Passed through until relaunch"],
foot: until ?? ["This host stays encrypted until Yaak Proxy is relaunched."],
};
}
+7 -7
View File
@@ -33,8 +33,8 @@ export function useSaveFlowBody(): (flow: Flow, which: FlowBody) => Promise<void
if (path == null) return;
const saved = await rpc("save_flow_body", { flowId: flow.id, which, path });
const notes = [
saved.raw ? "as it was on the wire, since it failed to decode" : null,
saved.truncated ? "cut at the capture limit" : null,
saved.raw ? "as received, since it failed to decode" : null,
saved.truncated ? "truncated at the capture limit" : null,
].filter((n) => n != null);
showToast({
message: `Saved ${formatSize(saved.size)}${notes.length > 0 ? `, ${notes.join(", ")}` : ""}`,
@@ -135,9 +135,9 @@ export function useFlowActions(): (flow: Flow) => DropdownItem[] {
return;
}
await navigator.clipboard.writeText(value);
showToast({ message: `Copied ${label}` });
showToast({ message: "Copied to clipboard" });
} catch (err) {
showToast({ message: `Failed to copy ${label}: ${String(err)}`, color: "danger" });
showToast({ message: `Failed to copy: ${String(err)}`, color: "danger" });
}
};
const noResponse = (flow.error != null || flow.cancelled) && flow.status == null;
@@ -173,7 +173,7 @@ export function useFlowActions(): (flow: Flow) => DropdownItem[] {
// A command that sends other bytes than the app did would mislead.
if (body == null) {
showToast({
message: "Body is binary or truncated; export HAR instead",
message: "The body is binary or truncated. Export as HAR instead.",
color: "info",
});
return;
@@ -209,12 +209,12 @@ export function useFlowActions(): (flow: Flow) => DropdownItem[] {
onSelect: () => saveBody(flow, "response"),
},
{
label: "Export HAR",
label: "Export as HAR",
icon: "download",
onSelect: async () => {
try {
const written = await exportHar({ ids: [flow.id] }, "flow.har");
if (written != null) showToast({ message: "Exported the flow as HAR" });
if (written != null) showToast({ message: "Exported 1 flow as HAR" });
} catch (err) {
showToast({ message: `Failed to export HAR: ${String(err)}`, color: "danger" });
}
+1 -1
View File
@@ -225,7 +225,7 @@ describe("flowResponseToView", () => {
test("says when the client gave up before a response", () => {
const view = flowResponseToView({ ...flow, status: null, elapsedMs: null, cancelled: true });
expect(view?.error).toBe("Cancelled by the client");
expect(view?.error).toBe("Cancelled by the app");
});
});
+1 -1
View File
@@ -119,7 +119,7 @@ function responseView(flow: Flow, body: FlowBodyData | null, ended: boolean): Ht
elapsedMs: flow.elapsedMs ?? 0,
elapsedHeadersMs: flow.elapsedHeadersMs ?? 0,
elapsedDnsMs: 0,
error: flow.error ?? (flow.cancelled && flow.status == null ? "Cancelled by the client" : null),
error: flow.error ?? (flow.cancelled && flow.status == null ? "Cancelled by the app" : null),
remoteAddr: flow.remoteAddr,
version: flow.upstreamHttpVersion ?? flow.clientHttpVersion,
cookieHeaders: {
+2 -2
View File
@@ -14,8 +14,8 @@ describe("lockedLine", () => {
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).",
message: "Traffic is locked: the proxy is already running in the CLI (pid 4).",
};
expect(lockedLine(lock, "macos")).toBe("The proxy is already running in the yaak CLI (pid 4).");
expect(lockedLine(lock, "macos")).toBe("The proxy is already running in the CLI (pid 4).");
});
});
+1 -1
View File
@@ -271,7 +271,7 @@ describe("syncSidebarSelection", () => {
test("All traffic is exclusive either way round", () => {
const { store, selected, idOf, choose } = sidebar();
const [all, github] = [idOf("All traffic"), idOf("github.com")];
const [all, github] = [idOf("All Traffic"), idOf("github.com")];
choose([github]);
choose([github, all]);
expect(store.get(selected)).toEqual([all]);
+2 -2
View File
@@ -20,7 +20,7 @@ describe("opener", () => {
await revealPath("/data/app.yaak.proxy", report);
expect(opener.revealItemInDir).toHaveBeenCalledWith("/data/app.yaak.proxy");
expect(report).toHaveBeenCalledWith(
"Failed to show /data/app.yaak.proxy: not allowed: opener.reveal_item_in_dir",
"Failed to reveal /data/app.yaak.proxy: not allowed: opener.reveal_item_in_dir",
);
});
@@ -43,6 +43,6 @@ describe("opener", () => {
test("names the file manager by platform", () => {
expect(revealLabel("macos")).toBe("Reveal in Finder");
expect(revealLabel("windows")).toBe("Show in Explorer");
expect(revealLabel("linux")).toBe("Show in Folder");
expect(revealLabel("linux")).toBe("Show in File Manager");
});
});
+2 -2
View File
@@ -8,7 +8,7 @@ export async function revealPath(path: string, report: ReportFailure): Promise<v
try {
await revealItemInDir(path);
} catch (err) {
report(`Failed to show ${path}: ${String(err)}`);
report(`Failed to reveal ${path}: ${String(err)}`);
}
}
@@ -25,5 +25,5 @@ export async function openLink(url: string, report: ReportFailure): Promise<void
export function revealLabel(os: string): string {
if (os === "macos") return "Reveal in Finder";
if (os === "windows") return "Show in Explorer";
return "Show in Folder";
return "Show in File Manager";
}
+1 -1
View File
@@ -41,7 +41,7 @@ export async function loadSeenValues(paths: string[]): Promise<Record<string, st
const PROBLEM_LABELS: Record<QueryDiagnostic["kind"], string> = {
unknownField: "Unknown field",
invalidValue: "Invalid value",
notYet: "Not yet",
notYet: "Not supported",
};
/** The terms of a search the backend cannot run, as the box underlines them */
+2 -2
View File
@@ -50,7 +50,7 @@ describe("buildSidebarTree", () => {
test("groups apps, programs by folder, sites and System", () => {
expect((root.children ?? []).flatMap((c) => shape(c))).toEqual([
"All traffic 40",
"All Traffic 40",
"Apps 3",
" node (fieldwork) 2",
" github.com 2",
@@ -338,7 +338,7 @@ describe("saved searches", () => {
];
const root = buildSidebarTree(flows, 4, saved);
expect((root.children ?? []).slice(0, 2).flatMap((c) => shape(c))).toEqual([
"All traffic 4",
"All Traffic 4",
"Saved 0",
" Errors 12",
" Stripe 0",
+1 -1
View File
@@ -347,7 +347,7 @@ export function buildSidebarTree(
for (const site of domains.children.values()) domains.item.count += site.children.size;
root.children?.push({
item: { id: ALL_ID, kind: "all", label: "All traffic", count: total, query: "" },
item: { id: ALL_ID, kind: "all", label: "All Traffic", count: total, query: "" },
parent: root,
depth: 1,
draggable: false,
+23 -18
View File
@@ -14,12 +14,12 @@ describe("systemProxySwitch", () => {
test("says whether this Mac's traffic is routed now", () => {
expect(systemProxySwitch({ ...off, routing: true })).toEqual({
checked: true,
description: "Routing this Mac's web traffic through Yaak Proxy",
description: "Traffic from every app will be recorded.",
failed: false,
});
expect(systemProxySwitch(off)).toEqual({
checked: false,
description: "Not routing. Only apps pointed at port 9090 are captured",
description: "Only traffic sent to port 9090 will be recorded.",
failed: false,
});
});
@@ -27,7 +27,7 @@ describe("systemProxySwitch", () => {
test("shows the new value while the password prompt is up", () => {
expect(systemProxySwitch({ ...off, pending: true })).toEqual({
checked: true,
description: "Waiting for your password",
description: "Waiting for macOS",
failed: false,
});
});
@@ -41,9 +41,9 @@ describe("systemProxySwitch", () => {
});
test("snaps back on a cancelled prompt and says so", () => {
expect(systemProxySwitch({ ...off, error: "Cancelled in the password dialog" })).toEqual({
expect(systemProxySwitch({ ...off, error: "The password prompt was cancelled." })).toEqual({
checked: false,
description: "Cancelled in the password prompt",
description: "The password prompt was cancelled.",
failed: true,
});
expect(systemProxySwitch({ ...off, error: "networksetup failed" }).description).toBe(
@@ -53,27 +53,32 @@ describe("systemProxySwitch", () => {
});
describe("helperRow", () => {
test("offers each next step, and says which way changes go", () => {
test("offers each next step, and says what the helper is for", () => {
expect(helperRow("not_registered", "admin_prompt")).toMatchObject({
title: "Helper: not installed",
description: "The helper is not installed.",
action: "install",
});
expect(helperRow("requires_approval", "admin_prompt")).toMatchObject({
title: "Helper: awaiting approval",
description: "The helper is awaiting approval.",
action: "approve",
});
const enabled = helperRow("enabled", "helper");
expect(enabled).toMatchObject({ title: "Helper: enabled", action: "unregister" });
expect(enabled.description).toMatch(/No password/);
expect(enabled).toMatchObject({
description: "The helper is installed.",
action: "unregister",
});
expect(enabled.tooltip).toMatch(/without a password prompt/);
});
test("says when the password prompt still has settings to put back", () => {
expect(helperRow("enabled", "admin_prompt").description).toMatch(/password prompt/);
expect(helperRow("enabled", "admin_prompt").tooltip).toBe(
"One more password prompt is needed to restore the system proxy settings.",
);
});
test("offers to install when macOS can't find it, rather than blaming a dev build", () => {
expect(helperRow("not_found", "admin_prompt")).toMatchObject({
title: "Helper: not found by macOS",
description: "macOS could not find the helper.",
action: "install",
});
});
@@ -89,13 +94,13 @@ describe("helperRow", () => {
for (const state of states) {
for (const backend of ["helper", "admin_prompt"] as const) {
const row = helperRow(state, backend);
expect(row.title.length).toBeLessThanOrEqual(30);
expect(row.description.length).toBeLessThanOrEqual(60);
expect(row.tooltip.length).toBeLessThanOrEqual(80);
}
}
});
test("has a titled row for every state the backend sends", () => {
test("has a row for every state the backend sends", () => {
// A missing key here fails the typecheck, so a new state can't be left out.
const states: Record<HelperState, true> = {
unavailable: true,
@@ -106,7 +111,7 @@ describe("helperRow", () => {
};
for (const state of Object.keys(states) as HelperState[]) {
for (const backend of ["helper", "admin_prompt", "sudo"] as const) {
expect(helperRow(state, backend).title).toMatch(/^Helper: \S/);
expect(helperRow(state, backend).description).toMatch(/helper.*\.$/);
}
}
});
@@ -114,15 +119,15 @@ describe("helperRow", () => {
test("still gives a row for a state this build doesn't know", () => {
const row = helperRow("from_the_future" as HelperState, "admin_prompt");
expect(row).toEqual({
title: "Helper: from_the_future",
description: "Changes use the password prompt.",
description: "The helper is from_the_future.",
tooltip: "The helper changes the system proxy without a password prompt.",
action: null,
});
});
test("says plainly that a dev build cannot use it", () => {
expect(helperRow("unavailable", "admin_prompt")).toMatchObject({
title: "Helper: not in dev builds",
description: "The helper is not available in dev builds.",
action: null,
});
});
+26 -22
View File
@@ -28,7 +28,7 @@ export interface SystemProxySwitch {
/** What the system proxy switch shows */
export function systemProxySwitch(state: SystemProxyState): SystemProxySwitch {
if (state.pending != null) {
const description = state.helper ? "Changing the system proxy" : "Waiting for your password";
const description = state.helper ? "Changing the system proxy" : "Waiting for macOS";
return { checked: state.pending, description, failed: false };
}
// A failed change leaves the switch where the system proxy actually is.
@@ -37,71 +37,75 @@ export function systemProxySwitch(state: SystemProxyState): SystemProxySwitch {
const cancelled = /cancel/i.test(state.error);
return {
checked,
description: cancelled ? "Cancelled in the password prompt" : state.error,
description: cancelled ? "The password prompt was cancelled." : state.error,
failed: true,
};
}
return {
checked,
description: checked
? "Routing this Mac's web traffic through Yaak Proxy"
: `Not routing. Only apps pointed at port ${state.port ?? 9090} are captured`,
? "Traffic from every app will be recorded."
: `Only traffic sent to port ${state.port ?? 9090} will be recorded.`,
failed: false,
};
}
export interface HelperRow {
title: string;
/** Which way changes go, and why */
/** The helper's state, in one sentence */
description: string;
/** What the helper is for, or what this state still needs, behind the row's info icon */
tooltip: string;
/** What the row's button does, if it has one: the first two open the approval dialog */
action: "install" | "approve" | "unregister" | null;
}
/** The privileged helper's row in the state pill menu, kept short */
/** What the helper is for, the same in every state but one */
const HELPER_TOOLTIP = "The helper changes the system proxy without a password prompt.";
/** The privileged helper's row in Settings, kept short */
export function helperRow(helper: HelperState, backend: SystemProxyBackend): HelperRow {
switch (helper) {
case "unavailable":
return {
title: "Helper: not in dev builds",
description: "Changes use the password prompt.",
description: "The helper is not available in dev builds.",
tooltip: HELPER_TOOLTIP,
action: null,
};
case "not_found":
// What macOS reports until the helper is first installed.
return {
title: "Helper: not found by macOS",
description: "Changes use the password prompt.",
description: "macOS could not find the helper.",
tooltip: HELPER_TOOLTIP,
action: "install",
};
case "not_registered":
return {
title: "Helper: not installed",
description: "Changes use the password prompt.",
description: "The helper is not installed.",
tooltip: HELPER_TOOLTIP,
action: "install",
};
case "requires_approval":
return {
title: "Helper: awaiting approval",
description: "Changes use the password prompt.",
description: "The helper is awaiting approval.",
tooltip: HELPER_TOOLTIP,
action: "approve",
};
case "enabled":
return {
title: "Helper: enabled",
description:
description: "The helper is installed.",
tooltip:
backend === "helper"
? "No password prompts."
: "One more password prompt puts back your settings.",
? HELPER_TOOLTIP
: "One more password prompt is needed to restore the system proxy settings.",
action: "unregister",
};
default: {
// Every state is handled above; one this build doesn't know still gets a row, rather
// than taking the whole menu down.
// than taking the whole page down.
const unknown: never = helper;
return {
title: `Helper: ${String(unknown)}`,
description: "Changes use the password prompt.",
description: `The helper is ${String(unknown)}.`,
tooltip: HELPER_TOOLTIP,
action: null,
};
}
+1 -1
View File
@@ -672,7 +672,7 @@ droppedHeaders: Array<DroppedHeader>,
*/
keptHeaders: number,
/**
* The body as it is saved: "JSON", "form URL-encoded with 7 fields". None without a body.
* The body as it is saved: "JSON", "a URL-encoded form with 7 fields". None without a body.
*/
body: string | null,
/**
+1 -2
View File
@@ -12,8 +12,7 @@ use yaak_models::blob_manager::BlobManager;
const MESSAGE_PAGE: usize = 500;
/// What a WebSocket's response body reads as: it has none.
pub const WEBSOCKET_BODY_NOTE: &str =
"A WebSocket has no body after its 101 response. Its frames are its messages.";
pub const WEBSOCKET_BODY_NOTE: &str = "A WebSocket has no body";
/// Whether a flow's body is its stream's messages rather than bytes stored on their own: an
/// event stream's response, a gRPC call's request, and its response when the server answered
+1 -1
View File
@@ -395,7 +395,7 @@ pub(crate) fn already_owned(owner: &ControlInfo) -> String {
pub(crate) fn owner_name(owner: Owner) -> &'static str {
match owner {
Owner::App => "Yaak Proxy",
Owner::Cli => "the yaak CLI",
Owner::Cli => "the CLI",
}
}
+6 -20
View File
@@ -65,14 +65,11 @@ pub fn flow_to_request(flow: &Flow, body: &DecodedBody) -> Conversion {
let (headers, dropped_headers) = split_headers(flow, body);
let has_authorization = headers.iter().any(|h| h.name.eq_ignore_ascii_case("authorization"));
if has_authorization {
warnings.push("The Authorization header is saved as captured, in plain text.".into());
warnings.push("The Authorization header is saved in plain text.".into());
}
let (body_type, body_fields) = request_body(flow, body, &mut warnings);
if body.truncated {
warnings.push(format!(
"The body was cut at the capture limit, so only its first {} bytes are saved.",
body.data.len()
));
warnings.push(format!("Only the first {} bytes of the body are saved.", body.data.len()));
}
// No name, as in Yaak, which then shows the URL.
@@ -169,10 +166,7 @@ fn request_body(
let content_type = find_header(&flow.request_headers, "content-type").unwrap_or_default();
let mime = content_type.split(';').next().unwrap_or_default().trim().to_ascii_lowercase();
let Ok(text) = std::str::from_utf8(&body.data) else {
warnings.push(
"The body is binary and is saved as text, so bytes that are not UTF-8 are replaced."
.into(),
);
warnings.push("The body is binary and may be altered when saved.".into());
return text_body(BODY_TYPE_OTHER, &String::from_utf8_lossy(&body.data));
};
@@ -195,9 +189,7 @@ fn request_body(
);
}
if mime == "multipart/form-data" {
warnings.push(
"The multipart body is saved as text with its Content-Type, not as form fields.".into(),
);
warnings.push("The multipart body is saved as text, not form fields.".into());
}
text_body(BODY_TYPE_OTHER, text)
}
@@ -300,10 +292,7 @@ mod tests {
("Proxy-Authorization", DropReason::HopByHop),
]
);
assert_eq!(
c.warnings,
vec!["The Authorization header is saved as captured, in plain text."]
);
assert_eq!(c.warnings, vec!["The Authorization header is saved in plain text."]);
}
#[test]
@@ -463,9 +452,6 @@ mod tests {
let c = flow_to_request(&f, &data);
assert_eq!(names(&c.request), vec!["Content-Type"]);
assert_eq!(c.dropped_headers[0].reason, DropReason::BodyDecoded);
assert_eq!(
c.warnings,
vec!["The body was cut at the capture limit, so only its first 4 bytes are saved."]
);
assert_eq!(c.warnings, vec!["Only the first 4 bytes of the body are saved."]);
}
}
+3 -7
View File
@@ -154,10 +154,7 @@ fn any_encrypted(data_dir: &Path) -> bool {
}
fn missing_data_key(data_dir: &Path) -> String {
format!(
"The proxy's captured traffic is encrypted, but its key is missing from {}",
data_dir.display()
)
format!("The encryption key is missing from {}.", data_dir.display())
}
/// Both databases, opened together.
@@ -324,12 +321,11 @@ pub fn encrypt_existing(
Ok(converted)
}
const KEY_UNAVAILABLE: &str =
"Yaak Proxy stores traffic encrypted and couldn't read its key from the keychain";
const KEY_UNAVAILABLE: &str = "Failed to read the encryption key from the keychain";
/// Why the databases cannot be opened, or the CA made, without the data key.
pub fn key_unavailable(reason: &str) -> String {
format!("{KEY_UNAVAILABLE}: {reason}. Allow it access, then retry.")
format!("{KEY_UNAVAILABLE}: {reason}")
}
/// `reason` is [`key_unavailable`]'s: the keychain would not give up the key.
@@ -18,7 +18,7 @@ pub struct FilterHistoryEntry {
}
fn failed(what: &str, e: impl std::fmt::Display) -> String {
format!("Failed to {what} the filter history: {e}")
format!("Failed to {what} the search history: {e}")
}
/// Every filter kept, most recently applied first. Empty for a database from before the
+1 -1
View File
@@ -719,7 +719,7 @@ pub fn delete_all(db: &ProxyQueryManager) -> Result<(), String> {
}
Ok::<_, yaak_database::Error>(())
})
.map_err(|e| format!("Failed to delete captured traffic: {e}"))
.map_err(|e| format!("Failed to delete recorded traffic: {e}"))
}
/// Flows deleted per transaction while deleting cleared ones, so a flow being written never
+10 -6
View File
@@ -387,7 +387,7 @@ async fn find_schema(
}
Err(_) => Ok((None, Some(format!("No schema for {service} on {authority}")))),
},
Err(e) => Ok((None, Some(format!("No schema for {service}; reflection failed: {e}")))),
Err(e) => Ok((None, Some(format!("No schema for {service}. Reflection failed: {e}")))),
}
}
@@ -448,11 +448,15 @@ fn decode_with(
json: yaak_grpc::message_to_json(&decoded)?,
message_type: Some(message_type),
schema: Some(schema),
note: message.truncated.then(|| "The message is cut at the capture limit".into()),
note: message
.truncated
.then(|| "The message was truncated at the capture limit".into()),
}),
Err(e) => {
Ok(schemaless(payload, message, Some(format!("Did not decode as {message_type}: {e}"))))
}
Err(e) => Ok(schemaless(
payload,
message,
Some(format!("Failed to decode as {message_type}: {e}")),
)),
}
}
@@ -460,7 +464,7 @@ fn schemaless(payload: &[u8], message: &FlowMessage, reason: Option<String>) ->
let mut notes: Vec<String> = reason.into_iter().collect();
notes.push("Decoded without a schema: field names and types are guesses".into());
if message.truncated {
notes.push("The message is cut at the capture limit".into());
notes.push("The message was truncated at the capture limit".into());
}
let fields = decode_fields(payload, 0).unwrap_or_else(|| {
notes.push("Not a valid protobuf message: shown as bytes".into());
+9 -7
View File
@@ -153,12 +153,12 @@ mod imp {
}
// SAFETY: unregisters the plist bundled with this app.
unsafe { service.unregisterAndReturnError() }
.map_err(|e| format!("Failed to remove the helper: {}", e.localizedDescription()))?;
.map_err(|e| format!("Failed to uninstall the helper: {}", e.localizedDescription()))?;
Ok(state_of(&service))
}
fn unavailable() -> String {
"The helper is only available in the installed app, not a dev build".to_string()
"The helper is not available in dev builds".to_string()
}
/// Sends one request and reads its response.
@@ -181,13 +181,15 @@ mod imp {
let _ = stream.set_write_timeout(Some(IO_TIMEOUT));
let mut line = serde_json::to_string(request).map_err(|e| e.to_string())?;
line.push('\n');
(&stream).write_all(line.as_bytes()).map_err(|e| format!("The helper hung up: {e}"))?;
(&stream)
.write_all(line.as_bytes())
.map_err(|e| format!("The helper disconnected: {e}"))?;
let mut reply = String::new();
BufReader::new(&stream)
.read_line(&mut reply)
.map_err(|e| format!("The helper did not answer: {e}"))?;
.map_err(|e| format!("The helper did not respond: {e}"))?;
let response: Response = serde_json::from_str(&reply)
.map_err(|e| format!("The helper's answer was not understood: {e}"))?;
.map_err(|e| format!("The helper sent an invalid response: {e}"))?;
response.into_result()
}
@@ -214,12 +216,12 @@ pub fn state() -> HelperState {
#[cfg(not(target_os = "macos"))]
pub fn diagnose() -> String {
"The helper is only available on macOS".to_string()
"The helper is only supported on macOS".to_string()
}
#[cfg(not(target_os = "macos"))]
pub fn register() -> Result<HelperState, String> {
Err("The helper is only available on macOS".to_string())
Err("The helper is only supported on macOS".to_string())
}
#[cfg(not(target_os = "macos"))]
+10 -13
View File
@@ -233,7 +233,7 @@ impl ProxyCtx {
fn lock_reason(&self) -> String {
if self.encrypting().is_some() {
return "traffic captured before this version is being encrypted.".to_string();
return "traffic recorded before this version is being encrypted.".to_string();
}
let reason = self.lock_reason.lock().unwrap_or_else(PoisonError::into_inner).clone();
reason.unwrap_or_else(|| "its key has not been read yet.".to_string())
@@ -275,8 +275,7 @@ impl ProxyCtx {
&& owner.pid != std::process::id()
{
return Err(format!(
"{}, so the traffic it stored can't be encrypted yet. Take over or quit it, then \
retry.",
"{}. Take over or quit it, then retry.",
control::already_owned(&owner)
));
}
@@ -720,7 +719,7 @@ impl ProxyCtx {
.map_err(|e| e.to_string())?;
if !released {
return Err(format!(
"The proxy in {} (pid {}) stopped but did not let go of it",
"Failed to take over from {} (pid {})",
control::owner_name(owner.owner),
owner.pid
));
@@ -808,7 +807,7 @@ impl ProxyCtx {
pub fn set_system_proxy(&self, enabled: bool) -> Result<SystemProxyStatus, String> {
let Some(port) = self.port() else {
if enabled {
return Err("The proxy is not running, so there is nothing to route to".into());
return Err("The proxy is not running".into());
}
// Clears one left pointing at the configured port; asks for nothing otherwise.
self.change_system_proxy(false, self.settings()?.port, false)?;
@@ -2246,7 +2245,7 @@ pub struct SavePreview {
/// Headers saved as captured, Authorization among them.
#[ts(type = "number")]
pub kept_headers: usize,
/// The body as it is saved: "JSON", "form URL-encoded with 7 fields". None without a body.
/// The body as it is saved: "JSON", "a URL-encoded form with 7 fields". None without a body.
pub body: Option<String>,
/// What does not carry over exactly, one sentence each.
pub warnings: Vec<String>,
@@ -2784,8 +2783,8 @@ async fn preview_save(ctx: ProxyCtx, req: PreviewSaveRequest) -> Result<SavePrev
"application/x-www-form-urlencoded" => {
let fields = request.body.get("form").and_then(|f| f.as_array()).map_or(0, Vec::len);
match fields {
1 => "form URL-encoded with 1 field".to_string(),
n => format!("form URL-encoded with {n} fields"),
1 => "a URL-encoded form with 1 field".to_string(),
n => format!("a URL-encoded form with {n} fields"),
}
}
_ => "text".to_string(),
@@ -3184,8 +3183,7 @@ async fn count_saved_queries(
// -- Event loop --
/// Why the engine cannot be taken over from another app.
pub const ANOTHER_APP_OWNS: &str =
"Another Yaak Proxy window owns the engine. Quit it to use this one.";
pub const ANOTHER_APP_OWNS: &str = "Quit the other Yaak Proxy to use this one.";
/// How long taking over waits for the owner to let go of the engine.
const TAKE_OVER_WAIT: Duration = Duration::from_secs(10);
@@ -3229,11 +3227,10 @@ const STOPPED_MID_STREAM: &str = "Yaak Proxy stopped before the stream ended";
const STOPPED_MID_TUNNEL: &str = "Yaak Proxy stopped before the connection closed";
/// Why a failed handshake's flow can't be saved as a request.
pub(crate) const NOT_A_REQUEST: &str =
"Not a request: the client's TLS handshake failed before it sent one";
pub(crate) const NOT_A_REQUEST: &str = "The TLS handshake failed before any request was sent";
/// The error of a flow recording a client's refusal of the proxy's certificate.
const REJECTED_CERTIFICATE: &str = "Rejected the proxy's certificate";
const REJECTED_CERTIFICATE: &str = "Certificate rejected by the app";
/// A flow between its `RequestStart` and its end, with its bodies being written as they
/// stream.
+3 -3
View File
@@ -287,7 +287,7 @@ mod imp {
.map_err(|e| format!("Failed to run osascript: {e}"))?;
// -128 is the dialog's Cancel button.
if String::from_utf8_lossy(&output.stderr).contains("(-128)") {
return Err("Cancelled in the password dialog".to_string());
return Err("The password prompt was cancelled.".to_string());
}
check(&output, "osascript")
}
@@ -647,7 +647,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(SNAPSHOT_FILE);
let ns = fake();
let cancel = |_: &[Vec<String>]| Err("Cancelled in the password dialog".to_string());
let cancel = |_: &[Vec<String>]| Err("The password prompt was cancelled.".to_string());
assert!(set_scripted(&ns, cancel, true, 9090, &path).is_err());
assert!(!path.exists());
assert!(ns.changes.lock().unwrap().is_empty());
@@ -663,7 +663,7 @@ mod tests {
set_scripted(&ns, runner(&ns, &prompts), true, 9090, &path).unwrap();
// A second enable, the engine on a new port, must not save ours as the user's.
set_scripted(&ns, runner(&ns, &prompts), true, 9191, &path).unwrap();
let cancel = |_: &[Vec<String>]| Err("Cancelled in the password dialog".to_string());
let cancel = |_: &[Vec<String>]| Err("The password prompt was cancelled.".to_string());
assert!(set_scripted(&ns, cancel, true, 9191, &path).is_err());
assert!(path.exists(), "a cancelled re-enable keeps the original snapshot");
set_scripted(&ns, runner(&ns, &prompts), false, 9191, &path).unwrap();
+7 -7
View File
@@ -88,14 +88,14 @@ pub fn schema() -> &'static Schema {
field("method", text(), "GET, POST, …", "@method:post"),
field("status", number(), "Response status", "@status:4xx @status:>=400"),
field("type", text(), "Content type", "@type:json"),
field("app", text(), "App or program", "@app:Slack"),
field("app", text(), "App", "@app:Slack"),
field(
"is",
text(),
"error, pending, system, encrypted, rejected, …",
"@is:error -@is:system @is:encrypted",
),
map("header", "A header on either side", "@header.content-type:json"),
map("header", "A request or response header", "@header.content-type:json"),
map("req.header", "A request header", "@req.header.accept:*"),
map("res.header", "A response header", "@res.header.x-cache:hit"),
field("size", bytes(), "Response body size", "@size:>100kb"),
@@ -109,8 +109,8 @@ pub fn schema() -> &'static Schema {
field("port", number(), "Port", "@port:8080"),
field("proto", text(), "h1, h2, ws, sse, grpc", "@proto:h2"),
field("id", number(), "Flow number", "@id:>1200"),
field("cwd", text(), "Working folder", "@cwd:myapp"),
field("pid", number(), "Process id", "@pid:4121"),
field("cwd", text(), "Working directory", "@cwd:myapp"),
field("pid", number(), "Process ID", "@pid:4121"),
field(
"graphql.operation",
text(),
@@ -119,9 +119,9 @@ pub fn schema() -> &'static Schema {
),
field("grpc.service", text(), "gRPC service", "@grpc.service:pkg.Greeter"),
field("grpc.method", text(), "gRPC method", "@grpc.method:SayHello"),
field("body", text(), "Not yet", ""),
field("req.body", text(), "Not yet", ""),
field("res.body", text(), "Not yet", ""),
field("body", text(), "Not supported", ""),
field("req.body", text(), "Not supported", ""),
field("res.body", text(), "Not supported", ""),
],
FreeText::Substring,
)
@@ -391,7 +391,7 @@ fn encrypted_and_rejected() {
url: "https://my.1password.com".into(),
client_http_version: "HTTP/1.1".into(),
tls_rejected: Some("unknown_ca".into()),
error: Some("Rejected the proxy's certificate".into()),
error: Some("Certificate rejected by the app".into()),
elapsed_ms: Some(3),
started_at: (now() - Duration::minutes(5)).naive_utc(),
..Default::default()
+1 -1
View File
@@ -44,7 +44,7 @@ pub fn open(vars: &[(String, String)]) -> Result<(), String> {
#[cfg(not(target_os = "macos"))]
pub fn open(_vars: &[(String, String)]) -> Result<(), String> {
Err("Opening a terminal with the proxy set is only supported on macOS so far".to_string())
Err("Opening a terminal is only supported on macOS".to_string())
}
#[cfg(test)]
+6 -6
View File
@@ -189,7 +189,7 @@ fn stores_nothing_in_plaintext_and_reads_back_with_the_key() {
)
.err()
.unwrap();
assert!(err.contains("couldn't read its key from the keychain"), "{err}");
assert!(err.contains("Failed to read the encryption key from the keychain"), "{err}");
// Nor without any key at all.
let raw = rusqlite::Connection::open(dir.path().join(PROXY_DB)).unwrap();
let err = raw.query_row("SELECT count(*) FROM flows", [], |r| r.get::<_, i64>(0)).unwrap_err();
@@ -366,8 +366,8 @@ fn a_refused_key_refuses_capture() {
let err = ctx.start().unwrap_err();
assert_eq!(
err,
"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."
"Failed to read the encryption key from the keychain: Encryption error: User canceled \
the operation"
);
assert_eq!(ctx.start_error().as_deref(), Some(err.as_str()));
assert_eq!(ctx.port(), None);
@@ -426,8 +426,8 @@ fn waits_for_an_older_owner_before_converting() {
std::fs::write(&control, serde_json::to_string(&owner).unwrap()).unwrap();
let ctx = open_ctx(dir.path(), MasterKeySource::memory());
assert!(ctx.load_ca().err().unwrap().contains("can't be encrypted yet"));
assert!(ctx.start().unwrap_err().starts_with("The proxy is already running in the yaak CLI"));
assert!(ctx.load_ca().err().unwrap().contains("Take over or quit it, then retry."));
assert!(ctx.start().unwrap_err().starts_with("The proxy is already running in the CLI"));
for name in [PROXY_DB, BLOBS_DB] {
assert_eq!(file_state(&dir.path().join(name)).unwrap(), FileState::Plain);
}
@@ -489,7 +489,7 @@ fn a_lost_data_key_is_not_replaced() {
std::fs::remove_file(dir.path().join(DATA_KEY)).unwrap();
let ctx = open_ctx(dir.path(), keys);
let err = ctx.start().unwrap_err();
assert!(err.starts_with("The proxy's captured traffic is encrypted, but its key is missing"));
assert!(err.starts_with("The encryption key is missing from"));
assert!(!dir.path().join(DATA_KEY).exists());
assert_eq!(std::fs::read_to_string(dir.path().join("ca/ca.pem")).unwrap(), pem);
assert!(ctx.db().is_err(), "locked");
+3 -6
View File
@@ -346,10 +346,7 @@ async fn save_writes_the_request_into_yaak() {
let saved = client.save(&flow.id, &target).await.unwrap();
let dropped: Vec<&str> = saved.dropped_headers.iter().map(|h| h.name.as_str()).collect();
assert!(dropped.contains(&"host") && dropped.contains(&"content-length"), "{dropped:?}");
assert_eq!(
saved.warnings,
vec!["The Authorization header is saved as captured, in plain text."]
);
assert_eq!(saved.warnings, vec!["The Authorization header is saved in plain text."]);
// The row is in Yaak's database, written as a sync so a running Yaak picks it up.
let request = query_manager.connect().get_http_request(&saved.request.id).unwrap();
@@ -443,7 +440,7 @@ async fn start_is_refused_while_another_process_owns_the_engine() {
std::fs::write(dir.path().join("control.json"), serde_json::to_vec(&owner).unwrap()).unwrap();
let err = ctx.start().unwrap_err();
assert!(err.contains("already running in the yaak CLI"), "{err}");
assert!(err.contains("already running in the CLI"), "{err}");
assert_eq!(ctx.port(), None);
daemon.kill().unwrap();
@@ -471,7 +468,7 @@ async fn the_app_takes_over_from_the_cli() {
// The app opens onto the CLI's engine and cannot start its own.
let app = ctx(dir.path(), client_dir.path());
assert!(app.serve_control_api().unwrap_err().contains("already running in the yaak CLI"));
assert!(app.serve_control_api().unwrap_err().contains("already running in the CLI"));
let state = rpc(&app, "get_proxy_state", json!({})).await.unwrap();
assert_eq!(state["state"], "stopped");
assert_eq!(state["otherOwner"]["owner"], "cli");
@@ -164,12 +164,12 @@ async fn a_refused_handshake_is_one_flow_per_address() {
assert_eq!(rejected.url, url);
assert_eq!(rejected.method, "");
assert_eq!(rejected.tls_rejected.as_deref(), Some("unknown_ca"));
assert_eq!(rejected.error.as_deref(), Some("Rejected the proxy's certificate"));
assert_eq!(rejected.error.as_deref(), Some("Certificate rejected by the app"));
assert!(!rejected.tunnelled);
assert!(rejected.elapsed_ms.is_some(), "ended, not pending");
// No request was sent, so there is none to save.
let preview = rpc(&ctx, "preview_save", json!({"flowId": rejected.id})).await;
assert!(preview.unwrap_err().starts_with("Not a request"));
assert!(preview.unwrap_err().starts_with("The TLS handshake failed"));
// Passed through from now on, naming the refusal.
let _tunnel = open_tunnel(&ctx, &target).await;
+1 -1
View File
@@ -448,7 +448,7 @@ async fn grpc_web_calls_are_stored_and_decoded() {
let decode = json!({ "flowId": flow.id, "seq": 1 });
let schemaless = rpc(&ctx, "decode_grpc_message", decode.clone()).await.unwrap();
assert_eq!(schemaless["messageType"], Value::Null);
assert!(schemaless["note"].as_str().unwrap().contains("reflection failed"), "{schemaless}");
assert!(schemaless["note"].as_str().unwrap().contains("Reflection failed"), "{schemaless}");
let fields: Value = serde_json::from_str(schemaless["json"].as_str().unwrap()).unwrap();
assert_eq!(fields[0]["string"], "Hello Ada");
assert_eq!(fields[1], json!({ "field": 2, "wireType": "varint", "value": 3 }));
+1 -1
View File
@@ -140,7 +140,7 @@ async fn cmd_open_settings(
let main = app_handle
.get_webview_window(MAIN_WINDOW_LABEL)
.ok_or("The main window is not open to open Settings over")?;
.ok_or("Failed to open Settings: the main window is closed")?;
let win = yaak_window::window::create_child_window(
&main,
"/",
+2 -2
View File
@@ -184,7 +184,7 @@ impl GrpcParser {
message.truncated = true;
message.compressed = compressed;
message.error = Some(format!(
"The body ended partway through this message, {} of its {length} bytes in",
"The body ended after {} of this message's {length} bytes.",
length - remaining
));
Some(message)
@@ -318,7 +318,7 @@ mod tests {
assert_eq!(messages.len(), 1);
let cut = parser.end().expect("the unfinished message");
assert_eq!((&cut.data[..], cut.size, cut.truncated), (&b"hal"[..], 10, true));
assert!(cut.error.as_deref().unwrap().contains("3 of its 10 bytes"));
assert!(cut.error.as_deref().unwrap().contains("3 of this message's 10 bytes"));
// Nothing unfinished: nothing more.
assert!(parser.end().is_none());
// A trailers frame cut short is still a trailers frame.
+3 -3
View File
@@ -348,13 +348,13 @@ impl FrameParser {
Some(inflater) if !inflater.failed => {
if let Err(e) = inflater.inflate(chunk, &mut message.payload) {
inflater.failed = true;
message.error = Some(format!("Failed to inflate the message: {e}"));
message.error = Some(format!("Failed to decompress the message: {e}"));
}
}
Some(_) => {
message.payload.push(chunk);
message.error.get_or_insert_with(|| {
"Not inflated: an earlier message failed to, which lost the context".into()
"This message was not decompressed because an earlier one failed.".into()
});
}
None => message.payload.push(chunk),
@@ -396,7 +396,7 @@ impl FrameParser {
&& let Err(e) = inflater.inflate(&DEFLATE_TAIL, &mut assembly.payload)
{
inflater.failed = true;
assembly.error = Some(format!("Failed to inflate the message: {e}"));
assembly.error = Some(format!("Failed to decompress the message: {e}"));
}
if inflater.no_context_takeover && !inflater.failed {
inflater.decompress.reset(false);
@@ -481,7 +481,7 @@ function StreamEventDetail({
/>
{event.truncated || event.error ? (
<Banner color={event.error ? "danger" : "notice"} className="mb-2 text-sm">
{event.error ?? "The message is cut at the capture limit."}
{event.error ?? "The message was truncated at the capture limit."}
</Banner>
) : (
<span aria-hidden />