Hide secondary windows until the login UI is painted, open external links in the default browser, and add signed Githop auto-updates

Hop-State: A_06FPJAAEMY0VXGY9S90PE90
Hop-Proposal: R_06FPJA9RY0ZAWJ1K0K760W8
Hop-Task: T_06FPJ5688NFG8VWKSFCTZEG
Hop-Attempt: AT_06FPJ5688M08V2K0PEFVQX0
This commit is contained in:
2026-07-15 21:22:22 -07:00
committed by Hop
parent ec3c8d58b0
commit 7c185c9b36
13 changed files with 677 additions and 12 deletions
+26
View File
@@ -35,3 +35,29 @@ cargo test --manifest-path src-tauri/Cargo.toml
- The selected node's complete website experience after authentication.
The node address and HttpOnly website session persist between launches using the native webview's cookie store.
## Auto-updates
Production builds check the newest public release at
`GnosysLabs/Synapsis-Desktop` on Githop. When a newer signed release is present,
Synapsis downloads it, verifies it with the embedded updater public key, installs
it, and relaunches automatically. Development builds do not check for updates.
The encrypted updater private key lives outside this repository at
`/Users/christopher/.tauri/synapsis-desktop-updater.key`. Matching
`TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets are
configured for the Githop repository. Never commit the private key or password.
To prepare a macOS update locally:
1. Increment `version` in `src-tauri/tauri.conf.json`, `src-tauri/Cargo.toml`, and
`package.json`.
2. Export `TAURI_SIGNING_PRIVATE_KEY` and
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` for the updater signing key.
3. Run `bun run release` to build the app, updater archive, and signature.
4. Run `bun run release:manifest` to create `latest.json` beside the archive.
5. Create a Githop release tagged `v<version>` and attach
`Synapsis.app.tar.gz`, `Synapsis.app.tar.gz.sig`, and `latest.json`.
The release tag and manifest tag must match. Override the defaults with
`SYNAPSIS_RELEASE_TAG` and `SYNAPSIS_RELEASE_NOTES` when needed.
+3 -1
View File
@@ -7,7 +7,9 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
"tauri": "tauri",
"release": "tauri build --config src-tauri/tauri.release.conf.json",
"release:manifest": "bun scripts/create-update-manifest.mjs"
},
"dependencies": {
"@tauri-apps/api": "^2",
+31
View File
@@ -0,0 +1,31 @@
import { readFile, writeFile } from "node:fs/promises";
import process from "node:process";
const repository = "https://githop.xyz/GnosysLabs/Synapsis-Desktop";
const bundleDirectory = new URL("../src-tauri/target/release/bundle/macos/", import.meta.url);
const config = JSON.parse(
await readFile(new URL("../src-tauri/tauri.conf.json", import.meta.url), "utf8"),
);
const version = config.version;
const tag = process.env.SYNAPSIS_RELEASE_TAG || `v${version}`;
const notes = process.env.SYNAPSIS_RELEASE_NOTES || `Synapsis ${version}`;
const archiveName = "Synapsis.app.tar.gz";
const signature = (
await readFile(new URL(`${archiveName}.sig`, bundleDirectory), "utf8")
).trim();
const architecture = process.arch === "arm64" ? "aarch64" : "x86_64";
const manifest = {
version,
notes,
pub_date: new Date().toISOString(),
platforms: {
[`darwin-${architecture}`]: {
signature,
url: `${repository}/releases/download/${encodeURIComponent(tag)}/${archiveName}`,
},
},
};
const destination = new URL("latest.json", bundleDirectory);
await writeFile(destination, `${JSON.stringify(manifest, null, 2)}\n`);
console.log(`Created updater manifest for Synapsis ${version}.`);
+361 -6
View File
@@ -47,6 +47,15 @@ version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -725,6 +734,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -1027,6 +1047,16 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "filetime"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
"cfg-if",
"libc",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -1849,6 +1879,25 @@ version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -1894,6 +1943,36 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.18",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn 2.0.119",
]
[[package]]
name = "jni-sys"
version = "0.3.1"
@@ -2109,6 +2188,12 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minisign-verify"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -2388,6 +2473,18 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.13.1",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -2451,6 +2548,17 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "open"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5"
dependencies = [
"dunce",
"is-wsl",
"libc",
]
[[package]]
name = "openssl"
version = "0.10.81"
@@ -2510,6 +2618,20 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]]
name = "pango"
version = "0.18.3"
@@ -3083,15 +3205,20 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -3159,6 +3286,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.0"
@@ -3169,6 +3308,33 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni 0.22.4",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.13"
@@ -3522,6 +3688,22 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "simd_cesu8"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "1.0.3"
@@ -3683,6 +3865,8 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-updater",
]
[[package]]
@@ -3757,7 +3941,7 @@ dependencies = [
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
"jni",
"jni 0.21.1",
"libc",
"log",
"ndk",
@@ -3790,6 +3974,17 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "tar"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
@@ -3813,7 +4008,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
"jni",
"jni 0.21.1",
"libc",
"log",
"mime",
@@ -3944,6 +4139,61 @@ dependencies = [
"url",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29"
dependencies = [
"dunce",
"glob",
"objc2-app-kit",
"objc2-foundation",
"open",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
"url",
"windows",
"zbus",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
dependencies = [
"base64 0.22.1",
"dirs",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.4",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip",
]
[[package]]
name = "tauri-runtime"
version = "2.11.3"
@@ -3954,7 +4204,7 @@ dependencies = [
"dpi",
"gtk",
"http",
"jni",
"jni 0.21.1",
"objc2",
"objc2-ui-kit",
"objc2-web-kit",
@@ -3977,7 +4227,7 @@ checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
dependencies = [
"gtk",
"http",
"jni",
"jni 0.21.1",
"log",
"objc2",
"objc2-app-kit",
@@ -4784,6 +5034,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "1.0.8"
@@ -5052,6 +5311,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -5085,13 +5353,30 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
@@ -5122,6 +5407,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -5134,6 +5425,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -5146,12 +5443,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -5164,6 +5473,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -5176,6 +5491,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -5188,6 +5509,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -5200,6 +5527,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.5.40"
@@ -5264,7 +5597,7 @@ dependencies = [
"gtk",
"http",
"javascriptcore-rs",
"jni",
"jni 0.21.1",
"libc",
"ndk",
"objc2",
@@ -5311,6 +5644,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]]
name = "yoke"
version = "0.8.3"
@@ -5475,6 +5818,18 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.14.0",
"memchr",
]
[[package]]
name = "zmij"
version = "1.0.23"
+2
View File
@@ -23,6 +23,8 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["cookies", "json", "rustls-tls"] }
tauri-plugin-notification = "2"
tauri-plugin-opener = "2"
tauri-plugin-updater = "2"
[dev-dependencies]
http = "1"
+2
View File
@@ -8,6 +8,8 @@ fn main() {
"has_web_session",
"open_web_app",
"open_login_window",
"open_external_url",
"reveal_window",
]),
))
.unwrap();
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-open-external-url"
description = "Enables the open_external_url command without any pre-configured scope."
commands.allow = ["open_external_url"]
[[permission]]
identifier = "deny-open-external-url"
description = "Denies the open_external_url command without any pre-configured scope."
commands.deny = ["open_external_url"]
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-reveal-window"
description = "Enables the reveal_window command without any pre-configured scope."
commands.allow = ["reveal_window"]
[[permission]]
identifier = "deny-reveal-window"
description = "Denies the reveal_window command without any pre-configured scope."
commands.deny = ["reveal_window"]
+2
View File
@@ -9,4 +9,6 @@ commands.allow = [
"has_web_session",
"open_web_app",
"open_login_window",
"open_external_url",
"reveal_window",
]
+195 -5
View File
@@ -6,6 +6,8 @@ use std::sync::Mutex;
use std::time::Duration;
use tauri::webview::{Cookie, PageLoadEvent};
use tauri::{Manager, State, Webview, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_updater::UpdaterExt;
struct AppState {
client: Client,
@@ -56,6 +58,85 @@ struct ErrorPayload {
error: Option<String>,
}
const UPDATER_PUBLIC_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYzOEZCMTg0QzJERkUwNTcKUldSWDROL0NoTEdQODlCOW10cXAvYzNTNitFQkxUd3F2T3UybUV3REJYT1Z6OVczazFmamxhWlIK";
const LATEST_RELEASE_API: &str =
"https://githop.xyz/api/v1/repos/GnosysLabs/Synapsis-Desktop/releases/latest";
#[derive(Debug, Deserialize)]
struct ReleaseAsset {
name: String,
browser_download_url: Url,
}
#[derive(Debug, Deserialize)]
struct ReleaseInfo {
assets: Vec<ReleaseAsset>,
}
fn manifest_url(release: ReleaseInfo) -> Option<Url> {
release
.assets
.into_iter()
.find(|asset| asset.name == "latest.json")
.map(|asset| asset.browser_download_url)
}
async fn latest_manifest_url(client: &Client) -> Result<Option<Url>, String> {
let response = client
.get(LATEST_RELEASE_API)
.header("Accept", "application/json")
.send()
.await
.map_err(|error| format!("Couldnt reach the Synapsis release feed: {error}"))?;
if response.status() == StatusCode::NOT_FOUND {
return Ok(None);
}
let release = decode_response::<ReleaseInfo>(response).await?;
Ok(manifest_url(release))
}
fn start_auto_updater(app: tauri::AppHandle, client: Client) {
if cfg!(debug_assertions) {
return;
}
tauri::async_runtime::spawn(async move {
let endpoint = match latest_manifest_url(&client).await {
Ok(Some(endpoint)) => endpoint,
Ok(None) => return,
Err(error) => {
eprintln!("Couldnt discover the latest Synapsis release: {error}");
return;
}
};
let updater = match app
.updater_builder()
.pubkey(UPDATER_PUBLIC_KEY)
.endpoints(vec![endpoint])
.and_then(|builder| builder.build())
{
Ok(updater) => updater,
Err(error) => {
eprintln!("Couldnt initialize Synapsis auto-updates: {error}");
return;
}
};
match updater.check().await {
Ok(Some(update)) => {
let version = update.version.clone();
if let Err(error) = update.download_and_install(|_, _| {}, || {}).await {
eprintln!("Couldnt install Synapsis {version}: {error}");
return;
}
app.restart();
}
Ok(None) => {}
Err(error) => eprintln!("Couldnt check for a Synapsis update: {error}"),
}
});
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RegistrationRequest<'a> {
@@ -304,7 +385,11 @@ fn remote_node_capability(origin: &str, window_label: &str, sequence: u64) -> St
"remote": {
"urls": [format!("{origin}/*")]
},
"permissions": ["notification:default", "allow-open-login-window"]
"permissions": [
"notification:default",
"allow-open-login-window",
"allow-open-external-url"
]
})
.to_string()
}
@@ -377,6 +462,48 @@ async fn install_new_window_button(client: Client, webview: Webview, page_url: U
let _ = webview.eval(new_window_button_script(accent_color.as_deref()));
}
fn external_link_script() -> &'static str {
r#"
(() => {
if (window.__SYNAPSIS_EXTERNAL_LINKS_INSTALLED__) return;
window.__SYNAPSIS_EXTERNAL_LINKS_INSTALLED__ = true;
document.addEventListener("click", (event) => {
if (event.defaultPrevented || event.button !== 0) return;
const link = event.target instanceof Element ? event.target.closest("a[href]") : null;
if (!link || link.hasAttribute("download")) return;
let destination;
try {
destination = new URL(link.href, window.location.href);
} catch {
return;
}
if (!/^https?:$/.test(destination.protocol) || destination.origin === window.location.origin) {
return;
}
event.preventDefault();
event.stopImmediatePropagation();
window.__TAURI_INTERNALS__.invoke("open_external_url", { url: destination.href })
.catch((error) => console.error("Could not open external link", error));
}, true);
})();
"#
}
#[tauri::command]
fn open_external_url(app: tauri::AppHandle, url: String) -> Result<(), String> {
let destination = Url::parse(&url).map_err(|_| "This link is not a valid URL.".to_string())?;
if !matches!(destination.scheme(), "http" | "https") {
return Err("Only web links can be opened externally.".into());
}
app.opener()
.open_url(destination.as_str(), None::<&str>)
.map_err(|error| format!("Couldnt open this link in the default browser: {error}"))
}
#[tauri::command]
async fn open_login_window(
app: tauri::AppHandle,
@@ -395,6 +522,7 @@ async fn open_login_window(
.inner_size(1280.0, 820.0)
.min_inner_size(720.0, 560.0)
.center()
.visible(false)
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Visible)
.theme(Some(tauri::Theme::Dark))
@@ -404,6 +532,19 @@ async fn open_login_window(
.map_err(|error| format!("Couldnt open another Synapsis window: {error}"))
}
#[tauri::command]
fn reveal_window(webview: WebviewWindow) -> Result<(), String> {
if !webview.label().starts_with("node-") {
return Ok(());
}
webview
.show()
.map_err(|error| format!("Couldnt reveal the Synapsis window: {error}"))?;
webview
.set_focus()
.map_err(|error| format!("Couldnt focus the Synapsis window: {error}"))
}
#[tauri::command]
async fn logout(state: State<'_, AppState>, node_url: String) -> Result<(), String> {
let base = normalize_node(&node_url)?;
@@ -432,19 +573,34 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_opener::init())
.plugin(
tauri_plugin_updater::Builder::new()
.pubkey(UPDATER_PUBLIC_KEY)
.build(),
)
.manage(AppState {
client,
notification_capability_sequence: AtomicU64::new(0),
window_sequence: AtomicU64::new(1),
entry_urls: Mutex::new(HashMap::new()),
})
.setup(|app| {
let client = app.state::<AppState>().client.clone();
start_auto_updater(app.handle().clone(), client);
Ok(())
})
.on_page_load(|webview, payload| {
if payload.event() != PageLoadEvent::Finished
|| !matches!(payload.url().scheme(), "http" | "https")
{
if payload.event() != PageLoadEvent::Finished {
return;
}
if !matches!(payload.url().scheme(), "http" | "https") {
return;
}
let _ = webview.eval(external_link_script());
let Ok(cookies) = webview.cookies_for_url(payload.url().clone()) else {
return;
};
@@ -493,7 +649,9 @@ pub fn run() {
logout,
has_web_session,
open_web_app,
open_login_window
open_login_window,
open_external_url,
reveal_window
])
.run(tauri::generate_context!())
.expect("error while running Synapsis");
@@ -559,6 +717,7 @@ mod tests {
assert_eq!(value["remote"]["urls"][0], "https://example.com/*");
assert_eq!(value["permissions"][0], "notification:default");
assert_eq!(value["permissions"][1], "allow-open-login-window");
assert_eq!(value["permissions"][2], "allow-open-external-url");
}
#[test]
@@ -571,6 +730,15 @@ mod tests {
assert!(script.contains("open_login_window"));
}
#[test]
fn intercepts_external_links_without_hijacking_same_origin_navigation() {
let script = external_link_script();
assert!(script.contains("destination.origin === window.location.origin"));
assert!(script.contains("open_external_url"));
assert!(script.contains("event.preventDefault()"));
}
#[test]
fn returns_to_entry_only_for_logged_out_explore() {
let explore = Url::parse("https://example.com/explore").unwrap();
@@ -581,4 +749,26 @@ mod tests {
assert!(!is_logged_out_explore(&timeline, &[]));
assert!(!is_logged_out_explore(&explore, &[active_cookie]));
}
#[test]
fn selects_latest_json_from_githop_release_assets() {
let release = ReleaseInfo {
assets: vec![
ReleaseAsset {
name: "Synapsis.app.tar.gz".into(),
browser_download_url: Url::parse("https://example.com/Synapsis.app.tar.gz")
.unwrap(),
},
ReleaseAsset {
name: "latest.json".into(),
browser_download_url: Url::parse("https://example.com/latest.json").unwrap(),
},
],
};
assert_eq!(
manifest_url(release).unwrap().as_str(),
"https://example.com/latest.json"
);
}
}
+6
View File
@@ -38,5 +38,11 @@
"icons/icon.icns",
"icons/icon.ico"
]
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYzOEZCMTg0QzJERkUwNTcKUldSWDROL0NoTEdQODlCOW10cXAvYzNTNitFQkxUd3F2T3UybUV3REJYT1Z6OVczazFmamxhWlIK",
"endpoints": []
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"bundle": {
"createUpdaterArtifacts": true
}
}
+22
View File
@@ -264,6 +264,10 @@ function SignalField() {
}
function App() {
const isSecondaryWindow = useRef(
Boolean(window.__SYNAPSIS_FRESH_LOGIN__) ||
new URLSearchParams(window.location.search).has("newWindow"),
);
const [screen, setScreen] = useState<Screen>("node");
const [nodeInput, setNodeInput] = useState(DEFAULT_NODE);
const [nodeUrl, setNodeUrl] = useState("");
@@ -325,6 +329,24 @@ function App() {
void restore();
}, []);
useEffect(() => {
if (bootstrapping || !isSecondaryWindow.current || !isTauri()) return;
let cancelled = false;
let secondFrame = 0;
const firstFrame = requestAnimationFrame(() => {
secondFrame = requestAnimationFrame(() => {
if (!cancelled) {
void invoke("reveal_window");
}
});
});
return () => {
cancelled = true;
cancelAnimationFrame(firstFrame);
cancelAnimationFrame(secondFrame);
};
}, [bootstrapping]);
async function connect(event: FormEvent) {
event.preventDefault();
if (!isTauri()) {