From 8cb2901df985266b172ee3b41d6d0eb11d637aa3 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sat, 24 Oct 2026 11:38:40 +0900 Subject: [PATCH 001/123] ci: publish AltKeeper Docker image for Raspberry Pi arm64 --- .github/workflows/docker.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4bb731a..6cf3e0b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,7 +1,7 @@ # Prova il codice e pubblica l'immagine Docker su GitHub Container Registry (ghcr.io). # # - Pull request: prova i test e la compilazione dell'immagine (amd64), senza pubblicare. -# - Push su main: pubblica `latest` e `sha-` (solo amd64: è la compilazione veloce). +# - Push su main: pubblica `latest` e `sha-` per amd64 e arm64. # - Tag `vX.Y.Z` (per esempio `git tag v0.2.0 && git push --tags`): pubblica `X.Y.Z`, `X.Y` e # `latest`, per amd64 e arm64 (l'arm64 gira sotto emulazione: richiede parecchi minuti). # @@ -65,8 +65,8 @@ jobs: - uses: docker/build-push-action@v7 with: context: . - # I tag di versione pubblicano anche arm64; il resto solo amd64 (più rapido). - platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }} + # Il fork pubblica amd64 e arm64 anche su main, per Raspberry Pi. + platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From f712a14eb51ad486474b83e08decb47eb25b0bfd Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 08:48:32 +0900 Subject: [PATCH 002/123] feat: query installed user apps through installation_proxy --- src/phone.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/phone.rs b/src/phone.rs index 5e3e6d1..4fa6d9d 100644 --- a/src/phone.rs +++ b/src/phone.rs @@ -2,6 +2,7 @@ //! TLS-PSK, RSD e client misagent per i profili di provisioning. use std::any::Any; +use std::collections::HashSet; use std::net::{IpAddr, SocketAddr}; use std::path::Path; use std::time::{Duration, Instant}; @@ -137,6 +138,31 @@ impl PhoneLink { misagent.copy_all().await.map_err(dbg) } + /// Installed user-app bundle IDs, not Apple Developer App IDs or provisioning profiles. + /// Fail closed on a lookup error: unattended renewal must never guess. + pub async fn installed_app_ids(&mut self) -> Result, String> { + let apps = match self { + Self::Legacy { provider, .. } + | Self::Rsd { + usb: Some(provider), + .. + } => { + let mut client = InstallationProxyClient::connect(provider).await.map_err(dbg)?; + client.get_apps(Some("User"), None).await.map_err(dbg)? + } + Self::Rsd { + adapter, + hs, + usb: None, + .. + } => { + let mut client = InstallationProxyClient::connect_rsd(adapter, hs).await.map_err(dbg)?; + client.get_apps(Some("User"), None).await.map_err(dbg)? + } + }; + Ok(apps.into_keys().collect()) + } + pub async fn install_profile(&mut self, profile: Vec) -> Result<(), String> { let misagent = match self { Self::Rsd { misagent, .. } | Self::Legacy { misagent, .. } => misagent, From 151cc09916e619e5b12a0a78d37b6d07493f8213 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 08:48:57 +0900 Subject: [PATCH 003/123] feat: filter unattended renewals by installed apps and share verified profile removal --- src/main.rs | 80 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7943bef..0ec2571 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ //! altkeeper pair-usb [file] crea il pairing remoto via USB //! altkeeper login accede e salva l'account //! altkeeper apps elenca App ID e scadenze -//! altkeeper renew [--phone ip:porta] [--dry-run] [--force] [--min-days N] +//! altkeeper renew [--phone ip:porta] [--dry-run] [--force] [--min-days N] [--bundle ID] [--installed-only] //! altkeeper profile-remove [--phone ip:porta] toglie un profilo (ne salva prima una copia) //! altkeeper profile-restore [--phone ip:porta] rimette un profilo salvato //! altkeeper serve [--bind ip:porta] [--pin PIN] [--dir cartella] [--phone ip:porta] @@ -229,6 +229,8 @@ async fn cmd_renew( dry_run: bool, force: bool, min_days: i64, + bundle_filter: Option<&str>, + installed_only: bool, say: &mut dyn FnMut(&str), ) -> Result<(), String> { let acc = apple::load_account(Path::new("."))?; @@ -255,14 +257,40 @@ async fn cmd_renew( // finché il profilo vecchio non scade). let latest = profile::latest_by_bundle(mine); + // Only unattended/expiring-app renewal requires proof of actual installation. + // A failed listing aborts the operation rather than silently renewing stale profiles. + let installed = if installed_only { + Some(phone::with_timeout( + "Installed app lookup", + std::time::Duration::from_secs(30), + link.installed_app_ids(), + ).await?) + } else { + None + }; + if let Some(apps) = &installed { + say(&format!("Detected {} installed user apps on the iPhone.", apps.len())); + } + // Si decide cosa rinnovare prima di parlare con Apple: se niente è in scadenza non si // accede affatto (meno login, meno rischio che Apple limiti le richieste). let mut due: Vec<(&str, i64)> = Vec::new(); + let mut requested_found = false; for p in latest { let Some(bundle) = p.bundle_id.as_deref() else { say(&format!(" {}: bundle ID cannot be read, skipping", p.name)); continue; }; + if let Some(selected) = bundle_filter { + if selected != bundle { continue; } + requested_found = true; + } + if let Some(apps) = &installed { + if !apps.contains(bundle) { + say(&format!(" {bundle}: no installed app found, skipping")); + continue; + } + } let days = p.days_left().unwrap_or(-1); if !force && days > min_days { say(&format!(" {bundle}: expires in {days} days, no renewal needed")); @@ -270,6 +298,9 @@ async fn cmd_renew( } due.push((bundle, days)); } + if bundle_filter.is_some() && !requested_found { + return Err("Selected app has no provisioning profile for the configured Apple team.".into()); + } if due.is_empty() { say("Nothing to renew: skipping Apple sign-in."); return Ok(()); @@ -318,30 +349,50 @@ async fn cmd_renew( /// Toglie dal telefono il profilo con questo UUID, dopo averne salvato e riletto una copia in /// `profili-salvati/` (si rimette con `profile-restore`). Serve a capire se iOS accetta i /// profili nuovi per un'app installata con quello originale. -async fn cmd_profile_remove(addr: Option, uuid: Option) -> Result<(), String> { - let uuid = uuid.ok_or(crate::tr!("uso: altkeeper profile-remove ", "usage: altkeeper profile-remove "))?; - let mut link = open_phone(addr, &mut |s| println!("{s}")).await?; +/// Validate an exact UUID, back up its original bytes, then remove only that profile. +/// The web endpoint also restricts deletion to the configured Apple team. +async fn remove_profile_with_backup( + link: &mut phone::PhoneLink, + uuid: &str, + required_team: Option<&str>, +) -> Result { + if uuid.is_empty() || uuid.len() > 64 || !uuid.bytes().all(|b| b.is_ascii_hexdigit() || b == b'-') { + return Err("Invalid profile UUID.".into()); + } let raw = link.profiles().await?; let (found, parsed) = raw .iter() .find_map(|r| { profile::parse(r) - .filter(|p| p.uuid.eq_ignore_ascii_case(&uuid)) + .filter(|p| p.uuid.eq_ignore_ascii_case(uuid)) .map(|p| (r, p)) }) - .ok_or(crate::tr!("profilo non trovato sul telefono", "profile not found on the phone"))?; + .ok_or("Profile not found on the iPhone.")?; + if let Some(team) = required_team { + if parsed.team_id.as_deref() != Some(team) || parsed.bundle_id.is_none() { + return Err("Refusing to remove a profile outside your Apple team.".into()); + } + } std::fs::create_dir_all(SAVED_DIR).map_err(dbg)?; fsperm::restrict_dir(Path::new(SAVED_DIR)).map_err(dbg)?; let path = Path::new(SAVED_DIR).join(format!("{}.mobileprovision", parsed.uuid)); fsperm::write_private(&path, found).map_err(dbg)?; if std::fs::read(&path).map_err(dbg)? != *found { - return Err(crate::tr!(format!("la copia in {} non coincide: non tolgo niente", path.display()), format!("saved copy at {} does not match; refusing to remove the profile", path.display()))); + return Err("Profile backup verification failed; nothing was removed.".into()); } - println!("{}", crate::tr!(format!("Copia salvata in {}", path.display()), format!("Copy saved to {}", path.display()))); - link.remove_profile(&parsed.uuid).await?; - println!("{}", crate::tr!(format!("Tolto dal telefono: {} [{}]", parsed.name, parsed.uuid), format!("Removed from the phone: {} [{}]", parsed.name, parsed.uuid))); + Ok(parsed) +} + +async fn cmd_profile_remove(addr: Option, uuid: Option) -> Result<(), String> { + let uuid = uuid.ok_or(crate::tr!("uso: altkeeper profile-remove ", "usage: altkeeper profile-remove "))?; + let mut link = open_phone(addr, &mut |s| println!("{s}")).await?; + let parsed = remove_profile_with_backup(&mut link, &uuid, None).await?; + println!("{}", crate::tr!( + format!("Tolto dal telefono: {} [{}] (copia in {})", parsed.name, parsed.uuid, SAVED_DIR), + format!("Removed from the phone: {} [{}] (backup in {})", parsed.name, parsed.uuid, SAVED_DIR) + )); Ok(()) } @@ -453,11 +504,18 @@ async fn run() -> Result<(), String> { .map(|v| v.parse::().map_err(|e| crate::tr!(format!("--min-days non valido: {e}"), format!("invalid --min-days: {e}")))) .transpose()? .unwrap_or(3); + let bundle = opt("--bundle"); + let force = flag("--force"); + // Default CLI renewal (e.g. cron) and web "due" use installed-only. + // Explicit --force without --installed-only retains manual override behavior. + let installed_only = flag("--installed-only") || (!force && bundle.is_none()); cmd_renew( phone_addr(opt("--phone"))?, flag("--dry-run"), - flag("--force"), + force, min_days, + bundle.as_deref(), + installed_only, &mut |s| println!("{s}"), ) .await From 491b05cc70c5783f00e737eb8f8be54758524140 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 08:49:33 +0900 Subject: [PATCH 004/123] feat(web): per-app renew and safe team-scoped profile deletion with backups --- src/web.rs | 116 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 8 deletions(-) diff --git a/src/web.rs b/src/web.rs index ff6e532..fe1fb63 100644 --- a/src/web.rs +++ b/src/web.rs @@ -64,6 +64,7 @@ struct Snapshot { detail: Option, profiles_total: usize, apps: Vec, + profiles: Vec, } #[derive(Clone, PartialEq, Debug)] @@ -75,6 +76,15 @@ struct AppView { level: &'static str, } +#[derive(Clone, PartialEq, Debug)] +struct ProfileView { + uuid: String, + bundle: String, + label: String, + days: i64, + expires: String, +} + #[derive(Default, Clone)] struct JobView { kind: String, @@ -195,6 +205,7 @@ pub async fn serve( .route("/api/state", get(state)) .route("/api/check", post(check)) .route("/api/renew", post(renew)) + .route("/api/profile/remove", post(remove_profile)) .route( "/api/install", post(install).layer(DefaultBodyLimit::max(MAX_IPA_BYTES + 1024 * 1024)), @@ -392,6 +403,10 @@ async fn state(State(app): State>) -> Json { "bundle": a.bundle, "label": a.label, "days": a.days, "expires": a.expires, "level": a.level, })).collect::>(), + "profiles": g.snapshot.profiles.iter().map(|p| json!({ + "uuid": p.uuid, "bundle": p.bundle, "label": p.label, + "days": p.days, "expires": p.expires, + })).collect::>(), "job": { "kind": g.job.kind, "running": g.job.running, "lines": g.job.lines, "ok": g.job.ok, @@ -416,17 +431,27 @@ async fn check(State(app): State>) -> (StatusCode, Json) { #[derive(Deserialize)] struct RenewReq { - /// "due" rinnova solo le app in scadenza, "now" le rinnova tutte. + /// "due" renews expiring installed apps; "now" all profiles; "single" one bundle. mode: String, + bundle: Option, } async fn renew( State(app): State>, Json(req): Json, ) -> (StatusCode, Json) { - let force = match req.mode.as_str() { - "now" => true, - "due" => false, + let (force, installed_only, bundle) = match req.mode.as_str() { + "now" => (true, false, None), + "due" => (false, true, None), + "single" => { + let Some(b) = req.bundle.filter(|b| !b.is_empty() && b.len() <= 255) else { + return json_err(StatusCode::BAD_REQUEST, "Select an app to renew."); + }; + if !app.lock().snapshot.apps.iter().any(|a| a.bundle == b) { + return json_err(StatusCode::NOT_FOUND, "App is not in the current profile list."); + } + (true, false, Some(b)) + } _ => return json_err(StatusCode::BAD_REQUEST, "Invalid mode"), }; match apple::read_account(Path::new(".")) { @@ -444,13 +469,42 @@ async fn renew( { let mut g = app.lock(); g.job = JobView { - kind: if force { "renew_all".into() } else { "renew_due".into() }, + kind: if bundle.is_some() { "renew_one".into() } + else if force { "renew_all".into() } else { "renew_due".into() }, running: true, lines: Vec::new(), ok: None, }; } - spawn_job(guard, move |app| run_renew(app, force)); + spawn_job(guard, move |app| run_renew(app, force, bundle, installed_only)); + json_ok() +} + +#[derive(Deserialize)] +struct RemoveProfileReq { + uuid: String, +} + +async fn remove_profile( + State(app): State>, + Json(req): Json, +) -> (StatusCode, Json) { + if !app.lock().snapshot.profiles.iter().any(|p| p.uuid.eq_ignore_ascii_case(&req.uuid)) { + return json_err(StatusCode::NOT_FOUND, "Profile is not in your team's current list. Check again."); + } + let Some(guard) = try_busy(&app) else { + return json_err(StatusCode::CONFLICT, BUSY_MSG); + }; + { + let mut g = app.lock(); + g.job = JobView { + kind: "remove_profile".into(), + running: true, + lines: Vec::new(), + ok: None, + }; + } + spawn_job(guard, move |app| run_remove_profile(app, req.uuid)); json_ok() } @@ -686,13 +740,18 @@ async fn run_check(app: Arc) { g.snapshot.detail = None; g.snapshot.profiles_total = profiles.len(); g.snapshot.apps = team.as_deref().map(|t| app_views(&profiles, t)).unwrap_or_default(); + g.snapshot.profiles = team.as_deref().map(|t| profile_views(&profiles, t)).unwrap_or_default(); } Ok(Err(e)) => { + g.snapshot.apps.clear(); + g.snapshot.profiles.clear(); g.snapshot.reachable = Some(false); g.snapshot.error = Some(friendly_error(&e)); g.snapshot.detail = Some(e.chars().take(400).collect()); } Err(_) => { + g.snapshot.apps.clear(); + g.snapshot.profiles.clear(); g.snapshot.reachable = Some(false); g.snapshot.error = Some(friendly_error("non risponde")); g.snapshot.detail = Some("nessuna risposta entro 45 secondi".into()); @@ -700,9 +759,9 @@ async fn run_check(app: Arc) { } } -async fn run_renew(app: Arc, force: bool) { +async fn run_renew(app: Arc, force: bool, bundle: Option, installed_only: bool) { let mut say = |s: &str| app.push_line(&english_job_line(s)); - let result = crate::cmd_renew(app.phone, false, force, 3, &mut say).await; + let result = crate::cmd_renew(app.phone, false, force, 3, bundle.as_deref(), installed_only, &mut say).await; let ok = result.is_ok(); if let Err(e) = &result { @@ -719,6 +778,29 @@ async fn run_renew(app: Arc, force: bool) { run_check(app).await; } +async fn run_remove_profile(app: Arc, uuid: String) { + let result: Result<(), String> = async { + let team = apple::read_account(Path::new(".")) + .and_then(|a| a.team_id) + .ok_or("Sign in to select a team before removing profiles.")?; + let (_, mut link) = phone::connect_auto(app.phone, PAIRING_FILE, |_: &str| {}).await?; + let p = crate::remove_profile_with_backup(&mut link, &uuid, Some(&team)).await?; + app.push_line(&format!("Removed profile for {} [{}]. Backup saved in {}.", + p.bundle_id.as_deref().unwrap_or("?"), p.uuid, crate::SAVED_DIR)); + Ok(()) + }.await; + let ok = result.is_ok(); + if let Err(e) = result { + app.push_line(&format!("Error: {}", short_error(&e))); + } + { + let mut g = app.lock(); + g.job.running = false; + g.job.ok = Some(ok); + } + run_check(app).await; +} + async fn run_install(app: Arc, upload: UploadedIpa) { let result = install_ipa(&app, &upload).await; let ok = result.is_ok(); @@ -879,6 +961,24 @@ fn app_views(profiles: &[profile::PhoneProfile], team: &str) -> Vec { views } +fn profile_views(profiles: &[profile::PhoneProfile], team: &str) -> Vec { + let mut views: Vec<_> = profiles.iter() + .filter(|p| p.team_id.as_deref() == Some(team)) + .filter_map(|p| { + let bundle = p.bundle_id.clone()?; + Some(ProfileView { + uuid: p.uuid.clone(), + label: label_of(&bundle, team), + bundle, + days: p.days_left().unwrap_or(-1), + expires: p.expires_text(), + }) + }) + .collect(); + views.sort_by(|a, b| a.label.cmp(&b.label).then_with(|| b.expires.cmp(&a.expires))); + views +} + /// Traduce gli errori tecnici in una frase comprensibile. fn friendly_error(err: &str) -> String { let e = err.to_lowercase(); From 14f2a6cf6b36e19684d9876e7ac92a5bab43ed3b Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 08:50:40 +0900 Subject: [PATCH 005/123] feat: fall back to all profile renewals when installed-app lookup fails --- src/main.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0ec2571..74e049b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -257,20 +257,29 @@ async fn cmd_renew( // finché il profilo vecchio non scade). let latest = profile::latest_by_bundle(mine); - // Only unattended/expiring-app renewal requires proof of actual installation. - // A failed listing aborts the operation rather than silently renewing stale profiles. + // When the installed-app lookup fails, explicitly fall back to renewing ALL + // known team profiles. This is intentional and must be visible in logs. + let mut fallback_renew_all = false; let installed = if installed_only { - Some(phone::with_timeout( + match phone::with_timeout( "Installed app lookup", std::time::Duration::from_secs(30), link.installed_app_ids(), - ).await?) + ).await { + Ok(apps) => { + say(&format!("Detected {} installed user apps on the iPhone.", apps.len())); + Some(apps) + } + Err(err) => { + fallback_renew_all = true; + say(&format!("WARNING: installed-app lookup failed: {err}")); + say("FALLBACK: renewing ALL known profiles for this Apple team, regardless of expiry."); + None + } + } } else { None }; - if let Some(apps) = &installed { - say(&format!("Detected {} installed user apps on the iPhone.", apps.len())); - } // Si decide cosa rinnovare prima di parlare con Apple: se niente è in scadenza non si // accede affatto (meno login, meno rischio che Apple limiti le richieste). @@ -292,7 +301,7 @@ async fn cmd_renew( } } let days = p.days_left().unwrap_or(-1); - if !force && days > min_days { + if !force && !fallback_renew_all && days > min_days { say(&format!(" {bundle}: expires in {days} days, no renewal needed")); continue; } From 773b5c7b5af1c71e7238d7e50f570af95c038fe2 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 08:51:03 +0900 Subject: [PATCH 006/123] feat(web): individual renewal and confirmation-backed profile deletion UI --- src/web/index.html | 52 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/web/index.html b/src/web/index.html index fe9a6c9..f3f1aab 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -116,6 +116,13 @@

Your apps

+
+

Provisioning profiles on iPhone

+

Old profiles may remain after renewal. Deleting a profile still needed by an installed app can stop it opening. A verified server-side backup is saved before removal.

+ +
    +
    +

    Install an app from this PC

    Choose an IPA. AltKeeper signs it with the configured Apple ID and installs it over Wi-Fi. The Apple ID password must be saved on the server.

    @@ -239,16 +246,26 @@

    Recent renewals

    $("phone-error").title = p.detail || ""; // App + const canRenew = s.account.present && s.account.password_saved && s.account.team && !s.busy; const ul = $("apps"); ul.replaceChildren(); s.apps.forEach((a) => { const pct = Math.max(4, Math.min(100, Math.round((Math.max(a.days, 0) / 7) * 100))); const bar = div("bar", span("")); bar.firstChild.style.width = pct + "%"; - ul.append(li(a.level, + const details = div("grow", div("app-name", document.createTextNode(a.label)), - div("muted", span(days(a.days), "when"), document.createTextNode(" · " + a.expires.slice(0, 10))), - bar)); + div("muted", span(days(a.days), "when"), document.createTextNode(" · " + a.expires.slice(0, 10)))); + const renewBtn = document.createElement("button"); + renewBtn.type = "button"; + renewBtn.textContent = "Renew"; + renewBtn.disabled = !canRenew; + renewBtn.onclick = () => { + if (confirm("Renew only " + a.label + " now?")) { + act(() => api("/api/renew", { mode: "single", bundle: a.bundle })); + } + }; + ul.append(li(a.level, div("row", details, renewBtn), bar)); }); const empty = $("apps-empty"); if (!s.account.present) { empty.textContent = "Sign in with your Apple ID to see your apps."; empty.hidden = false; } @@ -257,7 +274,6 @@

    Recent renewals

    else { empty.hidden = true; } // Renewal buttons - const canRenew = s.account.present && s.account.password_saved && s.account.team && !s.busy; $("btn-due").disabled = !canRenew; $("btn-all").disabled = !canRenew; $("btn-check").disabled = s.busy; @@ -265,6 +281,30 @@

    Recent renewals

    : !s.account.password_saved ? "The password must be saved to renew: sign in again and leave the checkbox selected." : s.busy ? "Working…" : ""; + // Manual, individually backed-up profile removal. Never deletes apps or Apple App IDs. + const pu = $("profiles"); + pu.replaceChildren(); + (s.profiles || []).forEach((p) => { + const details = div("grow", + div("app-name", document.createTextNode(p.label)), + div("muted", document.createTextNode(days(p.days) + " · " + p.expires.slice(0, 10))), + div("muted", document.createTextNode("Profile " + p.uuid))); + const removeBtn = document.createElement("button"); + removeBtn.type = "button"; + removeBtn.textContent = "Delete profile"; + removeBtn.disabled = s.busy || s.phone.reachable !== true; + removeBtn.onclick = () => { + if (confirm("Remove this provisioning profile for " + p.label + + "? This may prevent the app from opening. A backup will be saved first.\n\n" + p.uuid)) { + act(() => api("/api/profile/remove", { uuid: p.uuid })); + } + }; + pu.append(li("", div("row", details, removeBtn))); + }); + const pempty = $("profiles-empty"); + pempty.textContent = "No provisioning profiles found for the current Apple team."; + pempty.hidden = (s.profiles || []).length > 0 || s.phone.reachable !== true; + const canInstall = s.account.present && s.account.password_saved && s.account.team && p.reachable === true && !s.busy; $("btn-install").disabled = !canInstall || uploading; @@ -283,9 +323,11 @@

    Recent renewals

    st.className = "banner"; st.textContent = job.kind === "install" ? "Signing and installing the app. Keep this page open until it finishes…" + : job.kind === "remove_profile" + ? "Backing up and deleting the selected profile…" : "Renewal in progress. Keep this page open until it finishes…"; } - else if (job.ok) { st.className = "banner good"; st.textContent = job.kind === "install" ? "App installed." : "Done."; } + else if (job.ok) { st.className = "banner good"; st.textContent = job.kind === "install" ? "App installed." : job.kind === "remove_profile" ? "Profile removed and backed up." : "Done."; } else { st.className = "banner bad"; st.textContent = "It did not complete successfully. Check the last line."; } const log = $("job-log"); const atEnd = log.scrollTop + log.clientHeight >= log.scrollHeight - 8; From 3ae1a6390aeb7f8a6533940e8ce2f14faa204347 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 08:51:11 +0900 Subject: [PATCH 007/123] ci: skip duplicate PR image build; publish arm64 only on main or tag --- .github/workflows/docker.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8ddb0ef..a80414d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -61,7 +61,9 @@ jobs: if ($LASTEXITCODE -ne 0 -or -not ($output -match '^uso: altkeeper')) { throw "Italian CLI help failed: $output" } image: - needs: test + # PRs run Linux and Windows tests only; build/push the multi-arch image once on main. + if: github.event_name != 'pull_request' + needs: [test, test-windows] runs-on: ubuntu-latest permissions: contents: read From 71f01151e7f7f0281bc1eeafef21a490e052ca59 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 08:51:35 +0900 Subject: [PATCH 008/123] docs: explain installed-only renewals, fallback, per-app control and backed-up deletion --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 28eb3c8..e105318 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,25 @@ answers with errors, wait before trying again instead of repeating it. `renew` looks at each app's newest profile, renews the ones with `--min-days` days left or fewer (default 3), and signs in to Apple **only if something is due**. +**Installed-app safety filter (default for scheduled/CLI `renew` and web “Renew expiring apps”):** +Before checking expiry, AltKeeper queries the phone's installed user apps through installation_proxy. +Profiles that do not correspond to installed apps are skipped. **If that lookup fails (including +a timeout), AltKeeper explicitly falls back to renewing *all* known profiles for the configured +Apple team, ignoring the expiry threshold.** This fallback is logged as WARNING/FALLBACK. +A successful lookup returning zero apps skips all renewals. + +To renew a specific app, use `altkeeper renew --bundle --force` or the per-app +**Renew** button on the web page. The manual `--force` and web “Renew all now” operations +do not check installed apps; `--installed-only` can be used with CLI `--force` to opt in. +No unattended schedule is enabled by merely running the Docker container. + +The web page also lists **individual provisioning profiles** (including older versions) with a +**Delete profile** button. Every removal requires confirmation, is limited to the saved Apple team, +and saves then verifies a private `profili-salvati/.mobileprovision` backup before removal. +Removing a profile currently needed by an installed app can prevent that app from opening. +To restore a backup: `altkeeper profile-restore profili-salvati/.mobileprovision`. +Neither profile deletion nor renewal removes Apple Developer App IDs. + ### 6. Automatic renewal (optional) A safety net that does not depend on the iPhone running AltStore. @@ -502,7 +521,7 @@ If it says it cannot find the server, watch the DNS with | `pair-usb [file]` | creates the remote pairing over USB (once) | | `login ` | signs in with 2FA and saves the account | | `apps` | lists your App IDs and their expiry dates | -| `renew [--dry-run] [--force] [--min-days N] [--phone ip:port]` | renews the profiles that are due | +| `renew [--dry-run] [--force] [--min-days N] [--phone ip:port] [--bundle ID] [--installed-only]` | renews profiles, optionally one bundle or with installed-app filtering | | `altserver [--port N] [--dir folder] [--phone ip:port] [--dns-domain D --dns-address IP]` | acts as AltServer for AltStore | | `serve [--bind ip:port] [--pin PIN] [--dir folder] [--phone ip:port] [--altserver] [--altserver-port N]` | web page (and, with `--altserver`, AltServer) | | `profile-remove ` | takes one profile off the phone, saving a copy in `profili-salvati/` | From d40444e8d482670fcfe6ad8ee725d8943e5709e9 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 08:59:40 +0900 Subject: [PATCH 009/123] feat: safely prune expired profile backups on a persisted 30-day cadence --- src/profile.rs | 117 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/src/profile.rs b/src/profile.rs index b5016a1..22a14e8 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -1,7 +1,9 @@ //! Lettura dei profili di provisioning (.mobileprovision) presenti sul telefono. -use std::io::Cursor; -use std::time::SystemTime; +use std::fs; +use std::io::{self, Cursor}; +use std::path::Path; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub struct PhoneProfile { /// Per ora non usato: servirà a togliere dal telefono i profili sostituiti. @@ -88,6 +90,68 @@ pub fn parse(raw: &[u8]) -> Option { }) } +const BACKUP_KEEP_AFTER_EXPIRY: Duration = Duration::from_secs(30 * 24 * 60 * 60); +const CLEANUP_INTERVAL: Duration = Duration::from_secs(30 * 24 * 60 * 60); +const CLEANUP_STAMP: &str = "state/profile-backup-last-cleanup"; + +/// Remove only verified expired backup files matching their original UUID. +/// An unreadable or malformed backup is retained, never guessed to be expired. +/// Symlinks, subdirectories and unrelated files are not touched. +pub fn cleanup_expired_backups(dir: &Path, now: SystemTime) -> io::Result { + let entries = match fs::read_dir(dir) { + Ok(entries) => entries, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(0), + Err(e) => return Err(e), + }; + let mut removed = 0; + for entry in entries { + let entry = entry?; + if !entry.file_type()?.is_file() { continue; } // no symlink following + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) != Some("mobileprovision") { + continue; + } + let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else { + continue; + }; + let data = fs::read(&path)?; + let Some(p) = parse(&data) else { + continue; + }; + // Never delete a file whose name does not match the embedded profile UUID. + if !p.uuid.eq_ignore_ascii_case(stem) { continue; } + let Some(expiry) = p.expires else { continue; }; + let Ok(elapsed) = now.duration_since(SystemTime::from(expiry)) else { + continue; + }; + if elapsed >= BACKUP_KEEP_AFTER_EXPIRY { + fs::remove_file(&path)?; + removed += 1; + } + } + Ok(removed) +} + +/// Once per 30 days, clean backups that expired 30+ days ago. +/// Store the last successful sweep in the persistent /data/state directory. +pub fn cleanup_backups_if_due(now: SystemTime) -> io::Result> { + let stamp = Path::new(CLEANUP_STAMP); + if let Ok(raw) = fs::read_to_string(stamp) { + if let Ok(last_seconds) = raw.trim().parse::() { + if let Some(last) = UNIX_EPOCH.checked_add(Duration::from_secs(last_seconds)) { + if now.duration_since(last).is_ok_and(|elapsed| elapsed < CLEANUP_INTERVAL) { + return Ok(None); + } + } + } + } + let removed = cleanup_expired_backups(Path::new(crate::SAVED_DIR), now)?; + fs::create_dir_all("state")?; + let timestamp = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); + crate::fsperm::write_private(stamp, timestamp.to_string().as_bytes())?; + Ok(Some(removed)) +} + #[cfg(test)] mod tests { use super::*; @@ -127,6 +191,55 @@ mod tests { assert_eq!(latest_by_bundle(&all).len(), 2); } + + fn backup_xml(uuid: &str, expires: Option) -> Vec { + let mut d = plist::Dictionary::new(); + d.insert("UUID".into(), plist::Value::String(uuid.into())); + if let Some(t) = expires { + d.insert("ExpirationDate".into(), plist::Value::Date(plist::Date::from(t))); + } + let mut out = Vec::new(); + plist::to_writer_xml(&mut out, &plist::Value::Dictionary(d)).unwrap(); + out + } + + #[test] + fn cleanup_only_expired_over_30_days_after_expiration() { + let now = SystemTime::now(); + let dir = std::env::temp_dir().join(format!( + "altkeeper-profile-cleanup-{}-{:?}", std::process::id(), std::thread::current().id())); + fs::create_dir_all(&dir).unwrap(); + + let old = dir.join("old.mobileprovision"); + let recent = dir.join("recent.mobileprovision"); + let active = dir.join("active.mobileprovision"); + let corrupt = dir.join("corrupt.mobileprovision"); + let mismatch = dir.join("mismatch.mobileprovision"); + let no_expiry = dir.join("unknown.mobileprovision"); + let unrelated = dir.join("notes.txt"); + fs::write(&old, backup_xml("old", Some(now - Duration::from_secs(31 * 86400)))).unwrap(); + fs::write(&recent, backup_xml("recent", Some(now - Duration::from_secs(29 * 86400)))).unwrap(); + fs::write(&active, backup_xml("active", Some(now + Duration::from_secs(86400)))).unwrap(); + fs::write(&corrupt, b"bad content").unwrap(); + fs::write(&mismatch, backup_xml("another", Some(now - Duration::from_secs(60 * 86400)))).unwrap(); + fs::write(&no_expiry, backup_xml("unknown", None)).unwrap(); + fs::write(&unrelated, b"unrelated").unwrap(); + + assert_eq!(cleanup_expired_backups(&dir, now).unwrap(), 1); + assert!(!old.exists()); + for file in [recent, active, corrupt, mismatch, no_expiry, unrelated] { + assert!(file.exists(), "unexpected deletion: {}", file.display()); + } + fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn nonexistent_backup_folder_is_a_noop() { + let dir = std::env::temp_dir().join(format!( + "altkeeper-no-backups-{}-{:?}", std::process::id(), std::thread::current().id())); + assert_eq!(cleanup_expired_backups(&dir, SystemTime::now()).unwrap(), 0); + } + #[test] fn senza_profili_non_c_e_niente() { assert!(latest_by_bundle(&[]).is_empty()); From 949f748ed135ff939bb7ae5ef3d6097062f445a7 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 09:00:00 +0900 Subject: [PATCH 010/123] feat: run monthly backup cleanup in persistent AltKeeper web service --- src/web.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/web.rs b/src/web.rs index fe1fb63..ef8c253 100644 --- a/src/web.rs +++ b/src/web.rs @@ -227,6 +227,20 @@ pub async fn serve( "PIN-protected: any username is accepted" } else { "only accessible from this computer" }) )); + // Runs independently of iPhone communications and renewal. Check daily but + // perform a sweep only if 30 days have passed since the last successful sweep. + tokio::spawn(async { + loop { + match profile::cleanup_backups_if_due(SystemTime::now()) { + Ok(Some(removed)) => println!( + "[backup-cleanup] Monthly sweep: removed {removed} profiles expired over 30 days ago" + ), + Ok(None) => {} + Err(e) => eprintln!("[backup-cleanup] Failed: {e}"), + } + tokio::time::sleep(Duration::from_secs(24 * 60 * 60)).await; + } + }); axum::serve(listener, router).await.map_err(|e| format!("{e:?}")) } From 1f134fe1ef73bdf17c44acedb0382667183a0486 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 09:00:16 +0900 Subject: [PATCH 011/123] docs: describe safe 30-day profile backup cleanup and retention --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index e105318..cc61c31 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,16 @@ Removing a profile currently needed by an installed app can prevent that app fro To restore a backup: `altkeeper profile-restore profili-salvati/.mobileprovision`. Neither profile deletion nor renewal removes Apple Developer App IDs. +**Monthly backup cleanup:** When the web service (`serve`, including Docker `serve --altserver`) +is running, it checks once a day whether a cleanup is due. Every **30 days after the last +successful sweep** (including the first startup), it removes only saved `.mobileprovision` +files whose embedded expiry date is at least **30 days in the past**. A verified UUID/file name +match is required; malformed, unreadable, future-dated or unrelated files are preserved. +A cleanup log line reports the number removed. The last successful sweep is recorded at +`state/profile-backup-last-cleanup` so Docker restarts do not reset the schedule. +No iPhone communication, Apple login, cron job, or new systemd timer is required. +This cleanup does not touch live iPhone profiles or Apple Developer App IDs. + ### 6. Automatic renewal (optional) A safety net that does not depend on the iPhone running AltStore. From 1e8e8232a041dbe17f2740f33d2c53026aa9d4af Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 09:12:19 +0900 Subject: [PATCH 012/123] feat(web): persistent auto renewal ON/OFF and daily KST scheduler --- src/web.rs | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 1 deletion(-) diff --git a/src/web.rs b/src/web.rs index ef8c253..21cbe72 100644 --- a/src/web.rs +++ b/src/web.rs @@ -24,13 +24,16 @@ use axum::{ response::{Html, IntoResponse, Response}, routing::{get, post}, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use crate::{PAIRING_FILE, apple, phone, profile}; const INDEX_HTML: &str = include_str!("web/index.html"); const RENEW_LOG: &str = "renew.log"; +const AUTO_RENEW_CONFIG: &str = "state/auto-renew.json"; +const KST_OFFSET_SECS: u64 = 9 * 60 * 60; +const DAY_SECS: u64 = 24 * 60 * 60; const MAX_JOB_LINES: usize = 300; const MAX_IPA_BYTES: usize = 1024 * 1024 * 1024; @@ -45,6 +48,7 @@ struct App { busy: AtomicBool, inner: Mutex, tfa: Arc, + auto_renew: Mutex, } #[derive(Default)] @@ -168,6 +172,118 @@ fn now() -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) } + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct AutoRenewConfig { + enabled: bool, + /// HH:MM in Asia/Seoul (fixed UTC+09:00), independent of container TZ. + time: String, + /// KST calendar day of the most recent automatic attempt. + last_attempt_day: Option, +} + +impl Default for AutoRenewConfig { + fn default() -> Self { + Self { + enabled: false, + time: "21:17".into(), + last_attempt_day: None, + } + } +} + +fn parse_kst_time(value: &str) -> Option { + let (hh, mm) = value.split_once(':')?; + if hh.len() != 2 || mm.len() != 2 { return None; } + if !hh.bytes().chain(mm.bytes()).all(|b| b.is_ascii_digit()) { return None; } + let hour: u64 = hh.parse().ok()?; + let minute: u64 = mm.parse().ok()?; + (hour < 24 && minute < 60).then_some(hour * 3600 + minute * 60) +} + +fn kst_day(now_secs: u64) -> u64 { + now_secs.saturating_add(KST_OFFSET_SECS) / DAY_SECS +} + +fn kst_seconds_of_day(now_secs: u64) -> u64 { + now_secs.saturating_add(KST_OFFSET_SECS) % DAY_SECS +} + +fn next_auto_run(config: &AutoRenewConfig, now_secs: u64) -> Option { + if !config.enabled { return None; } + let target = parse_kst_time(&config.time)?; + let today = kst_day(now_secs); + let can_run_today = kst_seconds_of_day(now_secs) < target + && config.last_attempt_day != Some(today); + let scheduled_day = if can_run_today { today } else { today + 1 }; + Some(scheduled_day * DAY_SECS + target - KST_OFFSET_SECS) +} + +fn load_auto_renew() -> AutoRenewConfig { + let raw = match std::fs::read_to_string(AUTO_RENEW_CONFIG) { + Ok(raw) => raw, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return AutoRenewConfig::default(), + Err(e) => { + eprintln!("[auto-renew] Cannot read config ({e}); automatic renewal disabled"); + return AutoRenewConfig::default(); + } + }; + match serde_json::from_str::(&raw) { + Ok(config) if parse_kst_time(&config.time).is_some() => config, + _ => { + eprintln!("[auto-renew] Invalid schedule config; automatic renewal disabled"); + AutoRenewConfig::default() + } + } +} + +fn save_auto_renew(config: &AutoRenewConfig) -> Result<(), String> { + std::fs::create_dir_all("state").map_err(crate::phone::dbg)?; + crate::fsperm::restrict_dir(Path::new("state")).map_err(crate::phone::dbg)?; + let data = serde_json::to_vec_pretty(config).map_err(crate::phone::dbg)?; + // Atomic write: avoid resetting the persistent on/off state after an interrupted write. + let tmp = Path::new("state/auto-renew.json.tmp"); + crate::fsperm::write_private(tmp, &data).map_err(crate::phone::dbg)?; + std::fs::rename(tmp, AUTO_RENEW_CONFIG).map_err(crate::phone::dbg) +} + +async fn auto_renew_loop(app: Arc) { + loop { + let now_secs = now(); + let guard = { + let config = app.auto_renew.lock().unwrap_or_else(|e| e.into_inner()); + let due = config.enabled + && parse_kst_time(&config.time) + .is_some_and(|target| kst_seconds_of_day(now_secs) >= target) + && config.last_attempt_day != Some(kst_day(now_secs)); + if due { try_busy(&app) } else { None } + }; + if let Some(guard) = guard { + let mut config = app.auto_renew.lock().unwrap_or_else(|e| e.into_inner()); + if config.enabled && config.last_attempt_day != Some(kst_day(now_secs)) { + let mut updated = config.clone(); + updated.last_attempt_day = Some(kst_day(now_secs)); + match save_auto_renew(&updated) { + Ok(()) => { + *config = updated; + app.lock().job = JobView { + kind: "auto_renew".into(), + running: true, + lines: vec!["Automatic renewal started (Asia/Seoul schedule).".into()], + ok: None, + }; + drop(config); + println!("[auto-renew] Starting scheduled renewal."); + spawn_job(guard, |app| run_renew(app, false, None, true)); + } + Err(err) => eprintln!("[auto-renew] Could not persist attempt; skipping: {err}"), + } + } + } + tokio::time::sleep(Duration::from_secs(60)).await; + } +} + // --------------------------------------------------------------------------------------------- // Avvio // --------------------------------------------------------------------------------------------- @@ -199,12 +315,14 @@ pub async fn serve( busy: AtomicBool::new(false), inner: Mutex::new(Inner::default()), tfa: Arc::new(apple::TfaChannel::default()), + auto_renew: Mutex::new(load_auto_renew()), }); let router = Router::new() .route("/", get(index)) .route("/api/state", get(state)) .route("/api/check", post(check)) .route("/api/renew", post(renew)) + .route("/api/auto-renew", post(update_auto_renew)) .route("/api/profile/remove", post(remove_profile)) .route( "/api/install", @@ -227,6 +345,8 @@ pub async fn serve( "PIN-protected: any username is accepted" } else { "only accessible from this computer" }) )); + // Built-in scheduler; no systemd socket/host privileges or separate cron required. + tokio::spawn(auto_renew_loop(app.clone())); // Runs independently of iPhone communications and renewal. Check daily but // perform a sweep only if 30 days have passed since the last successful sweep. tokio::spawn(async { @@ -390,6 +510,8 @@ async fn state(State(app): State>) -> Json { .map(|t| parse_history(&t, 6)) .unwrap_or_default(); let g = app.lock(); + let schedule = app.auto_renew.lock().unwrap_or_else(|e| e.into_inner()).clone(); + let next_scheduled = next_auto_run(&schedule, now()); let asking = app.tfa.asking.load(Ordering::SeqCst); let login_phase = if asking { "needs_code" } else { g.login.phase }; let code_error = app.tfa.last_error.lock().ok().and_then(|e| e.clone()); @@ -398,6 +520,12 @@ async fn state(State(app): State>) -> Json { "now": now(), "busy": app.busy.load(Ordering::SeqCst), "pairing": pairing, + "auto_renew": { + "enabled": schedule.enabled, + "time": schedule.time, + "time_zone": "Asia/Seoul", + "next_run": next_scheduled, + }, "account": { "present": account.is_some(), "apple_id": account.as_ref().map(|a| a.apple_id.clone()), @@ -443,6 +571,45 @@ async fn check(State(app): State>) -> (StatusCode, Json) { json_ok() } + +#[derive(Deserialize)] +struct AutoRenewReq { + enabled: bool, + time: String, +} + +async fn update_auto_renew( + State(app): State>, + Json(req): Json, +) -> (StatusCode, Json) { + let Some(target) = parse_kst_time(&req.time) else { + return json_err(StatusCode::BAD_REQUEST, "Use a valid 24-hour time (HH:MM)."); + }; + if req.enabled { + match apple::read_account(Path::new(".")) { + Some(acc) if !acc.password.is_empty() && acc.team_id.is_some() => {} + _ => return json_err(StatusCode::BAD_REQUEST, + "Save your Apple ID password before enabling automatic renewal."), + } + } + let mut config = app.auto_renew.lock().unwrap_or_else(|e| e.into_inner()); + let mut updated = config.clone(); + // Turning on or changing the clock time after today's scheduled time + // must NOT immediately trigger an unrequested refresh. + if req.enabled && (!config.enabled || config.time != req.time) + && kst_seconds_of_day(now()) >= target + { + updated.last_attempt_day = Some(kst_day(now())); + } + updated.enabled = req.enabled; + updated.time = req.time; + if let Err(err) = save_auto_renew(&updated) { + return json_err(StatusCode::INTERNAL_SERVER_ERROR, &format!("Cannot save schedule: {err}")); + } + *config = updated; + json_ok() +} + #[derive(Deserialize)] struct RenewReq { /// "due" renews expiring installed apps; "now" all profiles; "single" one bundle. @@ -1225,6 +1392,25 @@ fn append_log(lines: &[String], ok: bool) { #[cfg(test)] mod tests { + #[test] + fn auto_renew_kst_time_validation_and_next_run() { + assert_eq!(parse_kst_time("21:17"), Some(21 * 3600 + 17 * 60)); + for invalid in ["24:00", "21:60", "9:01", "abc", "21:17:00"] { + assert_eq!(parse_kst_time(invalid), None); + } + // Day 20 in KST: scheduled for 21:17, 20:00 currently. + let day = 20_u64; + let now_secs = day * DAY_SECS + 20 * 3600 - KST_OFFSET_SECS; + let mut conf = AutoRenewConfig { enabled: true, time: "21:17".into(), last_attempt_day: None }; + assert_eq!(next_auto_run(&conf, now_secs), + Some(day * DAY_SECS + 21 * 3600 + 17 * 60 - KST_OFFSET_SECS)); + conf.last_attempt_day = Some(day); + assert_eq!(next_auto_run(&conf, now_secs), + Some((day + 1) * DAY_SECS + 21 * 3600 + 17 * 60 - KST_OFFSET_SECS)); + conf.enabled = false; + assert_eq!(next_auto_run(&conf, now_secs), None); + } + #[test] fn il_progresso_si_scrive_ogni_dieci_punti() { let mut last = -1; From 5e749ad38c65695b832c7feccee78a44aea380b5 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 09:12:39 +0900 Subject: [PATCH 013/123] fix: avoid cross-lock deadlock between automatic job and web state --- src/web.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web.rs b/src/web.rs index 21cbe72..256a082 100644 --- a/src/web.rs +++ b/src/web.rs @@ -266,13 +266,13 @@ async fn auto_renew_loop(app: Arc) { match save_auto_renew(&updated) { Ok(()) => { *config = updated; + drop(config); // avoid lock-order inversion with /api/state app.lock().job = JobView { kind: "auto_renew".into(), running: true, lines: vec!["Automatic renewal started (Asia/Seoul schedule).".into()], ok: None, }; - drop(config); println!("[auto-renew] Starting scheduled renewal."); spawn_job(guard, |app| run_renew(app, false, None, true)); } From 4081e967eefb32f33c8ef00dff35b90f3ab76752 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 09:12:57 +0900 Subject: [PATCH 014/123] feat(web ui): enable/disable and set local-time auto-renew schedule --- src/web/index.html | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/web/index.html b/src/web/index.html index f3f1aab..c0ea9bb 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -116,6 +116,22 @@

    Your apps

    +
    +

    Automatic renewal

    +

    Runs once daily while this server is online. Renews expiring installed apps. If installed-app lookup fails, it falls back to all saved-team profiles. Default is OFF.

    +
    + OFF + Automatic renewal disabled +
    + +
    + + + +
    + +
    +

    Provisioning profiles on iPhone

    Old profiles may remain after renewal. Deleting a profile still needed by an installed app can stop it opening. A verified server-side backup is saved before removal.

    @@ -281,6 +297,23 @@

    Recent renewals

    : !s.account.password_saved ? "The password must be saved to renew: sign in again and leave the checkbox selected." : s.busy ? "Working…" : ""; + // Persistent server-side automatic renewal; UI does not call host systemctl. + const auto = s.auto_renew || {enabled:false, time:"21:17", next_run:null}; + $("auto-pill").textContent = auto.enabled ? "ON" : "OFF"; + $("auto-pill").className = "pill " + (auto.enabled ? "ok" : ""); + const next = auto.next_run + ? new Date(auto.next_run * 1000).toLocaleString("ko-KR", { + timeZone:"Asia/Seoul", year:"numeric", month:"2-digit", day:"2-digit", + hour:"2-digit", minute:"2-digit", hour12:false + }) + : null; + $("auto-next").textContent = auto.enabled ? "Next: " + (next || "checking") + " KST" + : "Automatic renewal disabled"; + if (document.activeElement !== $("auto-time")) $("auto-time").value = auto.time || "21:17"; + $("auto-enable").textContent = auto.enabled ? "Save time" : "Enable auto-renew"; + $("auto-enable").disabled = !s.account.password_saved || !s.account.team; + $("auto-disable").hidden = !auto.enabled; + // Manual, individually backed-up profile removal. Never deletes apps or Apple App IDs. const pu = $("profiles"); pu.replaceChildren(); @@ -380,6 +413,14 @@

    Recent renewals

    refresh(); } +$("auto-enable").onclick = () => { + banner("auto-error", ""); + act(() => api("/api/auto-renew", {enabled:true, time:$("auto-time").value}), "auto-error"); +}; +$("auto-disable").onclick = () => { + banner("auto-error", ""); + act(() => api("/api/auto-renew", {enabled:false, time:$("auto-time").value}), "auto-error"); +}; $("btn-check").onclick = () => act(() => api("/api/check", {})); $("btn-due").onclick = () => act(() => api("/api/renew", { mode: "due" })); $("btn-all").onclick = () => { From de59d038985ce82da4986a61a336380fde5676cc Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 09:13:20 +0900 Subject: [PATCH 015/123] docs: built-in KST web auto-renew schedule and no additional systemd requirement --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cc61c31..3e62b6c 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,23 @@ A successful lookup returning zero apps skips all renewals. To renew a specific app, use `altkeeper renew --bundle --force` or the per-app **Renew** button on the web page. The manual `--force` and web “Renew all now” operations do not check installed apps; `--installed-only` can be used with CLI `--force` to opt in. -No unattended schedule is enabled by merely running the Docker container. +No unattended schedule is enabled by merely running the Docker container (default: OFF). + +**Optional built-in scheduler in the web UI:** Use **Automatic renewal** to enable/disable a +once-daily job and choose a time in **Asia/Seoul (KST, UTC+09:00)**. The default clock +time is 21:17 KST and the switch is initially OFF. When enabled, AltKeeper runs +the same installed-app-aware renewal as **Renew expiring apps**, with the documented +all-profile fallback if installed-app listing fails. The server needs a saved +Apple ID password, must be running, and the iPhone must be reachable. +Only one scheduled *attempt* is made per KST calendar day (including failures). +A server that starts after the scheduled time on a day with no attempt will run +that day's job when it starts; enabling or changing the scheduled time after +today's slot does **not** trigger an immediate refresh. The next slot is tomorrow. +The schedule and last attempted day are stored in `state/auto-renew.json` under +the persistent `/data` volume, surviving Docker restarts. No host systemd, +cron, privileged host socket, or extra package is needed. **Do not enable +an external cron/systemd renewal job at the same time** unless deliberate. + The web page also lists **individual provisioning profiles** (including older versions) with a **Delete profile** button. Every removal requires confirmation, is limited to the saved Apple team, From d9aa2cf5a662be1f7d328a1f81aa21e092263818 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 09:14:29 +0900 Subject: [PATCH 016/123] chore: set default automatic renewal schedule to 06:00 KST --- src/web.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/web.rs b/src/web.rs index 256a082..335d79c 100644 --- a/src/web.rs +++ b/src/web.rs @@ -186,7 +186,7 @@ impl Default for AutoRenewConfig { fn default() -> Self { Self { enabled: false, - time: "21:17".into(), + time: "06:00".into(), last_attempt_day: None, } } @@ -1394,19 +1394,19 @@ fn append_log(lines: &[String], ok: bool) { mod tests { #[test] fn auto_renew_kst_time_validation_and_next_run() { - assert_eq!(parse_kst_time("21:17"), Some(21 * 3600 + 17 * 60)); + assert_eq!(parse_kst_time("06:00"), Some(6 * 3600)); for invalid in ["24:00", "21:60", "9:01", "abc", "21:17:00"] { assert_eq!(parse_kst_time(invalid), None); } - // Day 20 in KST: scheduled for 21:17, 20:00 currently. + // Day 20 in KST: scheduled for 06:00, 05:00 currently. let day = 20_u64; - let now_secs = day * DAY_SECS + 20 * 3600 - KST_OFFSET_SECS; - let mut conf = AutoRenewConfig { enabled: true, time: "21:17".into(), last_attempt_day: None }; + let now_secs = day * DAY_SECS + 5 * 3600 - KST_OFFSET_SECS; + let mut conf = AutoRenewConfig { enabled: true, time: "06:00".into(), last_attempt_day: None }; assert_eq!(next_auto_run(&conf, now_secs), - Some(day * DAY_SECS + 21 * 3600 + 17 * 60 - KST_OFFSET_SECS)); + Some(day * DAY_SECS + 6 * 3600 - KST_OFFSET_SECS)); conf.last_attempt_day = Some(day); assert_eq!(next_auto_run(&conf, now_secs), - Some((day + 1) * DAY_SECS + 21 * 3600 + 17 * 60 - KST_OFFSET_SECS)); + Some((day + 1) * DAY_SECS + 6 * 3600 - KST_OFFSET_SECS)); conf.enabled = false; assert_eq!(next_auto_run(&conf, now_secs), None); } From e2125066cfe54fb97e1c1d933293f3da1b9f66bd Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 09:14:35 +0900 Subject: [PATCH 017/123] chore: set default automatic renewal schedule to 06:00 KST --- src/web/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/web/index.html b/src/web/index.html index c0ea9bb..a2a42b5 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -125,7 +125,7 @@

    Automatic renewal

    - +
    @@ -298,7 +298,7 @@

    Recent renewals

    : s.busy ? "Working…" : ""; // Persistent server-side automatic renewal; UI does not call host systemctl. - const auto = s.auto_renew || {enabled:false, time:"21:17", next_run:null}; + const auto = s.auto_renew || {enabled:false, time:"06:00", next_run:null}; $("auto-pill").textContent = auto.enabled ? "ON" : "OFF"; $("auto-pill").className = "pill " + (auto.enabled ? "ok" : ""); const next = auto.next_run @@ -309,7 +309,7 @@

    Recent renewals

    : null; $("auto-next").textContent = auto.enabled ? "Next: " + (next || "checking") + " KST" : "Automatic renewal disabled"; - if (document.activeElement !== $("auto-time")) $("auto-time").value = auto.time || "21:17"; + if (document.activeElement !== $("auto-time")) $("auto-time").value = auto.time || "06:00"; $("auto-enable").textContent = auto.enabled ? "Save time" : "Enable auto-renew"; $("auto-enable").disabled = !s.account.password_saved || !s.account.team; $("auto-disable").hidden = !auto.enabled; From 9684b231fef33b1fc1ea5803dede24d694d0b55e Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 09:14:38 +0900 Subject: [PATCH 018/123] chore: set default automatic renewal schedule to 06:00 KST --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e62b6c..093af53 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ No unattended schedule is enabled by merely running the Docker container (defaul **Optional built-in scheduler in the web UI:** Use **Automatic renewal** to enable/disable a once-daily job and choose a time in **Asia/Seoul (KST, UTC+09:00)**. The default clock -time is 21:17 KST and the switch is initially OFF. When enabled, AltKeeper runs +time is 06:00 KST and the switch is initially OFF. When enabled, AltKeeper runs the same installed-app-aware renewal as **Renew expiring apps**, with the documented all-profile fallback if installed-app listing fails. The server needs a saved Apple ID password, must be running, and the iPhone must be reachable. From 918de3464c190d82af2d262f14026e0022b2c10f Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:15:49 +0900 Subject: [PATCH 019/123] feat: identify host/extension profile groups with exact team marker --- src/profile.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/profile.rs b/src/profile.rs index 22a14e8..e693743 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -33,6 +33,25 @@ impl PhoneProfile { } } +/// Associate extension bundles with their host app only when the embedded +/// Team ID marks them as extensions of an existing, exact host bundle. +/// Never infer a parent from arbitrary dotted reverse-DNS prefixes. +pub fn group_root<'a>(bundle: &'a str, bundles: &[&'a str], team: &str) -> &'a str { + bundles.iter().copied() + .filter(|candidate| { + *candidate != bundle + && bundle.starts_with(&format!("{candidate}.{team}.")) + }) + .max_by_key(|candidate| candidate.len()) + .unwrap_or(bundle) +} + +pub fn members_of_group<'a>(root: &str, bundles: &'a [&'a str], team: &str) -> Vec<&'a str> { + bundles.iter().copied() + .filter(|b| group_root(b, bundles, team) == root) + .collect() +} + /// Per ogni bundle vale il profilo che scade più tardi: dopo un rinnovo quelli vecchi restano /// sul telefono. I profili senza bundle ID restano ciascuno per conto suo. pub fn latest_by_bundle<'a>( @@ -240,6 +259,20 @@ mod tests { assert_eq!(cleanup_expired_backups(&dir, SystemTime::now()).unwrap(), 0); } + #[test] + fn extension_groups_do_not_swallow_unrelated_apps() { + let bundles = [ + "com.google.ios.youtube", + "com.google.ios.youtube.TEAM.OpenYouTube.Extension", + "com.kdt.livecontainer", + "com.kdt.livecontainer.TEAM.LiveProcess", + "com.google.ios.youtube.another.app", + ]; + assert_eq!(group_root(bundles[1], &bundles, "TEAM"), bundles[0]); + assert_eq!(group_root(bundles[3], &bundles, "TEAM"), bundles[2]); + assert_eq!(group_root(bundles[4], &bundles, "TEAM"), bundles[4]); + assert_eq!(members_of_group(bundles[2], &bundles, "TEAM").len(), 2); + } #[test] fn senza_profili_non_c_e_niente() { assert!(latest_by_bundle(&[]).is_empty()); From c863f9bd8f46ea173701a89ccab23790b734c429 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:16:07 +0900 Subject: [PATCH 020/123] fix: renew host and extensions as one app, including installed-only and due checks --- src/main.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 74e049b..4cf7332 100644 --- a/src/main.rs +++ b/src/main.rs @@ -256,6 +256,21 @@ async fn cmd_renew( // scade più tardi, e si rinnova una volta sola (altrimenti ogni giro rifarebbe il rinnovo // finché il profilo vecchio non scade). let latest = profile::latest_by_bundle(mine); + let bundles: Vec<&str> = latest.iter().filter_map(|p| p.bundle_id.as_deref()).collect(); + // Use the shortest remaining lifetime in each app/extension group, so an + // expiring extension triggers renewal for all related profiles. + let group_days: std::collections::HashMap<&str, i64> = { + let mut result = std::collections::HashMap::new(); + for p in &latest { + if let Some(bundle) = p.bundle_id.as_deref() { + let root = profile::group_root(bundle, &bundles, &team_id); + result.entry(root).and_modify(|days: &mut i64| { + *days = (*days).min(p.days_left().unwrap_or(-1)); + }).or_insert_with(|| p.days_left().unwrap_or(-1)); + } + } + result + }; // When the installed-app lookup fails, explicitly fall back to renewing ALL // known team profiles. This is intentional and must be visible in logs. @@ -290,18 +305,20 @@ async fn cmd_renew( say(&format!(" {}: bundle ID cannot be read, skipping", p.name)); continue; }; + let root = profile::group_root(bundle, &bundles, &team_id); if let Some(selected) = bundle_filter { - if selected != bundle { continue; } + if selected != root { continue; } requested_found = true; } if let Some(apps) = &installed { - if !apps.contains(bundle) { - say(&format!(" {bundle}: no installed app found, skipping")); + if !apps.contains(root) && !apps.contains(bundle) { + say(&format!(" {bundle}: host app not installed, skipping")); continue; } } let days = p.days_left().unwrap_or(-1); - if !force && !fallback_renew_all && days > min_days { + let group_left = *group_days.get(root).unwrap_or(&days); + if !force && !fallback_renew_all && group_left > min_days { say(&format!(" {bundle}: expires in {days} days, no renewal needed")); continue; } @@ -342,6 +359,7 @@ async fn cmd_renew( ) .await?; say(" installed on the phone"); + say(&format!("RENEWED {bundle}")); done += 1; } From 1650d3ea9a475bb4bf9c89e0f8577b536dba68ad Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:17:09 +0900 Subject: [PATCH 021/123] feat(web): group app and extension profiles, show earliest member expiry --- src/web.rs | 87 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/src/web.rs b/src/web.rs index 335d79c..7db3b60 100644 --- a/src/web.rs +++ b/src/web.rs @@ -78,12 +78,22 @@ struct AppView { days: i64, expires: String, level: &'static str, + members: Vec, +} + +#[derive(Clone, PartialEq, Debug)] +struct MemberView { + bundle: String, + label: String, + days: i64, + expires: String, } #[derive(Clone, PartialEq, Debug)] struct ProfileView { uuid: String, bundle: String, + root: String, label: String, days: i64, expires: String, @@ -544,9 +554,12 @@ async fn state(State(app): State>) -> Json { "apps": g.snapshot.apps.iter().map(|a| json!({ "bundle": a.bundle, "label": a.label, "days": a.days, "expires": a.expires, "level": a.level, + "members": a.members.iter().map(|m| json!({ + "bundle":m.bundle,"label":m.label,"days":m.days,"expires":m.expires + })).collect::>(), })).collect::>(), "profiles": g.snapshot.profiles.iter().map(|p| json!({ - "uuid": p.uuid, "bundle": p.bundle, "label": p.label, + "uuid": p.uuid, "bundle": p.bundle, "root": p.root, "label": p.label, "days": p.days, "expires": p.expires, })).collect::>(), "job": { @@ -1121,42 +1134,62 @@ fn level_for(days: i64) -> &'static str { } } -/// Le app del team, una riga per bundle (il profilo più recente), dalla più urgente. +/// Display one host app with all its latest extension profiles nested below. fn app_views(profiles: &[profile::PhoneProfile], team: &str) -> Vec { let mine = profiles.iter().filter(|p| p.team_id.as_deref() == Some(team)); - let mut views: Vec = profile::latest_by_bundle(mine) - .into_iter() - .filter_map(|p| { - let bundle = p.bundle_id.clone()?; - let days = p.days_left().unwrap_or(-1); - Some(AppView { - label: label_of(&bundle, team), - bundle, - days, - expires: p.expires_text(), - level: level_for(days), - }) - }) - .collect(); - views.sort_by(|a, b| a.days.cmp(&b.days).then_with(|| a.label.cmp(&b.label))); + let latest = profile::latest_by_bundle(mine); + let bundles: Vec<&str> = latest.iter().filter_map(|p| p.bundle_id.as_deref()).collect(); + let mut groups: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); + for p in latest { + let Some(bundle) = p.bundle_id.as_deref() else { continue; }; + let root = profile::group_root(bundle, &bundles, team).to_owned(); + groups.entry(root).or_default().push(MemberView { + bundle: bundle.to_owned(), + label: label_of(bundle, team), + days: p.days_left().unwrap_or(-1), + expires: p.expires_text(), + }); + } + let mut views = Vec::new(); + for (bundle, mut members) in groups { + members.sort_by(|a,b| a.bundle.cmp(&b.bundle)); + let earliest = members.iter().min_by_key(|m| m.days).unwrap(); + views.push(AppView { + label: label_of(&bundle, team), + bundle, + days: earliest.days, + expires: earliest.expires.clone(), + level: level_for(earliest.days), + members, + }); + } + views.sort_by(|a,b| a.days.cmp(&b.days).then_with(|| a.label.cmp(&b.label))); views } fn profile_views(profiles: &[profile::PhoneProfile], team: &str) -> Vec { + let bundles: Vec<&str> = profiles.iter() + .filter(|p| p.team_id.as_deref() == Some(team)) + .filter_map(|p| p.bundle_id.as_deref()) + .collect(); let mut views: Vec<_> = profiles.iter() .filter(|p| p.team_id.as_deref() == Some(team)) .filter_map(|p| { let bundle = p.bundle_id.clone()?; + let root = profile::group_root(&bundle, &bundles, team).to_owned(); Some(ProfileView { uuid: p.uuid.clone(), label: label_of(&bundle, team), bundle, + root, days: p.days_left().unwrap_or(-1), expires: p.expires_text(), }) }) .collect(); - views.sort_by(|a, b| a.label.cmp(&b.label).then_with(|| b.expires.cmp(&a.expires))); + views.sort_by(|a,b| a.root.cmp(&b.root).then_with(|| a.label.cmp(&b.label)) + .then_with(|| b.expires.cmp(&a.expires))); views } @@ -1513,6 +1546,24 @@ mod tests { assert_eq!(v[0].days, 6); assert_eq!(v[1].label, "com.b"); assert!(v.iter().all(|a| a.level == "ok")); + assert_eq!(v[0].members.len(), 1); + } + + #[test] + fn hosts_group_extensions_and_take_earliest_expiration() { + let p = vec![ + profile("a", "TEAM", "com.kdt.livecontainer", 6), + profile("b", "TEAM", "com.kdt.livecontainer.TEAM.LiveProcess", 1), + profile("c", "TEAM", "com.kdt.livecontainer.TEAM.ShareExtension", 5), + profile("d", "TEAM", "com.google.ios.youtube", 6), + ]; + let apps = app_views(&p, "TEAM"); + assert_eq!(apps.len(), 2); + let live = apps.iter().find(|a| a.bundle == "com.kdt.livecontainer").unwrap(); + assert_eq!(live.members.len(), 3); + assert_eq!(live.days, 1); + let profiles = profile_views(&p, "TEAM"); + assert!(profiles.iter().filter(|p| p.root == "com.kdt.livecontainer").count() == 3); } #[test] From 8b8f70988594486ac6b790f8dc2b93ad6f52c0ba Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:17:55 +0900 Subject: [PATCH 022/123] feat: persisted half-hour retry schedule and protected Discord failure webhook delivery --- src/web.rs | 112 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 16 deletions(-) diff --git a/src/web.rs b/src/web.rs index 7db3b60..41433c9 100644 --- a/src/web.rs +++ b/src/web.rs @@ -32,6 +32,10 @@ use crate::{PAIRING_FILE, apple, phone, profile}; const INDEX_HTML: &str = include_str!("web/index.html"); const RENEW_LOG: &str = "renew.log"; const AUTO_RENEW_CONFIG: &str = "state/auto-renew.json"; +const DISCORD_WEBHOOK_FILE: &str = "state/discord-webhook"; +const APP_STATUS_FILE: &str = "state/app-renew-status.json"; +const RETRY_SECONDS: u64 = 30 * 60; +const MAX_RETRIES: u8 = 2; const KST_OFFSET_SECS: u64 = 9 * 60 * 60; const DAY_SECS: u64 = 24 * 60 * 60; const MAX_JOB_LINES: usize = 300; @@ -188,16 +192,29 @@ struct AutoRenewConfig { enabled: bool, /// HH:MM in Asia/Seoul (fixed UTC+09:00), independent of container TZ. time: String, + #[serde(default = "default_min_days")] + min_days: u8, /// KST calendar day of the most recent automatic attempt. last_attempt_day: Option, + #[serde(default)] + retry_count: u8, + #[serde(default)] + retry_at: Option, + #[serde(default)] + last_error: Option, } +fn default_min_days() -> u8 { 3 } impl Default for AutoRenewConfig { fn default() -> Self { Self { enabled: false, time: "06:00".into(), + min_days: 3, last_attempt_day: None, + retry_count: 0, + retry_at: None, + last_error: None, } } } @@ -221,6 +238,7 @@ fn kst_seconds_of_day(now_secs: u64) -> u64 { fn next_auto_run(config: &AutoRenewConfig, now_secs: u64) -> Option { if !config.enabled { return None; } + if let Some(retry_at) = config.retry_at { return Some(retry_at); } let target = parse_kst_time(&config.time)?; let today = kst_day(now_secs); let can_run_today = kst_seconds_of_day(now_secs) < target @@ -239,7 +257,7 @@ fn load_auto_renew() -> AutoRenewConfig { } }; match serde_json::from_str::(&raw) { - Ok(config) if parse_kst_time(&config.time).is_some() => config, + Ok(config) if parse_kst_time(&config.time).is_some() && (1..=6).contains(&config.min_days) => config, _ => { eprintln!("[auto-renew] Invalid schedule config; automatic renewal disabled"); AutoRenewConfig::default() @@ -257,40 +275,102 @@ fn save_auto_renew(config: &AutoRenewConfig) -> Result<(), String> { std::fs::rename(tmp, AUTO_RENEW_CONFIG).map_err(crate::phone::dbg) } +fn retryable_renew_error(err: &str) -> bool { + let lower = err.to_ascii_lowercase(); + !["2fa", "incorrect password", "invalid password", "invalid credentials", + "unauthorized", "forbidden", "rate.limit", "too many requests", "429", + "apple id sign-in", "apple id login"].iter().any(|part| lower.contains(part)) +} + +fn validate_discord_webhook(url: &str) -> bool { + let prefixes = ["https://discord.com/api/webhooks/", "https://discordapp.com/api/webhooks/"]; + let Some(rest) = prefixes.iter().find_map(|prefix| url.strip_prefix(prefix)) else { + return false; + }; + let mut parts = rest.split('/'); + let Some(id) = parts.next() else { return false }; + let Some(token) = parts.next() else { return false }; + parts.next().is_none() && id.len() >= 10 && token.len() >= 20 + && id.bytes().all(|b| b.is_ascii_digit()) + && token.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') +} + +fn discord_configured() -> bool { + std::fs::read_to_string(DISCORD_WEBHOOK_FILE) + .ok().is_some_and(|url| validate_discord_webhook(url.trim())) +} + +fn send_discord_failure(error: &str) -> Result<(), String> { + use std::io::Write; + let url = std::fs::read_to_string(DISCORD_WEBHOOK_FILE).map_err(crate::phone::dbg)?; + let url = url.trim(); + if !validate_discord_webhook(url) { return Err("Invalid Discord webhook configuration".into()); } + // Pass the sensitive webhook URL via stdin (curl config), not process arguments. + let msg = format!("AltKeeper: automatic renewal failed after retries.\\n{}", + error.chars().take(240).collect::()); + let body = json!({ "content": msg }).to_string(); + let mut child = std::process::Command::new("curl") + .args(["--fail", "--silent", "--show-error", "--max-time", "12", + "--request", "POST", "--header", "Content-Type: application/json", + "--data-raw", &body, "--config", "-"]) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .spawn().map_err(crate::phone::dbg)?; + // Validated webhook has no quote/backslash/newline, so it is safe for curl config. + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(format!("url = \\"{url}\\"\\n").as_bytes()).map_err(crate::phone::dbg)?; + } + let output = child.wait_with_output().map_err(crate::phone::dbg)?; + if output.status.success() { Ok(()) } + else { Err("Discord rejected webhook notification".into()) } +} + async fn auto_renew_loop(app: Arc) { loop { let now_secs = now(); let guard = { let config = app.auto_renew.lock().unwrap_or_else(|e| e.into_inner()); - let due = config.enabled - && parse_kst_time(&config.time) - .is_some_and(|target| kst_seconds_of_day(now_secs) >= target) + let retry_due = config.retry_at.is_some_and(|at| at <= now_secs); + let scheduled_due = parse_kst_time(&config.time) + .is_some_and(|target| kst_seconds_of_day(now_secs) >= target) && config.last_attempt_day != Some(kst_day(now_secs)); - if due { try_busy(&app) } else { None } + if config.enabled && (retry_due || scheduled_due) { try_busy(&app) } else { None } }; if let Some(guard) = guard { let mut config = app.auto_renew.lock().unwrap_or_else(|e| e.into_inner()); - if config.enabled && config.last_attempt_day != Some(kst_day(now_secs)) { - let mut updated = config.clone(); - updated.last_attempt_day = Some(kst_day(now_secs)); - match save_auto_renew(&updated) { - Ok(()) => { + if config.enabled { + let is_retry = config.retry_at.is_some_and(|t| t <= now_secs); + let is_new_day = config.last_attempt_day != Some(kst_day(now_secs)) + && config.retry_at.is_none(); + if is_retry || is_new_day { + let mut updated = config.clone(); + if is_retry { + updated.retry_count = updated.retry_count.saturating_add(1); + } else { + updated.last_attempt_day = Some(kst_day(now_secs)); + updated.retry_count = 0; + } + updated.retry_at = None; + if let Err(err) = save_auto_renew(&updated) { + eprintln!("[auto-renew] Could not persist attempt: {err}"); + } else { + let threshold = updated.min_days as i64; + let attempt = updated.retry_count + 1; *config = updated; - drop(config); // avoid lock-order inversion with /api/state + drop(config); app.lock().job = JobView { kind: "auto_renew".into(), running: true, - lines: vec!["Automatic renewal started (Asia/Seoul schedule).".into()], + lines: vec![format!("Scheduled renewal attempt {attempt}/3 (Asia/Seoul).")], ok: None, }; - println!("[auto-renew] Starting scheduled renewal."); - spawn_job(guard, |app| run_renew(app, false, None, true)); + spawn_job(guard, move |app| run_renew(app, false, None, true, threshold, true)); } - Err(err) => eprintln!("[auto-renew] Could not persist attempt; skipping: {err}"), } } } - tokio::time::sleep(Duration::from_secs(60)).await; + tokio::time::sleep(Duration::from_secs(30)).await; } } From e5784b989fca02987dc03fcc353a1b6b23bc48ab Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:18:54 +0900 Subject: [PATCH 023/123] =?UTF-8?q?feat:=20configurable=201=E2=80=936=20da?= =?UTF-8?q?y=20threshold,=2030m=20retry=20twice=20and=20per-group=20outcom?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/web.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 9 deletions(-) diff --git a/src/web.rs b/src/web.rs index 41433c9..7fe05df 100644 --- a/src/web.rs +++ b/src/web.rs @@ -319,7 +319,7 @@ fn send_discord_failure(error: &str) -> Result<(), String> { .spawn().map_err(crate::phone::dbg)?; // Validated webhook has no quote/backslash/newline, so it is safe for curl config. if let Some(mut stdin) = child.stdin.take() { - stdin.write_all(format!("url = \\"{url}\\"\\n").as_bytes()).map_err(crate::phone::dbg)?; + stdin.write_all(format!("url = \"{url}\"\n").as_bytes()).map_err(crate::phone::dbg)?; } let output = child.wait_with_output().map_err(crate::phone::dbg)?; if output.status.success() { Ok(()) } @@ -613,6 +613,11 @@ async fn state(State(app): State>) -> Json { "auto_renew": { "enabled": schedule.enabled, "time": schedule.time, + "min_days": schedule.min_days, + "retry_count": schedule.retry_count, + "retry_at": schedule.retry_at, + "last_error": schedule.last_error, + "discord_configured": discord_configured(), "time_zone": "Asia/Seoul", "next_run": next_scheduled, }, @@ -652,6 +657,9 @@ async fn state(State(app): State>) -> Json { "code_error": if asking { code_error } else { None }, }, "history": history, + "app_status": std::fs::read_to_string(APP_STATUS_FILE).ok() + .and_then(|s| serde_json::from_str::(&s).ok()) + .unwrap_or(json!({})), })) } @@ -669,6 +677,8 @@ async fn check(State(app): State>) -> (StatusCode, Json) { struct AutoRenewReq { enabled: bool, time: String, + #[serde(default = "default_min_days")] + min_days: u8, } async fn update_auto_renew( @@ -678,6 +688,9 @@ async fn update_auto_renew( let Some(target) = parse_kst_time(&req.time) else { return json_err(StatusCode::BAD_REQUEST, "Use a valid 24-hour time (HH:MM)."); }; + if !(1..=6).contains(&req.min_days) { + return json_err(StatusCode::BAD_REQUEST, "Renewal threshold must be 1-6 days."); + } if req.enabled { match apple::read_account(Path::new(".")) { Some(acc) if !acc.password.is_empty() && acc.team_id.is_some() => {} @@ -696,6 +709,11 @@ async fn update_auto_renew( } updated.enabled = req.enabled; updated.time = req.time; + updated.min_days = req.min_days; + if !req.enabled { + updated.retry_at = None; + updated.retry_count = 0; + } if let Err(err) = save_auto_renew(&updated) { return json_err(StatusCode::INTERNAL_SERVER_ERROR, &format!("Cannot save schedule: {err}")); } @@ -750,7 +768,8 @@ async fn renew( ok: None, }; } - spawn_job(guard, move |app| run_renew(app, force, bundle, installed_only)); + let threshold = app.auto_renew.lock().unwrap_or_else(|e| e.into_inner()).min_days as i64; + spawn_job(guard, move |app| run_renew(app, force, bundle, installed_only, threshold, false)); json_ok() } @@ -1033,13 +1052,16 @@ async fn run_check(app: Arc) { } } -async fn run_renew(app: Arc, force: bool, bundle: Option, installed_only: bool) { +async fn run_renew( + app: Arc, force: bool, bundle: Option, installed_only: bool, + min_days: i64, automatic: bool, +) { let mut say = |s: &str| app.push_line(&english_job_line(s)); - let result = crate::cmd_renew(app.phone, false, force, 3, bundle.as_deref(), installed_only, &mut say).await; - + let result = crate::cmd_renew(app.phone, false, force, min_days, bundle.as_deref(), installed_only, &mut say).await; let ok = result.is_ok(); - if let Err(e) = &result { - app.push_line(&format!("Error: {}", short_error(e))); + let error = result.as_ref().err().map(|err| short_error(err)); + if let Some(err) = &error { + app.push_line(&format!("Error: {err}")); } let lines = { let mut g = app.lock(); @@ -1048,10 +1070,77 @@ async fn run_renew(app: Arc, force: bool, bundle: Option, installed g.job.lines.clone() }; append_log(&lines, ok); - // Poi si aggiorna lo stato mostrato in pagina. + save_app_renew_status(&app, &lines, ok, bundle.as_deref()); + + if automatic { + let mut config = app.auto_renew.lock().unwrap_or_else(|e| e.into_inner()); + let mut updated = config.clone(); + let mut notify = false; + if ok { + updated.retry_count = 0; + updated.retry_at = None; + updated.last_error = None; + } else { + let err = error.as_deref().unwrap_or("Unknown renewal error"); + updated.last_error = Some(err.to_owned()); + if updated.enabled && updated.retry_count < MAX_RETRIES + && retryable_renew_error(err) + { + updated.retry_at = Some(now() + RETRY_SECONDS); + app.push_line(&format!( + "Automatic retry {}/{} in 30 minutes.", + updated.retry_count + 1, MAX_RETRIES + )); + } else { + updated.retry_at = None; + notify = true; + } + } + if let Err(e) = save_auto_renew(&updated) { + eprintln!("[auto-renew] Could not persist retry state: {e}"); + } else { + *config = updated; + } + drop(config); + if notify && discord_configured() { + let err = error.as_deref().unwrap_or("Unknown renewal error"); + match send_discord_failure(err) { + Ok(()) => app.push_line("Discord failure notification sent."), + Err(e) => app.push_line(&format!("Discord notification failed: {e}")), + } + } + } run_check(app).await; } +fn save_app_renew_status(app: &App, lines: &[String], ok: bool, selected: Option<&str>) { + let renewed: std::collections::HashSet<&str> = lines.iter() + .filter_map(|line| line.strip_prefix("RENEWED ")).collect(); + let attempted: std::collections::HashSet<&str> = lines.iter() + .filter_map(|line| line.trim().split_once(": expires in ")) + .filter(|(_, tail)| tail.contains("downloading a new profile")) + .map(|(bundle,_)| bundle).collect(); + let groups = app.lock().snapshot.apps.clone(); + let mut existing = std::fs::read_to_string(APP_STATUS_FILE).ok() + .and_then(|s| serde_json::from_str::>(&s).ok()) + .unwrap_or_default(); + for group in groups { + if selected.is_some_and(|s| s != group.bundle) { continue; } + let in_group = group.members.iter().filter(|m| renewed.contains(m.bundle.as_str())).count(); + let any_attempted = group.members.iter().any(|m| attempted.contains(m.bundle.as_str())); + if in_group == 0 && !any_attempted && selected.is_none() { continue; } + let status = if ok && in_group == group.members.len() { "success" } + else if in_group > 0 { "partial" } else { "failed" }; + existing.insert(group.bundle, json!({"status":status,"at":now(),"renewed":in_group, + "total":group.members.len()})); + } + if std::fs::create_dir_all("state").is_ok() { + if let Ok(bytes) = serde_json::to_vec(&existing) { + let _ = crate::fsperm::write_private(Path::new(APP_STATUS_FILE), &bytes); + } + } +} + async fn run_remove_profile(app: Arc, uuid: String) { let result: Result<(), String> = async { let team = apple::read_account(Path::new(".")) @@ -1514,7 +1603,7 @@ mod tests { // Day 20 in KST: scheduled for 06:00, 05:00 currently. let day = 20_u64; let now_secs = day * DAY_SECS + 5 * 3600 - KST_OFFSET_SECS; - let mut conf = AutoRenewConfig { enabled: true, time: "06:00".into(), last_attempt_day: None }; + let mut conf = AutoRenewConfig { enabled: true, time: "06:00".into(), last_attempt_day: None, ..AutoRenewConfig::default() }; assert_eq!(next_auto_run(&conf, now_secs), Some(day * DAY_SECS + 6 * 3600 - KST_OFFSET_SECS)); conf.last_attempt_day = Some(day); From 219c82608f60775f85316b938c35d4c409032118 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:19:32 +0900 Subject: [PATCH 024/123] feat: secured Discord webhook settings and web backup restore --- src/web.rs | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/web.rs b/src/web.rs index 7fe05df..548e354 100644 --- a/src/web.rs +++ b/src/web.rs @@ -413,7 +413,9 @@ pub async fn serve( .route("/api/check", post(check)) .route("/api/renew", post(renew)) .route("/api/auto-renew", post(update_auto_renew)) + .route("/api/discord", post(update_discord)) .route("/api/profile/remove", post(remove_profile)) + .route("/api/profile/restore", post(restore_profile)) .route( "/api/install", post(install).layer(DefaultBodyLimit::max(MAX_IPA_BYTES + 1024 * 1024)), @@ -657,6 +659,8 @@ async fn state(State(app): State>) -> Json { "code_error": if asking { code_error } else { None }, }, "history": history, + "backups": account.as_ref().and_then(|a| a.team_id.as_deref()) + .map(backup_views).unwrap_or_default(), "app_status": std::fs::read_to_string(APP_STATUS_FILE).ok() .and_then(|s| serde_json::from_str::(&s).ok()) .unwrap_or(json!({})), @@ -721,6 +725,117 @@ async fn update_auto_renew( json_ok() } + +fn backup_views(team: &str) -> Vec { + let Ok(entries) = std::fs::read_dir(crate::SAVED_DIR) else { return Vec::new(); }; + let mut result = Vec::new(); + for entry in entries.flatten() { + if !entry.file_type().is_ok_and(|ft| ft.is_file()) { continue; } + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) != Some("mobileprovision") { continue; } + let Ok(bytes) = std::fs::read(&path) else { continue; }; + let Some(p) = profile::parse(&bytes) else { continue; }; + if p.team_id.as_deref() != Some(team) + || path.file_stem().and_then(|s| s.to_str()) + .is_none_or(|stem| !stem.eq_ignore_ascii_case(&p.uuid)) { continue; } + result.push(json!({ + "uuid": p.uuid, "bundle": p.bundle_id, "expires":p.expires_text(), + "days": p.days_left() + })); + } + result.sort_by(|a,b| a["bundle"].as_str().cmp(&b["bundle"].as_str())); + result +} + +#[derive(Deserialize)] +struct DiscordReq { + #[serde(default)] + webhook: Option, + #[serde(default)] + clear: bool, +} + +async fn update_discord( + Json(req): Json, +) -> (StatusCode, Json) { + if req.clear { + match std::fs::remove_file(DISCORD_WEBHOOK_FILE) { + Ok(()) => return json_ok(), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return json_ok(), + Err(_) => return json_err(StatusCode::INTERNAL_SERVER_ERROR, "Could not clear webhook"), + } + } + let Some(webhook) = req.webhook else { + return json_err(StatusCode::BAD_REQUEST, "Supply a Discord webhook URL."); + }; + if !validate_discord_webhook(&webhook) { + return json_err(StatusCode::BAD_REQUEST, "Expected a valid Discord webhook URL."); + } + if std::fs::create_dir_all("state").is_err() + || crate::fsperm::restrict_dir(Path::new("state")).is_err() + || crate::fsperm::write_private(Path::new(DISCORD_WEBHOOK_FILE), webhook.as_bytes()).is_err() + { + return json_err(StatusCode::INTERNAL_SERVER_ERROR, "Could not save webhook"); + } + json_ok() +} + +#[derive(Deserialize)] +struct RestoreReq { + uuid: String, +} + +async fn restore_profile( + State(app): State>, + Json(req): Json, +) -> (StatusCode, Json) { + let team = apple::read_account(Path::new(".")).and_then(|a| a.team_id); + let Some(team) = team else { + return json_err(StatusCode::BAD_REQUEST, "Sign in before restoring profiles."); + }; + if !backup_views(&team).iter().any(|p| p["uuid"].as_str() == Some(&req.uuid)) { + return json_err(StatusCode::NOT_FOUND, "Verified profile backup was not found."); + } + let Some(guard) = try_busy(&app) else { + return json_err(StatusCode::CONFLICT, BUSY_MSG); + }; + app.lock().job = JobView { + kind:"restore_profile".into(), running:true, lines:Vec::new(), ok:None + }; + spawn_job(guard, move |app| run_restore_profile(app, req.uuid, team)); + json_ok() +} + +async fn run_restore_profile(app: Arc, uuid: String, team: String) { + let result: Result = async { + // Only original verified UUID-named files for the configured Apple team. + if uuid.len() > 64 || uuid.is_empty() + || !uuid.bytes().all(|b| b.is_ascii_hexdigit() || b == b'-') { + return Err("Invalid profile UUID".into()); + } + let path = Path::new(crate::SAVED_DIR).join(format!("{uuid}.mobileprovision")); + let bytes = std::fs::read(path).map_err(crate::phone::dbg)?; + let p = profile::parse(&bytes).ok_or("Invalid backup data")?; + if !p.uuid.eq_ignore_ascii_case(&uuid) || p.team_id.as_deref() != Some(&team) { + return Err("Backup does not belong to the configured Apple team".into()); + } + let (_, mut link) = phone::connect_auto(app.phone, PAIRING_FILE, |_: &str| {}).await?; + phone::with_timeout("Profile restore", Duration::from_secs(60), + link.install_profile(bytes)).await?; + Ok(format!("Restored profile {}. Original backup retained.", uuid)) + }.await; + let ok = result.is_ok(); + match result { + Ok(line) => app.push_line(&line), + Err(e) => app.push_line(&format!("Error: {}", short_error(&e))), + } + { + let mut g=app.lock(); + g.job.running=false; g.job.ok=Some(ok); + } + run_check(app).await; +} + #[derive(Deserialize)] struct RenewReq { /// "due" renews expiring installed apps; "now" all profiles; "single" one bundle. From 755e4018ba9cddbf33a0a6cafef3e1bb5d4b3d81 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:20:45 +0900 Subject: [PATCH 025/123] feat(ui): expandable grouped apps, renew preview, profile restore, retry threshold and Discord settings --- src/web/index.html | 230 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 191 insertions(+), 39 deletions(-) diff --git a/src/web/index.html b/src/web/index.html index a2a42b5..b310aaa 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -35,6 +35,15 @@ } .card h2 { font-size: 1.05rem; margin: 0 0 10px; } .row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } + .item-head { display: flex; align-items: flex-start; gap: 10px; flex-wrap: nowrap; } + .item-info { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; } + .item-head button { flex: 0 0 auto; white-space: nowrap; } + .nested { padding: 9px 12px; border-radius: 10px; background: var(--bg); margin-top: 9px; } + .nested > div + div { border-top: 1px solid var(--line); padding-top: 8px; margin-top: 8px; } + .mini { font-size: .82rem; padding: 7px 10px; } + select, input[type=time] { font: inherit; padding: 9px; border: 1px solid var(--line); + border-radius: 10px; background: var(--bg); color: var(--text); } + @media(max-width:420px){ .item-head { gap: 6px; } .item-head button { padding: 8px; font-size: .84rem; } } .grow { flex: 1 1 auto; } .pill { display: inline-block; padding: 3px 11px; border-radius: 999px; font-weight: 600; @@ -108,8 +117,10 @@

    Your apps

    +

    + -
    - +
    + + +
    +
    + + +
    +
    +

    If a scheduled renewal fails: retry after 30 minutes, up to twice more. Send Discord notification only after the final failure (or an unretryable authentication error).

    +
    +
    + +
    + Not configured + Only sent when automatic renewal fails after retries. +
    + +
    + + +
    +
    @@ -139,6 +175,13 @@

    Provisioning profiles on iPhone

      +
      +

      Saved profile backups

      +

      Restore a backed-up profile by UUID. Restoring does not extend its original expiry date. The backup is retained.

      +

      +
        +
        +

        Install an app from this PC

        Choose an IPA. AltKeeper signs it with the configured Apple ID and installs it over Wi-Fi. The Apple ID password must be saved on the server.

        @@ -199,6 +242,7 @@

        Recent renewals

        "use strict"; const $ = (id) => document.getElementById(id); let state = null, showForm = false, timer = null, sending = false, uploading = false; +let expandedApps = new Set(), expandedProfiles = new Set(), showPreview = false; async function api(path, body) { const opt = body === undefined ? {} : { @@ -261,27 +305,54 @@

        Recent renewals

        banner("phone-error", err, "bad"); $("phone-error").title = p.detail || ""; - // App + // One app per host bundle. Members are separate signing profiles. const canRenew = s.account.present && s.account.password_saved && s.account.team && !s.busy; const ul = $("apps"); ul.replaceChildren(); s.apps.forEach((a) => { + const members = a.members || [{bundle:a.bundle,label:a.label,days:a.days,expires:a.expires}]; const pct = Math.max(4, Math.min(100, Math.round((Math.max(a.days, 0) / 7) * 100))); const bar = div("bar", span("")); bar.firstChild.style.width = pct + "%"; - const details = div("grow", + const details = div("item-info", div("app-name", document.createTextNode(a.label)), - div("muted", span(days(a.days), "when"), document.createTextNode(" · " + a.expires.slice(0, 10)))); + div("muted", span(days(a.days), "when"), + document.createTextNode(" · " + (a.expires || "").slice(0, 10) + + " · " + members.length + " profiles"))); + const last = (s.app_status || {})[a.bundle]; + if (last) { + const date = new Date(last.at * 1000).toLocaleString("ko-KR", {timeZone:"Asia/Seoul"}); + details.append(div("muted", document.createTextNode("Last profile install: " + + last.status + " · " + date + " KST (" + last.renewed + "/" + last.total + ")"))); + } const renewBtn = document.createElement("button"); renewBtn.type = "button"; - renewBtn.textContent = "Renew"; + renewBtn.className = "mini"; + renewBtn.textContent = "Renew app"; renewBtn.disabled = !canRenew; renewBtn.onclick = () => { - if (confirm("Renew only " + a.label + " now?")) { - act(() => api("/api/renew", { mode: "single", bundle: a.bundle })); + if (confirm("Renew " + a.label + " and all " + members.length + " profiles now?")) { + act(() => api("/api/renew", {mode:"single",bundle:a.bundle})); } }; - ul.append(li(a.level, div("row", details, renewBtn), bar)); + const expandBtn = document.createElement("button"); + expandBtn.type = "button"; expandBtn.className = "mini"; + expandBtn.textContent = expandedApps.has(a.bundle) ? "Less ▲" : "Details ▼"; + expandBtn.onclick = () => { + if (expandedApps.has(a.bundle)) expandedApps.delete(a.bundle); + else expandedApps.add(a.bundle); + render(state); + }; + const item = li(a.level, + div("item-head", details, renewBtn, expandBtn), bar); + if (expandedApps.has(a.bundle)) { + const nested = div("nested"); + members.forEach(m => nested.append(div("", + div("app-name", document.createTextNode(m.label)), + div("muted", document.createTextNode(days(m.days) + " · " + m.bundle))))); + item.append(nested); + } + ul.append(item); }); const empty = $("apps-empty"); if (!s.account.present) { empty.textContent = "Sign in with your Apple ID to see your apps."; empty.hidden = false; } @@ -289,54 +360,115 @@

        Recent renewals

        else if (!s.apps.length && p.reachable === true) { empty.textContent = "No apps found for this Apple ID."; empty.hidden = false; } else { empty.hidden = true; } - // Renewal buttons $("btn-due").disabled = !canRenew; $("btn-all").disabled = !canRenew; $("btn-check").disabled = s.busy; $("renew-hint").textContent = !s.account.present ? "Sign in with your Apple ID first." - : !s.account.password_saved ? "The password must be saved to renew: sign in again and leave the checkbox selected." + : !s.account.password_saved ? "Save the Apple ID password to renew." : s.busy ? "Working…" : ""; + show("renew-preview", showPreview); + if (showPreview) { + const box = $("renew-preview"); + box.replaceChildren(div("app-name", document.createTextNode( + "Potential renewals (expires in " + (s.auto_renew.min_days || 3) + " days or less):"))); + const due = s.apps.filter(a => a.days <= (s.auto_renew.min_days || 3)); + if (!due.length) box.append(div("muted", document.createTextNode("No profiles appear due."))); + due.forEach(a => box.append(div("muted", + document.createTextNode(a.label + " · " + a.members.length + " profiles · " + days(a.days))))); + box.append(div("muted", document.createTextNode( + "Preview only: final renewal also verifies installed apps on the iPhone. " + + "If installed-app lookup fails, all known team profiles are renewed."))); + } - // Persistent server-side automatic renewal; UI does not call host systemctl. - const auto = s.auto_renew || {enabled:false, time:"06:00", next_run:null}; + // Persistent schedule and retry details + const auto = s.auto_renew || {enabled:false,time:"06:00",min_days:3,next_run:null}; $("auto-pill").textContent = auto.enabled ? "ON" : "OFF"; $("auto-pill").className = "pill " + (auto.enabled ? "ok" : ""); const next = auto.next_run ? new Date(auto.next_run * 1000).toLocaleString("ko-KR", { timeZone:"Asia/Seoul", year:"numeric", month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit", hour12:false - }) - : null; - $("auto-next").textContent = auto.enabled ? "Next: " + (next || "checking") + " KST" - : "Automatic renewal disabled"; + }) : null; + $("auto-next").textContent = auto.enabled ? (auto.retry_at ? "Retry: " : "Next: ") + + (next || "checking") + " KST" : "Automatic renewal disabled"; if (document.activeElement !== $("auto-time")) $("auto-time").value = auto.time || "06:00"; - $("auto-enable").textContent = auto.enabled ? "Save time" : "Enable auto-renew"; + if (document.activeElement !== $("auto-days")) $("auto-days").value = String(auto.min_days || 3); + $("auto-enable").textContent = auto.enabled ? "Save settings" : "Enable auto-renew"; $("auto-enable").disabled = !s.account.password_saved || !s.account.team; $("auto-disable").hidden = !auto.enabled; + $("discord-pill").textContent = auto.discord_configured ? "Configured" : "Not configured"; + $("discord-pill").className = "pill" + (auto.discord_configured ? " ok" : ""); + $("discord-clear").disabled = !auto.discord_configured; - // Manual, individually backed-up profile removal. Never deletes apps or Apple App IDs. + // Show provisioning profiles only when expanded, without moving controls when labels wrap. const pu = $("profiles"); pu.replaceChildren(); - (s.profiles || []).forEach((p) => { - const details = div("grow", - div("app-name", document.createTextNode(p.label)), - div("muted", document.createTextNode(days(p.days) + " · " + p.expires.slice(0, 10))), - div("muted", document.createTextNode("Profile " + p.uuid))); - const removeBtn = document.createElement("button"); - removeBtn.type = "button"; - removeBtn.textContent = "Delete profile"; - removeBtn.disabled = s.busy || s.phone.reachable !== true; - removeBtn.onclick = () => { - if (confirm("Remove this provisioning profile for " + p.label + - "? This may prevent the app from opening. A backup will be saved first.\n\n" + p.uuid)) { - act(() => api("/api/profile/remove", { uuid: p.uuid })); - } + const grouped = new Map(); + (s.profiles || []).forEach(profile => { + const root = profile.root || profile.bundle; + if (!grouped.has(root)) grouped.set(root, []); + grouped.get(root).push(profile); + }); + grouped.forEach((profiles, root) => { + const label = (s.apps.find(a => a.bundle === root) || {}).label || root; + const details = div("item-info", div("app-name", document.createTextNode(label)), + div("muted", document.createTextNode(profiles.length + " saved-on-phone profiles"))); + const toggle = document.createElement("button"); + toggle.type = "button"; toggle.className = "mini"; + toggle.textContent = expandedProfiles.has(root) ? "Hide ▲" : "Show ▼"; + toggle.onclick = () => { + if (expandedProfiles.has(root)) expandedProfiles.delete(root); + else expandedProfiles.add(root); + render(state); }; - pu.append(li("", div("row", details, removeBtn))); + const item = li("", div("item-head", details, toggle)); + if (expandedProfiles.has(root)) { + const nested = div("nested"); + profiles.forEach(prof => { + const info = div("item-info", + div("app-name", document.createTextNode(prof.label)), + div("muted", document.createTextNode(days(prof.days) + " · " + prof.expires.slice(0,10))), + div("muted", document.createTextNode("Profile " + prof.uuid))); + const remove = document.createElement("button"); + remove.type = "button"; remove.className = "mini"; + remove.textContent = "Delete profile"; + remove.disabled = s.busy || s.phone.reachable !== true; + remove.onclick = () => { + if (confirm("Delete profile " + prof.uuid + "? An original verified backup will be saved first. " + + "Removing an active profile may prevent the app from opening.")) { + act(() => api("/api/profile/remove", {uuid:prof.uuid})); + } + }; + nested.append(div("item-head", info, remove)); + }); + item.append(nested); + } + pu.append(item); }); const pempty = $("profiles-empty"); - pempty.textContent = "No provisioning profiles found for the current Apple team."; - pempty.hidden = (s.profiles || []).length > 0 || s.phone.reachable !== true; + pempty.textContent = "No provisioning profiles found for this Apple team."; + pempty.hidden = grouped.size > 0 || s.phone.reachable !== true; + + const backups = $("backups"); + backups.replaceChildren(); + (s.backups || []).forEach(b => { + const info = div("item-info", + div("app-name", document.createTextNode(b.bundle || "Unknown bundle")), + div("muted", document.createTextNode("Profile " + b.uuid)), + div("muted", document.createTextNode("Expires " + b.expires.slice(0,10)))); + const restore = document.createElement("button"); + restore.type = "button"; restore.className = "mini"; + restore.textContent = "Restore"; + restore.disabled = s.busy; + restore.onclick = () => { + if (confirm("Restore profile " + b.uuid + " to the iPhone? This does not renew the expiry.")) { + act(() => api("/api/profile/restore", {uuid:b.uuid})); + } + }; + backups.append(li("", div("item-head", info, restore))); + }); + $("backups-empty").hidden = (s.backups || []).length > 0; + $("backups-empty").textContent = "No saved profile backups for this Apple team."; const canInstall = s.account.present && s.account.password_saved && s.account.team && p.reachable === true && !s.busy; @@ -358,9 +490,10 @@

        Recent renewals

        ? "Signing and installing the app. Keep this page open until it finishes…" : job.kind === "remove_profile" ? "Backing up and deleting the selected profile…" + : job.kind === "restore_profile" ? "Restoring the selected profile…" : "Renewal in progress. Keep this page open until it finishes…"; } - else if (job.ok) { st.className = "banner good"; st.textContent = job.kind === "install" ? "App installed." : job.kind === "remove_profile" ? "Profile removed and backed up." : "Done."; } + else if (job.ok) { st.className = "banner good"; st.textContent = job.kind === "install" ? "App installed." : job.kind === "remove_profile" ? "Profile removed and backed up." : job.kind === "restore_profile" ? "Profile restored." : "Done."; } else { st.className = "banner bad"; st.textContent = "It did not complete successfully. Check the last line."; } const log = $("job-log"); const atEnd = log.scrollTop + log.clientHeight >= log.scrollHeight - 8; @@ -415,12 +548,31 @@

        Recent renewals

        $("auto-enable").onclick = () => { banner("auto-error", ""); - act(() => api("/api/auto-renew", {enabled:true, time:$("auto-time").value}), "auto-error"); + act(() => api("/api/auto-renew", { + enabled:true, time:$("auto-time").value, min_days:Number($("auto-days").value) + }), "auto-error"); }; $("auto-disable").onclick = () => { banner("auto-error", ""); - act(() => api("/api/auto-renew", {enabled:false, time:$("auto-time").value}), "auto-error"); + act(() => api("/api/auto-renew", { + enabled:false, time:$("auto-time").value, min_days:Number($("auto-days").value) + }), "auto-error"); +}; +$("discord-save").onclick = () => { + const url = $("discord-webhook").value.trim(); + banner("discord-error", ""); + if (!url) return banner("discord-error", "Enter the Discord webhook URL.", "bad"); + act(async () => { + await api("/api/discord", {webhook:url}); + $("discord-webhook").value = ""; + }, "discord-error"); +}; +$("discord-clear").onclick = () => { + if (confirm("Remove the saved Discord webhook?")) { + act(() => api("/api/discord", {clear:true}), "discord-error"); + } }; +$("btn-preview").onclick = () => {showPreview = !showPreview; if(state)render(state);}; $("btn-check").onclick = () => act(() => api("/api/check", {})); $("btn-due").onclick = () => act(() => api("/api/renew", { mode: "due" })); $("btn-all").onclick = () => { From 40b3d37f6275ca4d5aa84f85047310da16d52df0 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:21:12 +0900 Subject: [PATCH 026/123] fix: borrow latest profiles while grouping extension bundles --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 4cf7332..dad8cb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -300,7 +300,7 @@ async fn cmd_renew( // accede affatto (meno login, meno rischio che Apple limiti le richieste). let mut due: Vec<(&str, i64)> = Vec::new(); let mut requested_found = false; - for p in latest { + for p in &latest { let Some(bundle) = p.bundle_id.as_deref() else { say(&format!(" {}: bundle ID cannot be read, skipping", p.name)); continue; From a41655232a7defc22d38e9e02aa188a27113557d Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:21:21 +0900 Subject: [PATCH 027/123] fix: borrow latest profiles while grouping extension bundles --- src/web.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web.rs b/src/web.rs index 548e354..8d24e68 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1425,7 +1425,7 @@ fn app_views(profiles: &[profile::PhoneProfile], team: &str) -> Vec { let bundles: Vec<&str> = latest.iter().filter_map(|p| p.bundle_id.as_deref()).collect(); let mut groups: std::collections::BTreeMap> = std::collections::BTreeMap::new(); - for p in latest { + for p in &latest { let Some(bundle) = p.bundle_id.as_deref() else { continue; }; let root = profile::group_root(bundle, &bundles, team).to_owned(); groups.entry(root).or_default().push(MemberView { From 3c75baadaef7a9a9a402e35e532ac726dfac1d8a Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:21:24 +0900 Subject: [PATCH 028/123] build: install curl for Discord notifications --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9b5fd76..36fa38e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN cargo build --release --locked FROM debian:bookworm-slim RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates tzdata \ + && apt-get install -y --no-install-recommends ca-certificates tzdata curl \ && rm -rf /var/lib/apt/lists/* COPY --from=build /src/target/release/altkeeper /usr/local/bin/altkeeper WORKDIR /data From 6dbddb51e4baeb7e2853d77c119804384eead365 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 10:21:43 +0900 Subject: [PATCH 029/123] docs: grouped renewal, adjustable threshold, 30-minute retries and Discord failure alerts --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 093af53..2aa31b2 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,34 @@ Removing a profile currently needed by an installed app can prevent that app fro To restore a backup: `altkeeper profile-restore profili-salvati/.mobileprovision`. Neither profile deletion nor renewal removes Apple Developer App IDs. +**Grouped app management and retry controls:** The web UI groups extension profiles with +their host app when their bundle ID contains the saved team's extension marker +(e.g. `com.kdt.livecontainer.TEAM.LiveProcess` under `com.kdt.livecontainer`). +The expandable **Your apps** card renews *every* member profile in a group, rather +than only the selected host profile. Installed-app filtering also checks the +host app for an extension; the earliest-expiring member determines when the entire +group is due. The web preview lists groups that appear due, but the actual run +performs a fresh phone check and may fall back to all team profiles if the +installed-app inventory fails. + +The web scheduler's **renewal threshold** is adjustable from **1 to 6 days**, +default **3 days**. On automatic renewal failure it retries up to **two** more +times, **30 minutes apart** (up to 3 attempts in total). Authentication and +rate-limit failures are not automatically retried. Enabling Discord failure +notifications requires saving a webhook URL in the web UI. The secret is saved in +the private `state/discord-webhook` file and is never returned to the browser; +only a boolean indicates configuration. The Docker image includes `curl` to send +an alert only after the last failed attempt (or a non-retryable failure). +Successful automatic renewals do not send Discord messages. + +The web UI also lists previously saved profile backups and offers an explicit +**Restore** action after confirmation. Restoration verifies the saved profile +UUID and Apple Team ID and does not change its expiry date. The original file +is retained. The app summary shows the last known per-group profile-install +result from `state/app-renew-status.json`, and **Preview renewals** provides a +non-authoritative overview without signing in to Apple. + + **Monthly backup cleanup:** When the web service (`serve`, including Docker `serve --altserver`) is running, it checks once a day whether a cleanup is due. Every **30 days after the last successful sweep** (including the first startup), it removes only saved `.mobileprovision` From 52b2ef1706db9ae45d61ed656edf5e37283c9d17 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 11:22:48 +0900 Subject: [PATCH 030/123] fix: group extensions using embedded signing Team IDs, not only active profile team --- src/profile.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/src/profile.rs b/src/profile.rs index e693743..c1d0859 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -33,16 +33,30 @@ impl PhoneProfile { } } -/// Associate extension bundles with their host app only when the embedded -/// Team ID marks them as extensions of an existing, exact host bundle. -/// Never infer a parent from arbitrary dotted reverse-DNS prefixes. +/// AltStore and other signers may insert their *original* ten-character +/// Team ID into an extension bundle, even when the provisioned profile +/// belongs to a different (current) developer team. Match only a complete +/// parent + team-marker + nonempty extension suffix, not arbitrary prefixes. +fn extension_of(bundle: &str, parent: &str, team: &str) -> bool { + let Some(suffix) = bundle.strip_prefix(parent).and_then(|s| s.strip_prefix('.')) else { + return false; + }; + let Some((marker, extension_name)) = suffix.split_once('.') else { + return false; + }; + !extension_name.is_empty() && ( + marker == team || + (marker.len() == 10 && + marker.bytes().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())) + ) +} + +/// Resolve an extension to an *existing* host bundle: the longest valid +/// parent wins to avoid matching adjacent/unrelated dotted app identifiers. pub fn group_root<'a>(bundle: &'a str, bundles: &[&'a str], team: &str) -> &'a str { bundles.iter().copied() - .filter(|candidate| { - *candidate != bundle - && bundle.starts_with(&format!("{candidate}.{team}.")) - }) - .max_by_key(|candidate| candidate.len()) + .filter(|parent| *parent != bundle && extension_of(bundle, parent, team)) + .max_by_key(|parent| parent.len()) .unwrap_or(bundle) } @@ -273,6 +287,46 @@ mod tests { assert_eq!(group_root(bundles[4], &bundles, "TEAM"), bundles[4]); assert_eq!(members_of_group(bundles[2], &bundles, "TEAM").len(), 2); } + #[test] + fn real_altstore_extension_bundles_group_even_if_embedded_team_differs() { + // Real bundle shapes seen on iOS; the embedded marker is the app's + // original signer, which need not equal this profile's Apple Team ID. + let bundles = [ + "com.google.ios.youtube", + "com.google.ios.youtube.PXUL22799Q.OpenYouTube.Extension", + "com.kdt.livecontainer", + "com.kdt.livecontainer.PXUL22799Q.LaunchAppExtension", + "com.kdt.livecontainer.PXUL22799Q.LiveProcess", + "com.kdt.livecontainer.PXUL22799Q.ShareExtension", + ]; + let profile_team = "DIFFERENT1"; + assert_eq!(group_root(bundles[1], &bundles, profile_team), bundles[0]); + for extension in &bundles[3..] { + assert_eq!(group_root(extension, &bundles, profile_team), bundles[2]); + } + assert_eq!(members_of_group(bundles[0], &bundles, profile_team).len(), 2); + assert_eq!(members_of_group(bundles[2], &bundles, profile_team).len(), 4); + } + + #[test] + fn malformed_team_markers_do_not_group_unrelated_bundles() { + let bundles = [ + "com.example.app", + "com.example.app.feature.widget", + "com.example.app.short.Widget", + "com.example.app.PXUL22799Q", + "com.example.app.PXUL22799q.Widget", + "com.example.app.PXUL22799Q.Widget", + "com.example.app.PXUL22799Q.", // no extension name + "com.example.app.PXUL22799Q.Widget.More", + ]; + for index in [1,2,3,4,6] { + assert_eq!(group_root(bundles[index], &bundles, "SAVEDTEAM1"), bundles[index]); + } + assert_eq!(group_root(bundles[5], &bundles, "SAVEDTEAM1"), bundles[0]); + assert_eq!(group_root(bundles[7], &bundles, "SAVEDTEAM1"), bundles[0]); + } + #[test] fn senza_profili_non_c_e_niente() { assert!(latest_by_bundle(&[]).is_empty()); From 13795e2332daf9e9b90dcc8637cca97701248880 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 11:23:12 +0900 Subject: [PATCH 031/123] test(web): assert real iOS bundle names display as 2 apps and 6 member profiles --- src/web.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/web.rs b/src/web.rs index 8d24e68..9413501 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1850,6 +1850,30 @@ mod tests { assert!(profiles.iter().filter(|p| p.root == "com.kdt.livecontainer").count() == 3); } + #[test] + fn production_bundle_names_render_as_two_apps_not_six_profiles() { + let team = "SAVEDTEAM1"; + let all = vec![ + profile("a", team, "com.google.ios.youtube", 6), + profile("b", team, "com.google.ios.youtube.PXUL22799Q.OpenYouTube.Extension", 5), + profile("c", team, "com.kdt.livecontainer", 6), + profile("d", team, "com.kdt.livecontainer.PXUL22799Q.LaunchAppExtension", 6), + profile("e", team, "com.kdt.livecontainer.PXUL22799Q.LiveProcess", 2), + profile("f", team, "com.kdt.livecontainer.PXUL22799Q.ShareExtension", 6), + ]; + let apps = app_views(&all, team); + assert_eq!(apps.len(), 2); + let youtube = apps.iter().find(|a| a.bundle == "com.google.ios.youtube").unwrap(); + assert_eq!(youtube.members.len(), 2); + assert_eq!(youtube.days, 5); + let live = apps.iter().find(|a| a.bundle == "com.kdt.livecontainer").unwrap(); + assert_eq!(live.members.len(), 4); + assert_eq!(live.days, 2); + let views = profile_views(&all, team); + assert_eq!(views.iter().filter(|p| p.root == youtube.bundle).count(), 2); + assert_eq!(views.iter().filter(|p| p.root == live.bundle).count(), 4); + } + #[test] fn errori_in_parole_semplici() { assert!(friendly_error("manca il pairing remoto rp-pairing.plist: crealo").contains("missing")); From 6ddd862ab4e50d30556ab1420364618157eca2fd Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 12:03:40 +0900 Subject: [PATCH 032/123] fix: strip profile Team ID suffix only for host/extension comparisons; keep raw App IDs --- src/profile.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/profile.rs b/src/profile.rs index c1d0859..7900756 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -37,8 +37,19 @@ impl PhoneProfile { /// Team ID into an extension bundle, even when the provisioned profile /// belongs to a different (current) developer team. Match only a complete /// parent + team-marker + nonempty extension suffix, not arbitrary prefixes. +/// Apple provisioning App IDs may have a *second* Team ID suffix that +/// the UI deliberately hides (e.g. host "com.foo.PXUL22799Q" is shown as +/// "com.foo"). Keep the raw ID for Apple renewals, but ignore this final +/// suffix while comparing parent and extension relationships. +pub fn profile_app_id<'a>(bundle: &'a str, team: &str) -> &'a str { + if team.is_empty() { return bundle; } + bundle.strip_suffix(&format!(".{team}")).unwrap_or(bundle) +} + fn extension_of(bundle: &str, parent: &str, team: &str) -> bool { - let Some(suffix) = bundle.strip_prefix(parent).and_then(|s| s.strip_prefix('.')) else { + let child_id = profile_app_id(bundle, team); + let parent_id = profile_app_id(parent, team); + let Some(suffix) = child_id.strip_prefix(parent_id).and_then(|s| s.strip_prefix('.')) else { return false; }; let Some((marker, extension_name)) = suffix.split_once('.') else { @@ -308,6 +319,45 @@ mod tests { assert_eq!(members_of_group(bundles[2], &bundles, profile_team).len(), 4); } + #[test] + fn real_provisioning_app_ids_include_trailing_team_suffix() { + // The web UI strips the final ".PXUL22799Q" from each raw profile + // bundle when it displays the names seen in the user screenshot. + // Previous tests used the displayed IDs and missed this failure. + let team = "PXUL22799Q"; + let raw = [ + "com.google.ios.youtube.PXUL22799Q", + "com.google.ios.youtube.PXUL22799Q.OpenYouTube.Extension.PXUL22799Q", + "com.kdt.livecontainer.PXUL22799Q", + "com.kdt.livecontainer.PXUL22799Q.LaunchAppExtension.PXUL22799Q", + "com.kdt.livecontainer.PXUL22799Q.LiveProcess.PXUL22799Q", + "com.kdt.livecontainer.PXUL22799Q.ShareExtension.PXUL22799Q", + ]; + assert_eq!(profile_app_id(raw[0], team), "com.google.ios.youtube"); + assert_eq!( + profile_app_id(raw[1], team), + "com.google.ios.youtube.PXUL22799Q.OpenYouTube.Extension" + ); + assert_eq!(group_root(raw[1], &raw, team), raw[0]); + for ext in &raw[3..] { + assert_eq!(group_root(ext, &raw, team), raw[2]); + } + assert_eq!(members_of_group(raw[0], &raw, team).len(), 2); + assert_eq!(members_of_group(raw[2], &raw, team).len(), 4); + } + + #[test] + fn trailing_suffix_must_match_current_profile_team_exactly() { + let raw = [ + "com.example.app.AAAAAAAAAA", + "com.example.app.BBBBBBBBBB.Ext.AAAAAAAAAA", + "com.example.app.AAAAAAAAAA.other", + ]; + assert_eq!(group_root(raw[1], &raw, "AAAAAAAAAA"), raw[0]); + assert_eq!(group_root(raw[2], &raw, "AAAAAAAAAA"), raw[2]); + assert_eq!(profile_app_id(raw[0], "BBBBBBBBBB"), raw[0]); + } + #[test] fn malformed_team_markers_do_not_group_unrelated_bundles() { let bundles = [ From 66130954c90576298ac22fb4f5be2b22a18bd87c Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 12:04:13 +0900 Subject: [PATCH 033/123] test: use genuinely unrelated bundle name for negative grouping case --- src/profile.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/profile.rs b/src/profile.rs index 7900756..e4e6cab 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -351,7 +351,7 @@ mod tests { let raw = [ "com.example.app.AAAAAAAAAA", "com.example.app.BBBBBBBBBB.Ext.AAAAAAAAAA", - "com.example.app.AAAAAAAAAA.other", + "com.example.app.feature.other", ]; assert_eq!(group_root(raw[1], &raw, "AAAAAAAAAA"), raw[0]); assert_eq!(group_root(raw[2], &raw, "AAAAAAAAAA"), raw[2]); From 3fb47e7c0d1352d4f2180d742513ba8aafb05ab0 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 12:04:49 +0900 Subject: [PATCH 034/123] fix: installed-only renewal recognizes raw and normalized host app IDs --- src/main.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index dad8cb3..2253699 100644 --- a/src/main.rs +++ b/src/main.rs @@ -311,7 +311,14 @@ async fn cmd_renew( requested_found = true; } if let Some(apps) = &installed { - if !apps.contains(root) && !apps.contains(bundle) { + // InstallationProxy may report either the signed (raw) Bundle ID + // or the unsuffixed app Bundle ID. Profiles, unlike installed + // apps, may carry a trailing ".{team}" in their Apple App ID. + let host_id = profile::profile_app_id(root, &team_id); + let member_id = profile::profile_app_id(bundle, &team_id); + if !apps.contains(root) && !apps.contains(host_id) + && !apps.contains(bundle) && !apps.contains(member_id) + { say(&format!(" {bundle}: host app not installed, skipping")); continue; } From 5c1e5f6801572639405df447825ebdf599774ac6 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 12:05:19 +0900 Subject: [PATCH 035/123] test(web): regress using raw phone provisioning IDs with trailing signing Team suffix --- src/web.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/web.rs b/src/web.rs index 9413501..94a5a0f 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1405,7 +1405,7 @@ async fn run_login(app: Arc, apple_id: String, password: String, save: bool // --------------------------------------------------------------------------------------------- fn label_of(bundle: &str, team: &str) -> String { - bundle.strip_suffix(&format!(".{team}")).unwrap_or(bundle).to_string() + profile::profile_app_id(bundle, team).to_string() } fn level_for(days: i64) -> &'static str { @@ -1874,6 +1874,33 @@ mod tests { assert_eq!(views.iter().filter(|p| p.root == live.bundle).count(), 4); } + #[test] + fn real_raw_profile_ids_from_phone_display_as_two_grouped_apps() { + // Screenshots show these IDs after the UI has hidden the last + // ".PXUL22799Q". The phone's raw provisioning IDs contain that + // extra suffix; testing display-only names missed the regression. + let team = "PXUL22799Q"; + let all = vec![ + profile("a", team, "com.google.ios.youtube.PXUL22799Q", 6), + profile("b", team, "com.google.ios.youtube.PXUL22799Q.OpenYouTube.Extension.PXUL22799Q", 6), + profile("c", team, "com.kdt.livecontainer.PXUL22799Q", 6), + profile("d", team, "com.kdt.livecontainer.PXUL22799Q.LaunchAppExtension.PXUL22799Q", 6), + profile("e", team, "com.kdt.livecontainer.PXUL22799Q.LiveProcess.PXUL22799Q", 6), + profile("f", team, "com.kdt.livecontainer.PXUL22799Q.ShareExtension.PXUL22799Q", 6), + ]; + let apps = app_views(&all, team); + assert_eq!(apps.len(), 2, "UI must show two apps, not six profiles"); + let youtube = apps.iter().find(|a| a.label == "com.google.ios.youtube").unwrap(); + assert_eq!(youtube.members.len(), 2); + assert_eq!(youtube.bundle, "com.google.ios.youtube.PXUL22799Q"); + let live = apps.iter().find(|a| a.label == "com.kdt.livecontainer").unwrap(); + assert_eq!(live.members.len(), 4); + assert_eq!(live.bundle, "com.kdt.livecontainer.PXUL22799Q"); + let views = profile_views(&all, team); + assert_eq!(views.iter().filter(|p| p.root == youtube.bundle).count(), 2); + assert_eq!(views.iter().filter(|p| p.root == live.bundle).count(), 4); + } + #[test] fn errori_in_parole_semplici() { assert!(friendly_error("manca il pairing remoto rp-pairing.plist: crealo").contains("missing")); From ffc0ac0d4b181a8bbb00e917a9416770d7e6d2f4 Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 13:10:37 +0900 Subject: [PATCH 036/123] feat(web): add Apple touch and Safari bookmark PNG icon assets --- src/web/apple-touch-icon.png | Bin 0 -> 129853 bytes src/web/favicon-32.png | Bin 0 -> 4196 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/web/apple-touch-icon.png create mode 100644 src/web/favicon-32.png diff --git a/src/web/apple-touch-icon.png b/src/web/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..797038691e6265434a00ef986773da8e97f5da86 GIT binary patch literal 129853 zcmeI5TaX>adB>Nk#Fe7T2qaY=@?eR|b0jLyUQtQmN+={MmEcQQnQ*nk3J^CFN!Z2) zj26cMC)fy**gz82j*0OFsVZ^oR3#GRC14^~1;~pLI6x#Jt)$)64Kn%uGpA>Idb)eQ z?&+B`=bWCZ_MDsLz5Mp?-+cXb&x5~r`R{!2++RC)YHI3(m;UZ0zfb4g7{BrX`p@YX zKk%DV=bZheOE3AYE4H08l_d1qdAFZVD&w|O$$53-R{BgnPSRt;*m^QqQ5nnhnlf&o z=i0GEuMft}bgmtX$%>npW6OzTMctUC&kV--v&R$TxRKr~jbrp&Gya%f(~bpteK0;v zXX`jh@6-4cbKGzwIbRwV(BsOuo?c7GyL7gU>*&4GI84tq<63&HG7iye(zu2mTgO3q zpT^btv65adjX(O|qzd$H1b7|Hc)x3y0d?0f19mT%0k2r0-%!L`L zT}tK-VCDiH5@w_}2k0O$19o#dmr@xiO(2U=PH3lOw!?g0MKb`nq8WIu)eKjdk={(0 zp(743BcY9&!G#%VtMrFgroy`}4b>1vs&XglyI!D!rs!^2eTwu0e2-bPC8_;>Z+3w9j zF9d7r&9SPXomAntGbfoxsH!2u%uDDT=%^Y|HI#{zAT{SOsv*Fhvd#+TFx%5Pszy|e zVXHy;yiIjTlF?HzkH zibkX$-fT~2m#T&UIXz#6ZacMX$AmemMpTV9yxCVZRuaq@7NDbQ^iehDUpSp?{0cQp zaY}S!Jb_Y5W^~)7K)<5^%}nQ5)sWJ;!(NRwTNe@>2M)iVtl3)hIydTZ=GL5ht{PRK znJ`DykYMh(sxkM&lPS>nk6k}Lm8Y~8=rNx_$rE~@8Y{1Qhf-Qj=p5*%8hum^%nU9p z{a+3=9&G%|@v&cll7lNX@t0m3s74v+9OhWn=xbGD_KBqu%y=Tyqms}e(|JTxqX3!- zb5xDtsRq&+@ahhzM+xYVF!!h}w3N#EMIbH)Gu061skqpcT0g~V?n=G6A8nziYS@mA ze=4a52j~%O3)KT%h8a+^fR0rSDV;S{jp_SOBpV-EOlF^4PUfCFm5xV$ayr@j+F9*D zs?I%kG99x|E~RC->HFx9$I$bPf5&zpz1QAyl-J`tW>pQ5%;G!%x}yuh5pN4!clU{8 zX4g`(bN8tfRFekS;H!HN|0V!NMPugC#T53s{|?^Y_0J~;c}XHO*Co;jK9fAhVrI1Ki` zlc@)mR~ew@06k(=4FR$^lX4gmdV==VSaZkdLahEZWa^;-ngjC)tA-uukTCaHHKrd} zOm#wqV$`2k)T0s1!&&Ugt!fC6)AJ|ODn~V;$6Yn9d~7+{``@Fd6x=_wR6TB%)Pn=` zh^vMf=#c5$v#JI_Zlf~61+B}^wO|jtxd8g@%%R6cHIT|}9RTg=7tW+p-|Kcz-vwuveePb)gXL6Mg`)O6 zhxmRD9HL&2o2fP}oY|PnuHM|>Jrdz(?ubG4w=qFQjHJYd7_nzKFk(w z|IWz-%|~5qOfIiT2?4VFZ)EBrrL;DkM^`lfa|(2`be3)zF(lQ1VuW;M{9EXh%OIvM zyR^Gb+GU@+m$RXBuF_E_Ep}}~H}=2rUb6R9QllR%Cu?sSq_h@h*L3ceY8Zfaxn+dw zZ6oMb5Zv2FY~kHD;>N!z*4W8uc1DGYGo z%CiO!Tv{~~$K`P)?&y8Rn57khBc7b_#s_~&a7aN!4;KNvJ22y(6wuN+*)&|n+tX=j2HwWl3SB(ZhOX=L9YJ`9pvxaEe&tc}KbBn40 zperDUhIs;1Lju|*oh6t%RE?%EBb8^UF{uPH7v>gK!w54MXihR4VID8lNP*r=b?C~? zFpW3D9G+@4nfXJTXeE_H!|bUV+GOU!991K&JBwsqNibtL04=3+hpN%!0uYN?E2#{a zxj;wN7~GG$g_^Ung_m(gurwQp7SU`1rE=B+ZA@ng<_=ZE7iK61dY`i`y`(J&Twso> z(H_kCMlscWHA-MR!dy;hBh1554c|!Q(yN=XL~YN zmBR?Ml+GQg#)o%S_i+<-TH6_}z2**7F_cP%Nam;-L%#<~)^t?e!mE_bXg=~t=OUpw zFn6RHsAwQ%U5^ZAY9h+F^J+PrJ$f}_RioILyaKT3plbkg2DB08;i!hs;?>S!j;hf+ z)hLw12z2+V;nUB|f;pvVdcr(-lo5U2FVQ}Y=)7w!s8l-dh zs)k7Bm7Cw9Iy499+6%!jZJ|h7m!n+Guv`d+ggL541DIVe1bI~r2GCME*TPJn<@Pw= zWbSeHQH`R`Y=Jqd#>A+GA)zIhJ5mj7HQ-v+$YuZq$l@HU8pVAB(WfzfCuHr85ROw3N=ZFo#hMq_fMCeRjQhh^jGt@3Cb2-!CV- ze|je2EO^Cd_Wbm8vi;jj$@CW&iuHnxn)b(Mv1{HQss%c$Ip^s-fY}3Rx)4P9jWY;Z zfU6s>>AdY3^filWhB&u;A;?gTb=!}nw_u01t2Gn2oY%3(M!Wmb^><$l zdorU`rvNR%TqJZY%;8i6I~}<26B748-MRKcFeJ=MRin=^i=SZzwUp4;&|~W`m+8hqH30Uing5}Q6=On&O=r}S zxyPgba#{kqTh)MOjLI?d2;JrC+B%T&z8U6mQH>g)!@}&U8^tyR-S%qW0MV8$&YxCd?nQRs#yom)~3l;-+S&EWo=#OL-O zIGe29LNkg+sLkm-PO4!CI#5*uFt39aq zpc)|#?5;okMem3C8dF3g_0h5Dv5z{~VP+&aS2w6Ca!Nao6!W&kn0?F;q3SCG;zz&rw~ApzZ< zYMeiNJaO5gVBKf2X^f&9A-!2q4fHen98NHMtUzf3_4+&M4+~IJUxqrySvAT)x1btG zX8_IJt1k0r1!itKdu~kbTiGZy1L5h-WI9V#4RJSD?$;)Rg(X($tt74ws~Sbak=Rq?u-Ap+fkW?AlGz*P5mF5| zpljMf(|WT8W&q7SDz}da3A3Ua0NS@ymsK+uFmuy6@0=?@7MsMZ|I88YkFWH(4WFm4 zPfF=YPz^_*^=kujkR_Pqbaq*tQn?`H0PU$702&KgU61+aPY)^@?Wu-!0LIbcEKxPD$2ViYtTeyN!Eq`;8^~?@8nP^jI@ofkuBvb8qg)3KRgH!;A)`!Hl5kTmv*Cod?OR zrZa%vxtl-xCw251RnZi%i&H2^qj$&+%-?>)%_+AE#=r5b=)19Y>hh5$J|)8>kN<hV4>s+jD@5B>VM_Jr&7$PqF^K0f(1CrU8PFauGa?#5Pr?KuB4?73Fd&Rkpg|gk(khOQjeKxxB;!{J}O-Wim5_nt9>refWB0tq1Y@97UwBz!?UMqi1s<`9K$_kc2QGwOR5op9)H!S2fBf3 z2%vH19&^vp?XEb<%=Km@_`sdTd7>TK%bWXO)es8xA?3e}K-=B`HF{W9$| z!vWeuH2^eLw+;hntX$pq%IS3Aao0y)haS_ah5%Wd^+0oAhCf$;R+es4LZh9IbEKs^ zE6|j<+;mowIbEvVG0=c`(*v~n^{Tf@1Fzw|u8tIFuXG-bs)hhLJ%0i{q-q^HkZOpO zb}8Mh+d+RMTBsdh)~H4SwEOBP^9`{Hu3{5~cP>n8uM*6Donx1!&)EUqn!mQlhRX{Ax zZZOkALK#}y;}qyOm4ptc8e%GY-QE0cfS!JV?nhAp+990{s?jCTI?XTvjnB4yxC1?! zss1^z}D)A!NFczH?_&;eAVE1+F;LxA13c36Qffefe{ zz&vEtCxo4{33A8`Vs9d;cM_Q9M0qp^E*Ff9U zqYKpl%sA$rJyAGx3Fz@sje4L1sfOI+>{9K(F9Uaz*QPWFXh}81%}}uKZC6L{fG)x8 zU)9+B@R4NSf1OS)zn`{dGAKu0rZY~O$a~}ZO`r&X)$^2AIVvzWQ;lMxWFf)yHup$Z zgR3dcftfxdE(8V4X+lpb(1Bn^?HMqO<7cm*Nwz#r`+i7ZyTR<Fr874`5~htx4z7qzS#BKQEW)WPo*#xgB@_A9d+9TdB8W0JZ4NsGKe7JgLg@AgUY< zRpY`>y+gl)_t_bQq_%d`*#MT`8v&;ngaXAR5^Rigm=Xj;Kw|Agp+xOJ5k=o*+K z(Eh5i=Rf$fy!f4Ne&}5b(Cw&32K5_*i^2-b?Wl$fG|F)+OI-y-4X!Em1 zpf{s}p&NxeyQ&7hkIU~nYzA7^3EN{2)mZiMH%L8@(3N!7z#LFD)Ro(=cfs8ID&2@r zPG}~~7hFeE-yPPW+l18R-*yAKfLX0MbN)A>CgyZ*S2fw=r%LkNVjzCM=QIpJo`N8kH%@^RJ@uIEon1Qw>viu$=oi(aq09upIAz?N&?fYy1lln9V zX6a7yF`;#eAzmxe+0~m7MyNj<(^&@A_Lu{+Qq>S3i?gI08kk#D4YfzZ^?t=j_3Q@R zl)ix0GzRG$6MB%)HR*iujzdl*I>3x6a3jnuO`wR2KmoEi+kp<5&WdWNKu_Px@0Z38 z5eJ}Nd&^N4%n@jj#Ok?7W}k~d{2r{6D7HkF-{YjSMKu(lHR&7@W`k;|Q2VF{l6~zh zXhzbcw3N;OdgGV*vt9T_jl;3&OYdf@xwXj*xHF&uGm_Tz=!_>g zMm2Jv31-X$Vv5_f>XAcDz@|y9ff+z!%#<^Y}VY(%q!+Q$x?#2~>3=`7BuG0#5X){!Rw%c>s>64Y5T zi-g8`63Tb^go;5me5x7ZXPovv8&{(YM>X_7b71D)Ho{RgWSGUIiSNz10rkw+j;D$t zgPcFUcD&W2PBl3`+Aj@Z8 zsPV%`66X*P9Qp!+S&X?K@VENKzc&hpJ(=Hjo&l7<=T2dERE=svbAZ1fw_2oD20C0gA2*r_m@1!VuGDMgALjs z2778`<3ze00k$N-Knjb2I&)r{Gr-(LF#tPYu3c7Tt7ORU1+Hocki~hbSaCwtjuP1Z zZJ~K7&g9XWi@*}Z^u9>vZ1wiL>EuZhsPT*0^U~=otxI4ZBG9FA4L!DwgY-U)tE*!r zy;d6Xt%z-pTe=4dQrWdRA3rDg=p1HG)yRN8Kvj+s#4^+p&_hxU0kfC|#Ef85sFBv& zzqL3>YYAjPZ3P+cn_&jvC7AKt2C;b03+DRO+gJnWd*LU5X2IMO)u;hlf_aFlp#m+y zo`1givR6K`qEq!#<|CH+*5itM^0v0@X7dd)JSalMVOK zg=q?R3oz&D?2^odH(z}B;bhl8E(QL+8GC66&A{2F7xH@@sfH$@JzyS!YKU}}`!&!- zhkA49WB-A($@cjZ$%fAp+(WKvY#@kt{N3^7XY@6NKA@-lP0Lk{i*7%ZZ2s!e1QRA9F7ix&1Zh7r zK<2{Sv#N$j<`n3U<`;z$%ss4XwR&NC-cnT~^XF=ha|(57O1tQ)LyrdP&69fI zqEFp%-wo#et40;*km(!}W~EGL1ljl&ZD6k?m(N= zLVcYBGkr#P50t!`A+{u7R5fg>A*XZtSeXUfa1WKm_PK2p+vjvgi4RMwRDk8@ZZMCI zY7{_+Oy`g=2UHCm%<>fX+AW9E2^37EjM{F`71{sfYn8#2xm_WC) z0>#utS4!wc>8w``8RGKeVtN~sCYT-UGu#5=i*CiL?qcU76=3yw4zxUlZG1dhs=)_Z zRSMJdkm=kAW>hy^fG%KGr?=P69!j=Q!xCl)hgCPm|6|~P8#;m3&@73I9gny`yTLqq zs=)x-A)Q0Q+@WgJH6N*Mq3iEBoM2WE%d9YQ(gx)CH*ryiC03gsKANo)RMrP(yC6x) zjFg^m)yRN;omQ-99ohkABsK@=AyzfYdV!TEVwls}rB`Fa=ia5M?<2{UzdDw$Dcto> zOWGMi%o?IoPOJ??`{$+ysB4g#kJ2r>#JWInZU}S9osD`yiJZSTnyRt-qp#CUp$%x0 zTBxscV5ZOXp{h}U44}inT{wRC7>N(j;aw=0}V!|QA+3R-huh1it;9lHH%%v z3KabXAbOv(i$EFXGSDWqP+t$3&QUcO>8#Y7V^t$<T)`5DCv?bkj;g^(=cpRibgog24D6p%FNdy7 z7ZT>E8Vs1DYFJ@rs74C(A2b0vszy`|RWIb{9jnIbD}J7=)&dvgDpIG=nd!Wkws1VZd++(?&NtsY+V~}>EmOkpanAjn z`!Vv$_AMLMKeOKF^KIzb+W9Jf52K!bioc#78p`^7{%5;7U)*_i-MX;Uq*Rd=$qxv(d{C$h$??t7_--Ak#|0vgyq^`pY)Pg$nP}ji+ z)bbW;o?6~SC8*^+OFjjz2)MwH*8o=pT;N9yIFr!ehQL84Ap{gS699M_0amWn z8|Bs1_hqO;J(L^up%eVGt=>--*nckG8k)u|Wj6@h(_t^nVyvNjU9?m)7y`L9KXWp86<-jVZUaSXr zASwi0fwS>UmTlMlE&~_!YX}^-^%j9j>xZX{s;98MBJGUzn+DUg?ZZV??eB5;*k|4& zaAfMmdZ-UX|KS$6)FWgnR-zU@IXWeSXsaK zQbGNb<2RNn)C2tH?23bpiMLP4&DmuN_jBf3Szpb1fNKI4IMk2+qyq(kQ?_W>`q;Oc zTq^fOS+AB2TfgmtG~qKmv9EJwSzpb1fCu;9;}+NiIP2qgv_gI4_^ew~FKVYb^;G{E z7~tI_X`233&&|yIinC}OaD#NxaR?ae zp(ecVLiYd0``u_ro$5%%{f_*9gi*Ne!@L-gmOO)0b9MxdWV1lw{AFCGzc7x3U~f055tuLh58mm_80aJ&e_u4D-1eqxh617_;mj?B@> zSBunk_`VuC8(b6cxLnG4$0b89m$7duTQCvN6UzOJSOlhj(X#^*$^p}RfCu-$0r$<8 zryhwXXTI;VtXi{P1l+*#XGMUszWa4_{9=S%4Z67BCM zoz@7D*LSqz{w0+c96dl1*GIQ1p{Dh5Jm}@$I&)CqdDVlQi*j zX&zv^QjXB^upZ#S-81Zf%X+VSJ=QFijJV-ty?h4MwWeMvH|kq=2?17JalOK8mAui& zj^{NC^<&@X>DnK~vKsp?OVN)O)QXi&=QZ_WJ=7ZlYkQ?~HeG{(qmP~LyMVXu`jcDW zrQW_?iF(&7mHTq(UbCKkx2%Wy)}2DYzS&l>UfyQw-Eh3(7J=io9^eiEd+OJExvbiE m6F4&UN;zAv2<*5^JHEVgW9ycQ%ly9wzOI+IcTT=^;QW6b0qc?g literal 0 HcmV?d00001 From e1ee4d1d1174338447a984ad4f374b2a4062aaac Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 13:11:43 +0900 Subject: [PATCH 037/123] feat(web): serve public Safari bookmark PNGs and safe Discord webhook test API --- src/web.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/src/web.rs b/src/web.rs index 94a5a0f..1b349c4 100644 --- a/src/web.rs +++ b/src/web.rs @@ -30,6 +30,8 @@ use serde_json::{Value, json}; use crate::{PAIRING_FILE, apple, phone, profile}; const INDEX_HTML: &str = include_str!("web/index.html"); +const APPLE_TOUCH_ICON: &[u8] = include_bytes!("web/apple-touch-icon.png"); +const FAVICON_32: &[u8] = include_bytes!("web/favicon-32.png"); const RENEW_LOG: &str = "renew.log"; const AUTO_RENEW_CONFIG: &str = "state/auto-renew.json"; const DISCORD_WEBHOOK_FILE: &str = "state/discord-webhook"; @@ -301,14 +303,18 @@ fn discord_configured() -> bool { } fn send_discord_failure(error: &str) -> Result<(), String> { + let message = format!("AltKeeper: automatic renewal failed after retries.\\n{}", + error.chars().take(240).collect::()); + send_discord_message(&message) +} + +fn send_discord_message(message: &str) -> Result<(), String> { use std::io::Write; let url = std::fs::read_to_string(DISCORD_WEBHOOK_FILE).map_err(crate::phone::dbg)?; let url = url.trim(); if !validate_discord_webhook(url) { return Err("Invalid Discord webhook configuration".into()); } // Pass the sensitive webhook URL via stdin (curl config), not process arguments. - let msg = format!("AltKeeper: automatic renewal failed after retries.\\n{}", - error.chars().take(240).collect::()); - let body = json!({ "content": msg }).to_string(); + let body = json!({ "content": message }).to_string(); let mut child = std::process::Command::new("curl") .args(["--fail", "--silent", "--show-error", "--max-time", "12", "--request", "POST", "--header", "Content-Type: application/json", @@ -409,11 +415,14 @@ pub async fn serve( }); let router = Router::new() .route("/", get(index)) + .route("/apple-touch-icon.png", get(apple_touch_icon)) + .route("/favicon-32.png", get(favicon_32)) .route("/api/state", get(state)) .route("/api/check", post(check)) .route("/api/renew", post(renew)) .route("/api/auto-renew", post(update_auto_renew)) .route("/api/discord", post(update_discord)) + .route("/api/discord/test", post(test_discord)) .route("/api/profile/remove", post(remove_profile)) .route("/api/profile/restore", post(restore_profile)) .route( @@ -461,11 +470,16 @@ pub async fn serve( // --------------------------------------------------------------------------------------------- async fn guard(State(app): State>, req: Request, next: Next) -> Response { - if app.pin.is_none() && !host_is_local(req.headers()) { + // iOS Safari can fetch favorite icons separately from the authenticated tab, + // so only these two fixed, non-sensitive PNGs are served without the PIN. + let public_icon = req.method() == Method::GET + && matches!(req.uri().path(), "/apple-touch-icon.png" | "/favicon-32.png"); + if !public_icon && app.pin.is_none() && !host_is_local(req.headers()) { return (StatusCode::FORBIDDEN, "host non ammesso").into_response(); } - if let Some(pin) = &app.pin { - if !basic_password_ok(req.headers(), pin) { + if !public_icon { + if let Some(pin) = &app.pin { + if !basic_password_ok(req.headers(), pin) { // Rallenta chi prova PIN a caso. tokio::time::sleep(Duration::from_secs(1)).await; let mut res = (StatusCode::UNAUTHORIZED, "serve il PIN").into_response(); @@ -474,6 +488,7 @@ async fn guard(State(app): State>, req: Request, next: Next) -> Respons HeaderValue::from_static("Basic realm=\"altkeeper\""), ); return res; + } } } if req.method() == Method::POST @@ -483,14 +498,16 @@ async fn guard(State(app): State>, req: Request, next: Next) -> Respons } let mut res = next.run(req).await; let h = res.headers_mut(); - h.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store")); + h.insert(header::CACHE_CONTROL, HeaderValue::from_static( + if public_icon { "public, max-age=86400" } else { "no-store" } + )); h.insert(header::X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff")); h.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY")); h.insert(header::REFERRER_POLICY, HeaderValue::from_static("no-referrer")); h.insert( header::CONTENT_SECURITY_POLICY, HeaderValue::from_static( - "default-src 'none'; connect-src 'self'; style-src 'unsafe-inline'; \ + "default-src 'none'; connect-src 'self'; img-src 'self' data:; style-src 'unsafe-inline'; \ script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'", ), ); @@ -572,6 +589,13 @@ async fn index() -> Html<&'static str> { Html(INDEX_HTML) } +fn png_response(bytes: &'static [u8]) -> Response { + ([(header::CONTENT_TYPE, "image/png")], bytes).into_response() +} + +async fn apple_touch_icon() -> Response { png_response(APPLE_TOUCH_ICON) } +async fn favicon_32() -> Response { png_response(FAVICON_32) } + fn json_err(status: StatusCode, msg: &str) -> (StatusCode, Json) { (status, Json(json!({ "error": msg }))) } @@ -780,6 +804,23 @@ async fn update_discord( json_ok() } +/// User-initiated delivery check; never creates or renews a profile. +async fn test_discord() -> (StatusCode, Json) { + if !discord_configured() { + return json_err(StatusCode::BAD_REQUEST, "Configure a Discord webhook first."); + } + // curl is synchronous; leave Tokio free while we wait for Discord. + let sent = tokio::task::spawn_blocking(|| { + send_discord_message( + "AltKeeper test notification: Discord webhook is working. No renewal was performed." + ) + }).await; + match sent { + Ok(Ok(())) => json_ok(), + _ => json_err(StatusCode::BAD_GATEWAY, "Discord test delivery failed. Check webhook permissions and network access."), + } +} + #[derive(Deserialize)] struct RestoreReq { uuid: String, @@ -1728,6 +1769,14 @@ mod tests { assert_eq!(next_auto_run(&conf, now_secs), None); } + #[test] + fn safari_icons_are_real_pngs() { + for icon in [APPLE_TOUCH_ICON, FAVICON_32] { + assert!(icon.starts_with(b"\\x89PNG\\r\\n\\x1a\\n")); + assert!(icon.len() > 100); + } + } + #[test] fn il_progresso_si_scrive_ogni_dieci_punti() { let mut last = -1; From 5287bc12b0f8dcb757e44eaaa43c6204ecb7131e Mon Sep 17 00:00:00 2001 From: pingpingjinping Date: Sun, 25 Oct 2026 13:11:57 +0900 Subject: [PATCH 038/123] feat(web): link Safari touch icon and add Discord send-test button --- src/web/index.html | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/web/index.html b/src/web/index.html index b310aaa..25c7b8a 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -5,7 +5,9 @@ AltKeeper - + + + @@ -308,10 +321,19 @@

        Recent renewals

        For use on your home network only.
        + + + From 7a6c000f6868fb60e7c15c6137a566198cd31d5e Mon Sep 17 00:00:00 2001 From: Bartolo <162994065+BartolomeoRusso9@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:29:54 +0100 Subject: [PATCH 120/123] Fix --- .github/workflows/release.yml | 101 +- Cargo.lock | 2080 +-------------------------------- README.it.md | 13 +- README.md | 15 +- src/tray.rs | 35 +- src/update.rs | 31 +- 6 files changed, 188 insertions(+), 2087 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37cc0d3..823364f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,9 @@ -# Compila altkeeper.exe per Windows e lo pubblica in una GitHub Release, con il suo SHA-256. -# L'aggiornamento dall'icona nella barra di sistema scarica proprio questi due file. +# Compila AltKeeper per Windows, Linux e macOS e pubblica tutto in una GitHub Release. +# +# - Windows: `altkeeper.exe` + `altkeeper.exe.sha256`. L'aggiornamento dall'icona nella barra di +# sistema scarica proprio questi due file: i nomi non vanno cambiati. +# - Linux (x86-64 e arm64) e macOS (Apple Silicon e Intel): `altkeeper---.tar.gz` +# con il suo `.sha256`. Sono download manuali: l'auto-aggiornamento è solo per Windows. # # Si lancia con un tag, per esempio: git tag v0.3.1 && git push origin v0.3.1 # Il numero del tag deve coincidere con `version` in Cargo.toml. @@ -10,29 +14,92 @@ on: tags: ["v*"] jobs: - windows: - runs-on: windows-latest + build: + strategy: + fail-fast: true + matrix: + include: + - name: windows + runner: windows-latest + test: true + # ubuntu-22.04: glibc 2.35, così il binario gira anche su distribuzioni non recentissime. + - name: linux-x86_64 + runner: ubuntu-22.04 + test: true + - name: linux-aarch64 + runner: ubuntu-22.04-arm + test: true + - name: macos-aarch64 + runner: macos-latest + test: false + - name: macos-x86_64 + runner: macos-15-intel + test: false + runs-on: ${{ matrix.runner }} permissions: - contents: write + contents: read + defaults: + run: + shell: bash steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Check that the tag matches Cargo.toml - shell: pwsh run: | - $version = (Select-String -Path Cargo.toml -Pattern '^version = "(.+)"').Matches[0].Groups[1].Value - if ("v$version" -ne "${{ github.ref_name }}") { throw "tag ${{ github.ref_name }} != Cargo.toml v$version" } - - run: cargo test --locked + version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1) + if [ "v$version" != "${GITHUB_REF_NAME}" ]; then + echo "tag ${GITHUB_REF_NAME} != Cargo.toml v$version"; exit 1 + fi + - name: Test + if: matrix.test + run: cargo test --locked - run: cargo build --release --locked - - name: SHA-256 - shell: pwsh + - name: Package (Windows) + if: matrix.name == 'windows' + run: | + mkdir dist + cp target/release/altkeeper.exe dist/altkeeper.exe + cd dist + hash=$(sha256sum altkeeper.exe | cut -d' ' -f1) + printf '%s altkeeper.exe' "$hash" > altkeeper.exe.sha256 + - name: Package (Linux, macOS) + if: matrix.name != 'windows' run: | - Copy-Item target/release/altkeeper.exe altkeeper.exe - $hash = (Get-FileHash altkeeper.exe -Algorithm SHA256).Hash.ToLower() - Set-Content -Path altkeeper.exe.sha256 -Value "$hash altkeeper.exe" -NoNewline + pkg="altkeeper-${GITHUB_REF_NAME}-${{ matrix.name }}" + mkdir -p "dist/$pkg" + cp target/release/altkeeper LICENSE README.md README.it.md "dist/$pkg/" + cp -r examples "dist/$pkg/examples" + cd dist + tar -czf "$pkg.tar.gz" "$pkg" + rm -rf "$pkg" + if command -v sha256sum >/dev/null; then + sha256sum "$pkg.tar.gz" > "$pkg.tar.gz.sha256" + else + shasum -a 256 "$pkg.tar.gz" > "$pkg.tar.gz.sha256" + fi + - uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.name }} + path: dist/* + if-no-files-found: error + retention-days: 1 + + publish: + # Una sola release, creata solo se tutte le build sono riuscite. + needs: [build] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + pattern: dist-* + merge-multiple: true + - name: List files + run: ls -l dist - uses: softprops/action-gh-release@v2 with: - files: | - altkeeper.exe - altkeeper.exe.sha256 + files: dist/* + fail_on_unmatched_files: true diff --git a/Cargo.lock b/Cargo.lock index 14d24f6..a041439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,39 +78,20 @@ name = "altkeeper" version = "0.3.0" dependencies = [ "axum", - "chrono", "idevice", "isideload", "mdns-sd", "plist", - "reqwest", "rpassword", "rustls", "serde", "serde_json", - "sha2 0.10.9", - "tao", - "tauri-winrt-notification", "tempfile", "tokio", "tracing-subscriber", - "tray-icon", - "windows-sys 0.61.2", - "winreg", - "winresource", - "wry", "zip", ] -[[package]] -name = "android_system_properties" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" -dependencies = [ - "libc", -] - [[package]] name = "anyhow" version = "1.0.104" @@ -136,7 +117,7 @@ dependencies = [ "sha2 0.10.9", "sha2 0.11.0", "spki 0.7.3", - "thiserror 2.0.20", + "thiserror", "x509-cert 0.2.5", ] @@ -152,7 +133,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.20", + "thiserror", "time", ] @@ -235,34 +216,11 @@ dependencies = [ "crc32fast", "futures-lite", "pin-project", - "thiserror 2.0.20", + "thiserror", "tokio", "tokio-util", ] -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -354,21 +312,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec 0.8.0", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bit-vec" version = "0.9.1" @@ -378,12 +321,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.13.2" @@ -426,15 +363,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -453,31 +381,6 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.13.2", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - [[package]] name = "cbc" version = "0.1.2" @@ -506,22 +409,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.5" @@ -569,10 +456,8 @@ version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ - "iana-time-zone", "num-traits", "serde", - "windows-link", ] [[package]] @@ -675,16 +560,6 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" -[[package]] -name = "cookie" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" -dependencies = [ - "time", - "version_check", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -711,30 +586,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" -dependencies = [ - "bitflags 2.13.2", - "core-foundation 0.10.1", - "core-graphics-types", - "foreign-types 0.5.0", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.2", - "core-foundation 0.10.1", - "libc", -] - [[package]] name = "core_detect" version = "1.0.0" @@ -774,15 +625,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.8" @@ -844,29 +686,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "cssparser" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" -dependencies = [ - "quote", - "syn 3.0.6", -] - [[package]] name = "ctr" version = "0.10.1" @@ -918,17 +737,6 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" -[[package]] -name = "dbus" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.61.2", -] - [[package]] name = "der" version = "0.7.10" @@ -997,27 +805,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.119", -] - [[package]] name = "des" version = "0.9.0" @@ -1051,39 +838,6 @@ dependencies = [ "ctutils", ] -[[package]] -name = "dirs" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.2", - "block2", - "libc", - "objc2", -] - [[package]] name = "displaydoc" version = "0.2.7" @@ -1095,71 +849,6 @@ dependencies = [ "syn 3.0.6", ] -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "dom_query" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" -dependencies = [ - "bit-set", - "cssparser", - "foldhash", - "html5ever", - "precomputed-hash", - "selectors", - "tendril", -] - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "ed25519" version = "2.2.3" @@ -1228,31 +917,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - [[package]] name = "find-msvc-tools" version = "0.1.13" @@ -1272,7 +942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide 0.9.1", + "miniz_oxide", "zlib-rs", ] @@ -1293,40 +963,13 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared 0.3.1", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", + "foreign-types-shared", ] [[package]] @@ -1335,12 +978,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1452,304 +1089,63 @@ dependencies = [ ] [[package]] -name = "gdk" -version = "0.18.2" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", + "typenum", + "version_check", ] [[package]] -name = "gdk-pixbuf" -version = "0.18.5" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", + "cfg-if", + "js-sys", "libc", - "once_cell", + "wasi", + "wasm-bindgen", ] [[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", + "cfg-if", + "js-sys", "libc", - "system-deps", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", ] [[package]] -name = "gdk-sys" -version = "0.18.2" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", + "cfg-if", "libc", - "pango-sys", - "pkg-config", - "system-deps", + "r-efi 6.0.0", + "rand_core 0.10.1", ] [[package]] -name = "gdkwayland-sys" -version = "0.18.2" +name = "ghash" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", -] - -[[package]] -name = "ghash" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ "polyval", ] -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.13.2", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "h2" version = "0.4.19" @@ -1781,18 +1177,6 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hex" version = "0.4.3" @@ -1826,16 +1210,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "html5ever" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" -dependencies = [ - "log", - "markup5ever", -] - [[package]] name = "http" version = "1.5.0" @@ -1968,30 +1342,6 @@ dependencies = [ "windows-registry", ] -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "2.3.0" @@ -2106,7 +1456,7 @@ dependencies = [ "sha1 0.10.7", "sha2 0.10.9", "siphasher", - "thiserror 2.0.20", + "thiserror", "tokio", "tokio-rustls", "tracing", @@ -2231,7 +1581,7 @@ dependencies = [ "sha1 0.11.0", "sha2 0.11.0", "srp", - "thiserror 2.0.20", + "thiserror", "tokio", "tokio-tungstenite", "tracing", @@ -2261,7 +1611,7 @@ dependencies = [ "rc2", "sha1 0.11.0", "sha2 0.11.0", - "thiserror 2.0.20", + "thiserror", "x509-parser", ] @@ -2277,29 +1627,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "jktcp" version = "0.1.7" @@ -2315,22 +1642,6 @@ dependencies = [ "wasmtimer", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - [[package]] name = "jni" version = "0.22.4" @@ -2340,10 +1651,10 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.20", + "thiserror", "walkdir", "windows-link", ] @@ -2361,15 +1672,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - [[package]] name = "jni-sys" version = "0.4.1" @@ -2409,16 +1711,6 @@ dependencies = [ "cpufeatures 0.2.17", ] -[[package]] -name = "keyboard-types" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" -dependencies = [ - "bitflags 2.13.2", - "serde", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -2428,70 +1720,18 @@ dependencies = [ "spin", ] -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading", - "once_cell", -] - [[package]] name = "libc" version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" -[[package]] -name = "libdbus-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "libm" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" -[[package]] -name = "libredox" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" -dependencies = [ - "libc", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2519,17 +1759,6 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" -[[package]] -name = "markup5ever" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - [[package]] name = "matchers" version = "0.2.0" @@ -2586,17 +1815,8 @@ dependencies = [ ] [[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" +name = "mime" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" @@ -2606,16 +1826,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - [[package]] name = "miniz_oxide" version = "0.9.1" @@ -2638,26 +1848,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "muda" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "libc", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "png", - "thiserror 2.0.20", - "windows-sys 0.61.2", -] - [[package]] name = "multer" version = "3.1.0" @@ -2719,42 +1909,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.2", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - [[package]] name = "nom" version = "7.1.3" @@ -2835,222 +1989,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "objc2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" -dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.2", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.13.2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.2", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.13.2", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.13.2", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.2", - "block2", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.13.2", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.13.2", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.13.2", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-web-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" -dependencies = [ - "bitflags 2.13.2", - "block2", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", -] - [[package]] name = "oid-registry" version = "0.8.1" @@ -3078,9 +2016,9 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.2", + "bitflags", "cfg-if", - "foreign-types 0.3.2", + "foreign-types", "libc", "openssl-macros", "openssl-sys", @@ -3115,37 +2053,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "parking" version = "2.2.1" @@ -3219,59 +2126,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project" version = "1.1.13" @@ -3394,19 +2248,6 @@ dependencies = [ "plist", ] -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", -] - [[package]] name = "poly1305" version = "0.8.0" @@ -3453,65 +2294,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.17+spec-1.1.0", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.107" @@ -3627,12 +2409,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - [[package]] name = "rayon" version = "1.12.0" @@ -3682,17 +2458,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.2", -] - -[[package]] -name = "redox_users" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" -dependencies = [ - "libredox", - "thiserror 2.0.20", + "bitflags", ] [[package]] @@ -3721,9 +2487,7 @@ dependencies = [ "base64 0.23.1", "bytes", "encoding_rs", - "futures-channel", "futures-core", - "futures-util", "h2", "http", "http-body", @@ -3766,7 +2530,7 @@ dependencies = [ "async-trait", "http", "reqwest", - "thiserror 2.0.20", + "thiserror", "tower-service", ] @@ -3878,7 +2642,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.2", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -3928,7 +2692,7 @@ checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni 0.22.4", + "jni", "log", "once_cell", "rustls", @@ -4016,7 +2780,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.2", + "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4033,25 +2797,6 @@ dependencies = [ "libc", ] -[[package]] -name = "selectors" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" -dependencies = [ - "bitflags 2.13.2", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf", - "phf_codegen", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", -] - [[package]] name = "semver" version = "1.0.28" @@ -4112,24 +2857,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" -dependencies = [ - "serde_core", -] - [[package]] name = "serdect" version = "0.4.3" @@ -4140,15 +2867,6 @@ dependencies = [ "serde", ] -[[package]] -name = "servo_arc" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" -dependencies = [ - "stable_deref_trait", -] - [[package]] name = "sha1" version = "0.10.7" @@ -4299,32 +3017,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "spin" version = "0.9.9" @@ -4372,46 +3064,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.119" @@ -4471,7 +3129,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.2", + "bitflags", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4486,87 +3144,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.37.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548" -dependencies = [ - "bitflags 2.13.2", - "block2", - "core-foundation 0.10.1", - "core-graphics", - "crossbeam-channel", - "dbus", - "dispatch2", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni 0.21.1", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "parking_lot", - "percent-encoding", - "raw-window-handle", - "tao-macros", - "unicode-segmentation", - "url", - "windows", - "windows-core", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri-winrt-notification" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36" -dependencies = [ - "thiserror 2.0.20", - "windows", - "windows-version", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -4580,42 +3157,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "tendril" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" -dependencies = [ - "new_debug_unreachable", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.20", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "thiserror-impl", ] [[package]] @@ -4778,102 +3326,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml" -version = "1.1.8+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned 1.1.2", - "toml_datetime 1.1.2+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 1.0.4", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.25.17+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" -dependencies = [ - "indexmap", - "toml_datetime 1.1.2+spec-1.1.0", - "toml_parser", - "winnow 1.0.4", -] - -[[package]] -name = "toml_parser" -version = "1.1.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" -dependencies = [ - "winnow 1.0.4", -] - -[[package]] -name = "toml_writer" -version = "1.1.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" - [[package]] name = "tower" version = "0.5.3" @@ -4896,7 +3348,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags 2.13.2", + "bitflags", "bytes", "futures-core", "futures-util", @@ -4985,27 +3437,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tray-icon" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5cc5858ecf9574ddd8f0032a2ff7ffd6f7724d2b81e48b9374984b3fd6ddc9" -dependencies = [ - "crossbeam-channel", - "dirs", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "once_cell", - "png", - "thiserror 2.0.20", - "windows-sys 0.61.2", -] - [[package]] name = "triomphe" version = "0.1.16" @@ -5033,7 +3464,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1 0.10.7", - "thiserror 2.0.20", + "thiserror", ] [[package]] @@ -5054,12 +3485,6 @@ version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - [[package]] name = "universal-hash" version = "0.5.1" @@ -5135,12 +3560,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - [[package]] name = "version_check" version = "0.9.5" @@ -5269,62 +3688,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web_atoms" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" -dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - [[package]] name = "webpki-root-certs" version = "1.0.9" @@ -5352,56 +3715,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webview2-com" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows", - "windows-core", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "webview2-com-sys" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" -dependencies = [ - "thiserror 2.0.20", - "windows", - "windows-core", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.11" @@ -5411,95 +3724,12 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - [[package]] name = "windows-registry" version = "0.6.1" @@ -5529,22 +3759,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -5556,85 +3777,34 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "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", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-version" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5647,92 +3817,30 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" -dependencies = [ - "cfg-if", - "windows-sys 0.61.2", -] - -[[package]] -name = "winresource" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b" -dependencies = [ - "toml 1.1.8+spec-1.1.0", - "version_check", -] - [[package]] name = "wit-bindgen" version = "0.57.1" @@ -5745,70 +3853,6 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" -[[package]] -name = "wry" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9" -dependencies = [ - "base64 0.23.1", - "block2", - "cookie", - "crossbeam-channel", - "dirs", - "dom_query", - "dpi", - "dunce", - "gdkx11", - "gtk", - "http", - "javascriptcore-rs", - "jni 0.21.1", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "percent-encoding", - "raw-window-handle", - "sha2 0.10.9", - "soup3", - "tao-macros", - "thiserror 2.0.20", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows", - "windows-core", - "windows-version", - "x11-dl", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - [[package]] name = "x25519-dalek" version = "2.0.1" @@ -5860,7 +3904,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.20", + "thiserror", "time", ] @@ -5870,7 +3914,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" dependencies = [ - "bit-vec 0.9.1", + "bit-vec", "time", ] diff --git a/README.it.md b/README.it.md index 4a48ca7..503c5b8 100644 --- a/README.it.md +++ b/README.it.md @@ -106,6 +106,15 @@ i JSON o la pagina web. I comandi del menu 2FA restano `d`, `r` (se disponibile) ### 1. Ottieni il programma +**Già compilato:** ogni [release su GitHub](https://github.com/BartolomeoRusso9/altkeeper/releases) ha +`altkeeper--linux-x86_64.tar.gz`, `-linux-aarch64`, `-macos-aarch64` (Apple Silicon) e +`-macos-x86_64` (Intel), ciascuno con il suo file `.sha256` (verifica con `sha256sum -c`). Decomprimi e +usa il binario `altkeeper`. Le build Linux richiedono glibc 2.35 o più recente; su sistemi più vecchi +o Alpine usa Docker o compila da te. Le build macOS non sono firmate: se Gatekeeper le blocca, lancia +`xattr -d com.apple.quarantine altkeeper`. Si aggiorna da solo solo l'`.exe` di Windows. + +**Oppure compilalo tu:** + ```sh git clone https://github.com/BartolomeoRusso9/altkeeper cd altkeeper @@ -358,9 +367,9 @@ end-to-end. recenti. `--console` tiene la console, `--dir` sceglie la cartella dei dati (di default quella dell'`.exe`). - L'icona controlla anche, una volta al giorno, le release GitHub del fork; *Update now* scarica il + L'icona controlla anche, una volta al giorno, le release GitHub; *Update now* scarica il nuovo `altkeeper.exe`, ne verifica lo SHA-256, si riavvia e torna alla versione vecchia se la - nuova non parte. Le release le compila `.github/workflows/release.yml` quando pubblichi un tag + nuova non parte. Le release (Windows, Linux e macOS) le compila `.github/workflows/release.yml` quando pubblichi un tag uguale alla `version` di `Cargo.toml` (per esempio `v0.3.0`). Ogni riga del server (AltServer, pagina web, DNS-SD) viene scritta con data e ora anche in diff --git a/README.md b/README.md index 7f54ee9..2e677c8 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,15 @@ and server logs, **not** the HTML web page, protocol or JSON. The same 2FA menu ### 1. Get the program +**Prebuilt:** each [GitHub release](https://github.com/BartolomeoRusso9/altkeeper/releases) has +`altkeeper--linux-x86_64.tar.gz`, `-linux-aarch64`, `-macos-aarch64` (Apple Silicon) and +`-macos-x86_64` (Intel), each with a `.sha256` file (check with `sha256sum -c`). Unpack it and use the +`altkeeper` binary inside. The Linux builds need glibc 2.35 or newer; on older systems or Alpine, use +Docker or build it yourself. The macOS builds are not signed: if Gatekeeper blocks it, run +`xattr -d com.apple.quarantine altkeeper`. Only the Windows `.exe` updates itself. + +**Or build it yourself:** + ```sh git clone https://github.com/BartolomeoRusso9/altkeeper cd altkeeper @@ -437,10 +446,10 @@ prove Windows 11, other shell hosts, AltServer installation or unattended renewa WebView2 runtime, which Windows 11 and recent Windows 10 already have. `--console` keeps the console, `--dir` chooses the data folder (default: the folder of the `.exe`). - The icon also checks the GitHub releases of the fork once a day; *Update now* downloads the new + The icon also checks the GitHub releases once a day; *Update now* downloads the new `altkeeper.exe`, verifies its SHA-256, restarts, and goes back to the old version if the new - one does not start. Releases are built by `.github/workflows/release.yml` when you push a tag - equal to the `version` in `Cargo.toml` (for example `v0.3.0`). + one does not start. Releases (Windows, Linux and macOS) are built by `.github/workflows/release.yml` when you push a + tag equal to the `version` in `Cargo.toml` (for example `v0.3.0`). Every server line (AltServer, web page, DNS-SD) is also written, with date and time, to `altkeeper.log` (rotated at 5 MB) and shown in the **Logs** section of the web page. diff --git a/src/tray.rs b/src/tray.rs index 4412490..2e8f924 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -25,7 +25,7 @@ pub enum UserEvent { Menu(MenuEvent), ServerFailed(String), Status { reachable: bool, tip: String }, - Updates { fork: Option, upstream: Option }, + Updates(Option), Restart, UpdateFailed(String), } @@ -48,23 +48,18 @@ pub fn wait_for_exit(pid: u32) { } } -fn open_in_browser(url: &str) { - let _ = std::process::Command::new("explorer").arg(url).spawn(); -} - -/// Controlla fork e progetto originale; `manual` = chiesto dal menu, quindi si risponde anche -/// quando non c'è niente di nuovo. +/// Controlla le release; `manual` = chiesto dal menu, quindi si risponde anche quando non c'è +/// niente di nuovo. fn check_updates(proxy: &EventLoopProxy, manual: bool) { - let fork = crate::update::check(); - let upstream = crate::update::upstream_newer(); - for e in [fork.as_ref().err(), upstream.as_ref().err()].into_iter().flatten() { + let latest = crate::update::check(); + if let Err(e) = &latest { crate::log::err(&format!("[update] {e}")); } - let (fork, upstream) = (fork.ok().flatten(), upstream.ok().flatten()); - if manual && fork.is_none() { + let latest = latest.ok().flatten(); + if manual && latest.is_none() { toast(&format!("AltKeeperPlus {} is up to date.", env!("CARGO_PKG_VERSION"))); } - let _ = proxy.send_event(UserEvent::Updates { fork, upstream }); + let _ = proxy.send_event(UserEvent::Updates(latest)); } /// Stacca il processo dal terminale: lanciato con un doppio clic, la finestra nera si chiude. @@ -313,7 +308,6 @@ pub fn run(bind: SocketAddr, dir: &Path, start: impl FnOnce(EventLoopProxy { - if let Some(r) = &fork { + Event::UserEvent(UserEvent::Updates(latest)) => { + if let Some(r) = &latest { update_now.set_text(format!("Update now to {}", r.tag)); update_now.set_enabled(true); if announced != r.tag { @@ -420,11 +411,7 @@ pub fn run(bind: SocketAddr, dir: &Path, start: impl FnOnce(EventLoopProxy { crate::log::line("[update] restarting with the new version"); diff --git a/src/update.rs b/src/update.rs index 8c11c9f..2bd380b 100644 --- a/src/update.rs +++ b/src/update.rs @@ -1,17 +1,12 @@ -//! Controllo degli aggiornamenti sulle release GitHub del fork (e, come promemoria, su quelle -//! del progetto originale). Solo Windows: lo usa l'icona nella barra di sistema. +//! Controllo e installazione degli aggiornamenti dalle release GitHub di AltKeeper. +//! Solo Windows: lo usa l'icona nella barra di sistema. use std::path::Path; use std::time::Duration; use serde_json::Value; -const FORK_LATEST: &str = "https://api.github.com/repos/WazeJinx/AltKeeperPlus/releases/latest"; -// Il progetto originale non pubblica release: si guarda l'ultimo commit su main. -const UPSTREAM_HEAD: &str = "https://api.github.com/repos/BartolomeoRusso9/altkeeper/commits/main"; -pub const UPSTREAM_COMMITS: &str = "https://github.com/BartolomeoRusso9/altkeeper/commits/main"; -/// Commit del progetto originale su cui si basa questo fork (da aggiornare dopo ogni sync). -const UPSTREAM_BASE: &str = "940d315413451cee4edd4f14a3c4f85892312791"; +const LATEST: &str = "https://api.github.com/repos/BartolomeoRusso9/altkeeper/releases/latest"; pub const EXE_ASSET: &str = "altkeeper.exe"; pub const SHA_ASSET: &str = "altkeeper.exe.sha256"; @@ -58,7 +53,7 @@ pub fn parse_release(v: &Value) -> Option { pub fn client() -> Result { reqwest::blocking::Client::builder() - .user_agent(concat!("AltKeeperPlus/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("AltKeeper/", env!("CARGO_PKG_VERSION"))) .timeout(Duration::from_secs(20)) .build() .map_err(|e| e.to_string()) @@ -76,23 +71,14 @@ fn get_json(url: &str) -> Result, String> { .map_err(|e| format!("update check failed: {e}")) } -/// La release del fork più nuova di quella in uso, se c'è. +/// L'ultima release, se è più nuova di quella in uso. pub fn check() -> Result, String> { - let v = get_json(FORK_LATEST)?; + let v = get_json(LATEST)?; Ok(v.as_ref() .and_then(parse_release) .filter(|r| newer(env!("CARGO_PKG_VERSION"), &r.tag))) } -/// I primi 7 caratteri dell'ultimo commit del progetto originale, se non è la base del fork. -pub fn upstream_newer() -> Result, String> { - let v = get_json(UPSTREAM_HEAD)?; - Ok(v.as_ref() - .and_then(|v| v["sha"].as_str()) - .filter(|sha| *sha != UPSTREAM_BASE) - .map(|sha| sha.chars().take(7).collect())) -} - /// Nome dell'exe precedente dopo un aggiornamento, finché quello nuovo non parte bene. pub const OLD_EXE: &str = "altkeeper.old.exe"; const NEW_EXE: &str = "altkeeper.new.exe"; @@ -234,15 +220,14 @@ mod tests { #[ignore = "network"] fn live_check() { rustls::crypto::ring::default_provider().install_default().ok(); - println!("fork: {:?}", check()); - println!("upstream: {:?}", upstream_newer()); + println!("latest: {:?}", check()); } #[test] fn parse_release_json() { let v = json!({ "tag_name": "v0.2.0-plus.2", - "html_url": "https://github.com/WazeJinx/AltKeeperPlus/releases/tag/v0.2.0-plus.2", + "html_url": "https://github.com/BartolomeoRusso9/altkeeper/releases/tag/v0.2.0-plus.2", "assets": [ { "name": "altkeeper.exe", "browser_download_url": "https://x/altkeeper.exe" }, { "name": "altkeeper.exe.sha256", "browser_download_url": "https://x/altkeeper.exe.sha256" }, From d3480f7b4704d82524f33d6e60cd78550687e6a9 Mon Sep 17 00:00:00 2001 From: Bartolo <162994065+BartolomeoRusso9@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:31:43 +0100 Subject: [PATCH 121/123] Update Cargo.lock --- Cargo.lock | 2086 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 2021 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a041439..14d24f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,20 +78,39 @@ name = "altkeeper" version = "0.3.0" dependencies = [ "axum", + "chrono", "idevice", "isideload", "mdns-sd", "plist", + "reqwest", "rpassword", "rustls", "serde", "serde_json", + "sha2 0.10.9", + "tao", + "tauri-winrt-notification", "tempfile", "tokio", "tracing-subscriber", + "tray-icon", + "windows-sys 0.61.2", + "winreg", + "winresource", + "wry", "zip", ] +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -117,7 +136,7 @@ dependencies = [ "sha2 0.10.9", "sha2 0.11.0", "spki 0.7.3", - "thiserror", + "thiserror 2.0.20", "x509-cert 0.2.5", ] @@ -133,7 +152,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 2.0.20", "time", ] @@ -216,11 +235,34 @@ dependencies = [ "crc32fast", "futures-lite", "pin-project", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-util", ] +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -312,6 +354,21 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit-vec" version = "0.9.1" @@ -321,6 +378,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.2" @@ -363,6 +426,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -381,6 +453,31 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "cbc" version = "0.1.2" @@ -409,6 +506,22 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.5" @@ -456,8 +569,10 @@ version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ + "iana-time-zone", "num-traits", "serde", + "windows-link", ] [[package]] @@ -560,6 +675,16 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -586,6 +711,30 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "libc", +] + [[package]] name = "core_detect" version = "1.0.0" @@ -625,6 +774,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.8" @@ -686,6 +844,29 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" +dependencies = [ + "quote", + "syn 3.0.6", +] + [[package]] name = "ctr" version = "0.10.1" @@ -737,6 +918,17 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + [[package]] name = "der" version = "0.7.10" @@ -805,6 +997,27 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + [[package]] name = "des" version = "0.9.0" @@ -838,6 +1051,39 @@ dependencies = [ "ctutils", ] +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -849,6 +1095,71 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ed25519" version = "2.2.3" @@ -917,12 +1228,31 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "find-msvc-tools" version = "0.1.13" @@ -942,7 +1272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.9.1", "zlib-rs", ] @@ -963,13 +1293,40 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", ] [[package]] @@ -978,6 +1335,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1089,63 +1452,304 @@ dependencies = [ ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "gdk" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" dependencies = [ - "typenum", - "version_check", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "gdk-pixbuf" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" dependencies = [ - "cfg-if", - "js-sys", + "gdk-pixbuf-sys", + "gio", + "glib", "libc", - "wasi", - "wasm-bindgen", + "once_cell", ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "gdk-pixbuf-sys" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" dependencies = [ - "cfg-if", - "js-sys", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", + "system-deps", ] [[package]] -name = "getrandom" -version = "0.4.3" +name = "gdk-sys" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" dependencies = [ - "cfg-if", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", + "pango-sys", + "pkg-config", + "system-deps", ] [[package]] -name = "ghash" -version = "0.6.0" +name = "gdkwayland-sys" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ "polyval", ] +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "h2" version = "0.4.19" @@ -1177,6 +1781,18 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -1210,6 +1826,16 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + [[package]] name = "http" version = "1.5.0" @@ -1342,6 +1968,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.3.0" @@ -1456,7 +2106,7 @@ dependencies = [ "sha1 0.10.7", "sha2 0.10.9", "siphasher", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-rustls", "tracing", @@ -1581,7 +2231,7 @@ dependencies = [ "sha1 0.11.0", "sha2 0.11.0", "srp", - "thiserror", + "thiserror 2.0.20", "tokio", "tokio-tungstenite", "tracing", @@ -1611,7 +2261,7 @@ dependencies = [ "rc2", "sha1 0.11.0", "sha2 0.11.0", - "thiserror", + "thiserror 2.0.20", "x509-parser", ] @@ -1627,6 +2277,29 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "jktcp" version = "0.1.7" @@ -1642,6 +2315,22 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -1651,10 +2340,10 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -1672,6 +2361,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + [[package]] name = "jni-sys" version = "0.4.1" @@ -1711,6 +2409,16 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.13.2", + "serde", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1720,18 +2428,70 @@ dependencies = [ "spin", ] +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + [[package]] name = "libc" version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libm" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1759,6 +2519,17 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1815,10 +2586,19 @@ dependencies = [ ] [[package]] -name = "mime" -version = "0.3.17" +name = "memoffset" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -1826,6 +2606,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "miniz_oxide" version = "0.9.1" @@ -1848,6 +2638,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "muda" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libc", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "png", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + [[package]] name = "multer" version = "3.1.0" @@ -1909,6 +2719,42 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -1989,6 +2835,222 @@ dependencies = [ "libm", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -2016,9 +3078,9 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags", + "bitflags 2.13.2", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "openssl-macros", "openssl-sys", @@ -2053,6 +3115,37 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "parking" version = "2.2.1" @@ -2126,6 +3219,59 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -2248,6 +3394,19 @@ dependencies = [ "plist", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -2294,6 +3453,65 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.17+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -2410,8 +3628,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "rayon" -version = "1.12.0" +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ @@ -2458,7 +3682,17 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.20", ] [[package]] @@ -2487,7 +3721,9 @@ dependencies = [ "base64 0.23.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", + "futures-util", "h2", "http", "http-body", @@ -2530,7 +3766,7 @@ dependencies = [ "async-trait", "http", "reqwest", - "thiserror", + "thiserror 2.0.20", "tower-service", ] @@ -2642,7 +3878,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -2692,7 +3928,7 @@ checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -2780,7 +4016,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -2797,6 +4033,25 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags 2.13.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "semver" version = "1.0.28" @@ -2857,6 +4112,24 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + [[package]] name = "serdect" version = "0.4.3" @@ -2867,6 +4140,15 @@ dependencies = [ "serde", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha1" version = "0.10.7" @@ -3017,6 +4299,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "spin" version = "0.9.9" @@ -3064,12 +4372,46 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.119" @@ -3129,7 +4471,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags", + "bitflags 2.13.2", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3144,6 +4486,87 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548" +dependencies = [ + "bitflags 2.13.2", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri-winrt-notification" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36" +dependencies = [ + "thiserror 2.0.20", + "windows", + "windows-version", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -3157,13 +4580,42 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -3326,6 +4778,102 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + [[package]] name = "tower" version = "0.5.3" @@ -3348,7 +4896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags", + "bitflags 2.13.2", "bytes", "futures-core", "futures-util", @@ -3437,6 +4985,27 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tray-icon" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5cc5858ecf9574ddd8f0032a2ff7ffd6f7724d2b81e48b9374984b3fd6ddc9" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + [[package]] name = "triomphe" version = "0.1.16" @@ -3464,7 +5033,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1 0.10.7", - "thiserror", + "thiserror 2.0.20", ] [[package]] @@ -3485,6 +5054,12 @@ version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "universal-hash" version = "0.5.1" @@ -3560,6 +5135,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -3688,6 +5269,62 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + [[package]] name = "webpki-root-certs" version = "1.0.9" @@ -3715,6 +5352,56 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webview2-com" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" +dependencies = [ + "thiserror 2.0.20", + "windows", + "windows-core", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -3724,12 +5411,95 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -3759,13 +5529,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3777,34 +5556,85 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "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-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3817,30 +5647,92 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "winresource" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b" +dependencies = [ + "toml 1.1.8+spec-1.1.0", + "version_check", +] + [[package]] name = "wit-bindgen" version = "0.57.1" @@ -3853,6 +5745,70 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +[[package]] +name = "wry" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9" +dependencies = [ + "base64 0.23.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "raw-window-handle", + "sha2 0.10.9", + "soup3", + "tao-macros", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -3904,7 +5860,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror", + "thiserror 2.0.20", "time", ] @@ -3914,7 +5870,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" dependencies = [ - "bit-vec", + "bit-vec 0.9.1", "time", ] From 55b8296e95a3aa20d3fad9870dba8a3a1ef81f90 Mon Sep 17 00:00:00 2001 From: Bartolo <162994065+BartolomeoRusso9@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:36:42 +0100 Subject: [PATCH 122/123] Fix --- src/install_archive.rs | 5 +++-- src/profile.rs | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/install_archive.rs b/src/install_archive.rs index dd5a396..230b208 100644 --- a/src/install_archive.rs +++ b/src/install_archive.rs @@ -1,11 +1,11 @@ //! Package a *signed* .app as one IPA before staging it over Wi-Fi. //! The old directory-based sideload path remains available as a rollback. use std::fs::{self, File, Metadata}; -use std::io::{Read, Seek, Write}; +use std::io::{Seek, Write}; use std::path::Path; use idevice::{ - IdeviceService, ReadWrite, RsdService, + RsdService, afc::{AfcClient, opcode::AfcFopenMode}, installation_proxy::InstallationProxyClient, rsd::RsdHandshake, @@ -196,6 +196,7 @@ pub(crate) async fn install_signed_app_rsd( #[cfg(test)] mod tests { use super::*; + use std::io::Read; #[test] fn packages_signed_app_with_payload_and_nested_files() { diff --git a/src/profile.rs b/src/profile.rs index e4e6cab..88bd733 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -71,6 +71,7 @@ pub fn group_root<'a>(bundle: &'a str, bundles: &[&'a str], team: &str) -> &'a s .unwrap_or(bundle) } +#[cfg(test)] pub fn members_of_group<'a>(root: &str, bundles: &'a [&'a str], team: &str) -> Vec<&'a str> { bundles.iter().copied() .filter(|b| group_root(b, bundles, team) == root) From 58fb793f2c95fcb25dd874607a3e2cd9151eaafb Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:59:49 +0000 Subject: [PATCH 123/123] Fix update staging, restart log polling, backup cleanup, localized file handling, and CI workflows --- .github/workflows/docker.yml | 32 ++++++++++++--- .github/workflows/release.yml | 12 +++--- docs/KOREAN_UI_REVIEW.md | 12 +++--- scripts/check-web-ui.cjs | 43 ++++++++++++++++++++ src/profile.rs | 25 +++++++++++- src/tray.rs | 17 +++++--- src/web.rs | 8 ++-- src/web/index.html | 3 +- tests/localized_identity.rs | 41 +++++++++++++++++++ vendor/isideload/src/sideload/application.rs | 3 +- 10 files changed, 166 insertions(+), 30 deletions(-) create mode 100644 tests/localized_identity.rs diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5c14a50..915356a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,6 +1,6 @@ # Prova il codice e pubblica l'immagine Docker su GitHub Container Registry (ghcr.io). # -# - Pull request: esegue i test Linux e Windows, senza pubblicare immagini. +# - Pull request: esegue i test Linux e Windows e compila il Dockerfile per amd64, senza pubblicare immagini. # - Push su main: pubblica `latest` e `sha-` per amd64 e arm64. # - Tag `vX.Y.Z` (per esempio `git tag v0.2.0 && git push --tags`): pubblica `X.Y.Z`, `X.Y` e # `latest`, per amd64 e arm64 (build nativi paralleli, senza QEMU). @@ -64,8 +64,24 @@ jobs: $output = cargo run --locked -- --help if ($LASTEXITCODE -ne 0 -or -not ($output -match '^uso: altkeeper')) { throw "Italian CLI help failed: $output" } + build-pr: + if: github.event_name == 'pull_request' + needs: [test, test-windows] + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + - uses: docker/setup-buildx-action@v4 + - uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64 + push: false + cache-from: type=gha,scope=altkeeper-amd64 + image: - # Build architectures concurrently on native runners; PRs run tests only. + # Build architectures concurrently on native runners for publishing. if: github.event_name != 'pull_request' needs: [test, test-windows] strategy: @@ -91,17 +107,19 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - id: image-name + run: echo "image=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" - id: meta uses: docker/metadata-action@v6 with: - images: ghcr.io/${{ github.repository }} + images: ${{ steps.image-name.outputs.image }} - id: build uses: docker/build-push-action@v7 with: context: . platforms: ${{ matrix.platform }} # Immutable per-arch pushes; only the final manifest receives tags. - outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true + outputs: type=image,name=${{ steps.image-name.outputs.image }},push-by-digest=true,name-canonical=true,push=true labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=altkeeper-${{ matrix.arch }} cache-to: type=gha,scope=altkeeper-${{ matrix.arch }},mode=max @@ -138,10 +156,12 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - id: image-name + run: echo "image=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" - id: meta uses: docker/metadata-action@v6 with: - images: ghcr.io/${{ github.repository }} + images: ${{ steps.image-name.outputs.image }} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} @@ -156,7 +176,7 @@ jobs: test "$(find . -maxdepth 1 -type f | wc -l)" -eq 2 docker buildx imagetools create \ $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *) + $(printf '${{ steps.image-name.outputs.image }}@sha256:%s ' *) - name: Inspect published manifest env: DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 823364f..dfa1300 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,9 +42,9 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 - name: Check that the tag matches Cargo.toml run: | version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1) @@ -78,7 +78,7 @@ jobs: else shasum -a 256 "$pkg.tar.gz" > "$pkg.tar.gz.sha256" fi - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: dist-${{ matrix.name }} path: dist/* @@ -92,14 +92,14 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: path: dist pattern: dist-* merge-multiple: true - name: List files run: ls -l dist - - uses: softprops/action-gh-release@v2 + - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: files: dist/* fail_on_unmatched_files: true diff --git a/docs/KOREAN_UI_REVIEW.md b/docs/KOREAN_UI_REVIEW.md index e351678..e4e0905 100644 --- a/docs/KOREAN_UI_REVIEW.md +++ b/docs/KOREAN_UI_REVIEW.md @@ -1,14 +1,14 @@ -# AltKeeper 한국어 웹 UI 번역 — 검수 승인 및 적용 +# AltKeeper 한국어 웹 UI 번역 — 제안 -> 상태: **검수 승인 및 PR #8에 구현** — 한국어가 기본값이며 웹 상단의 언어 선택에서 English로 전환할 수 있습니다. 선택한 언어는 브라우저에 저장됩니다. +> 상태: **미적용 번역 제안** — 아래 한국어 문구는 검토를 위한 제안이며, 현재 웹 UI는 영어로 표시됩니다. ## 번역 원칙 -- UI의 **AltKeeper**, **Apple ID**, **Discord**, **IPA**, **UUID**, **KST** 등 식별자와 제품명은 번역하지 않음. +- UI의 **AltKeeper**, **Apple ID**, **Discord**, **IPA**, **UUID**, **UTC** 등 식별자와 제품명은 번역하지 않음. - `Renew`는 '갱신', `Install`은 '설치', `Provisioning profile`은 '프로비저닝 프로파일'로 통일. - 이 앱 목록은 휴대전화의 프로파일을 기반으로 하므로, 설치 앱 조회에 실패했을 때도 표시할 수 있음. 따라서 '설치된 앱' 대신 **앱 목록** 사용. - 실패 로그·Apple 원문 오류는 정확한 문제 진단을 위해 번역보다는 원문 유지 + 짧은 한국어 설명 병기를 권장. -- 구현: **한국어 기본 / English 전환**. 브라우저 선택값을 저장하며 서버 API 및 영어 진단 로그 내용은 보존합니다. +- 번역 적용 시 서버 API 및 영어 진단 로그 내용은 보존합니다. ## 주요 UI 문구 @@ -32,7 +32,7 @@ | OFF | 꺼짐 | | ON | 켜짐 | | Automatic renewal disabled | 자동 갱신이 꺼져 있습니다 | -| Daily time (KST) | 매일 실행 시간 (KST) | +| Daily time (UTC) | 매일 실행 시간 (UTC) | | Renew when days remaining | 만료까지 남은 기간이 | | 1 day / 2 days / … / 6 days | 1일 / 2일 / … / 6일 | | Enable auto-renew | 자동 갱신 켜기 | @@ -104,4 +104,4 @@ 3. '앱 목록'의 앱 표시는 프로파일 기반이며, 실제 설치 여부를 항상 확정하지 않는다는 설명을 보일지. 4. 일본어/영어로 출력되는 기기 진단 로그는 **원문 보존**할지, 한국어 요약을 함께 제공할지. -**검수 승인에 따라 정적·동적 웹 문구를 한국어로 적용했습니다.** 원본 진단 로그와 서버 오류 전문은 정확한 디버깅을 위해 영어로 유지합니다. 같은 버튼 영역의 버튼은 동일 크기와 가운데 정렬을 사용하고, 좁은 화면에서는 버튼이 줄바꿈됩니다. +**정적·동적 웹 문구의 한국어 번역은 아직 적용되지 않았습니다.** 원본 진단 로그와 서버 오류 전문은 정확한 디버깅을 위해 보존할 것을 제안합니다. 현재 같은 버튼 영역의 버튼은 같은 너비로 배치되고 문구는 가운데 정렬됩니다. 좁은 화면에서도 버튼은 줄바꿈되지 않고 한 줄에 유지됩니다. diff --git a/scripts/check-web-ui.cjs b/scripts/check-web-ui.cjs index 11835e0..653eca0 100644 --- a/scripts/check-web-ui.cjs +++ b/scripts/check-web-ui.cjs @@ -165,3 +165,46 @@ async function checkDashboardState() { } checkDashboardState().catch(error => { console.error(error); process.exitCode = 1; }); +// A restarted server resets its sequence. Fetch its startup lines before +// advancing the cursor, and keep the old cursor if that recovery request fails. +async function checkLogPolling() { + const view = { textContent: "before restart", scrollTop: 0, clientHeight: 100, scrollHeight: 100 }; + const requests = [], responses = [], timers = []; + const context = vm.createContext({ + $: id => { assert.equal(id, "logs-view"); return view; }, + api: async path => { + requests.push(path); + const response = responses.shift(); + if (response instanceof Error) throw response; + assert(response, "unexpected log request"); + return response; + }, + setTimeout: (_fn, delay) => timers.push(delay), + }); + const poll = embedded[1].match(/async function pollLogs\(\) \{[\s\S]*?\r?\n\}/); + assert(poll); + vm.runInContext('let logSeq = 100;\n' + poll[0], context); + responses.push({ seq: 2, lines: [] }, { seq: 3, lines: ["startup", "ready", "listening"] }); + await vm.runInContext('pollLogs()', context); + assert.deepEqual(requests.splice(0), ["/api/logs?since=100", "/api/logs?since=0"]); + assert.equal(view.textContent, "before restart\nstartup\nready\nlistening"); + assert.equal(vm.runInContext('logSeq', context), 3); + assert.equal(view.scrollTop, view.scrollHeight); + + responses.push({ seq: 4, lines: ["next"] }); + await vm.runInContext('pollLogs()', context); + assert.deepEqual(requests.splice(0), ["/api/logs?since=3"]); + assert(view.textContent.endsWith("\nnext")); + + responses.push({ seq: 1, lines: [] }, new Error("temporarily offline")); + await vm.runInContext('pollLogs()', context); + assert.equal(vm.runInContext('logSeq', context), 4); + assert.deepEqual(requests.splice(0), ["/api/logs?since=4", "/api/logs?since=0"]); + + responses.push({ seq: 0, lines: [] }, { seq: 0, lines: [] }); + await vm.runInContext('pollLogs()', context); + assert.equal(vm.runInContext('logSeq', context), 0); + assert(view.textContent.endsWith("\nnext")); + assert.deepEqual(timers, [2000, 2000, 2000, 2000]); +} +checkLogPolling().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/src/profile.rs b/src/profile.rs index 88bd733..1e97816 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -159,7 +159,9 @@ pub fn cleanup_expired_backups(dir: &Path, now: SystemTime) -> io::Result let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else { continue; }; - let data = fs::read(&path)?; + let Ok(data) = fs::read(&path) else { + continue; + }; let Some(p) = parse(&data) else { continue; }; @@ -285,6 +287,27 @@ mod tests { assert_eq!(cleanup_expired_backups(&dir, SystemTime::now()).unwrap(), 0); } + #[cfg(unix)] + #[test] + fn cleanup_retains_unreadable_backups_and_continues() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().unwrap(); + let now = SystemTime::now(); + let unreadable = dir.path().join("unreadable.mobileprovision"); + let expired = dir.path().join("expired.mobileprovision"); + let expiry = Some(now - Duration::from_secs(31 * 86400)); + fs::write(&unreadable, backup_xml("unreadable", expiry)).unwrap(); + fs::write(&expired, backup_xml("expired", expiry)).unwrap(); + fs::set_permissions(&unreadable, fs::Permissions::from_mode(0o000)).unwrap(); + // Root bypasses mode bits, so it cannot exercise a permission-denied read. + if fs::read(&unreadable).is_ok() { return; } + + assert_eq!(cleanup_expired_backups(dir.path(), now).unwrap(), 1); + assert!(unreadable.exists()); + assert!(!expired.exists()); + } + #[test] fn extension_groups_do_not_swallow_unrelated_apps() { let bundles = [ diff --git a/src/tray.rs b/src/tray.rs index 2e8f924..d379356 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -55,10 +55,14 @@ fn check_updates(proxy: &EventLoopProxy, manual: bool) { if let Err(e) = &latest { crate::log::err(&format!("[update] {e}")); } - let latest = latest.ok().flatten(); - if manual && latest.is_none() { - toast(&format!("AltKeeperPlus {} is up to date.", env!("CARGO_PKG_VERSION"))); + if manual { + match &latest { + Ok(None) => toast(&format!("AltKeeperPlus {} is up to date.", env!("CARGO_PKG_VERSION"))), + Err(e) => toast(&format!("Update check failed: {e}")), + Ok(Some(_)) => {} + } } + let latest = latest.ok().flatten(); let _ = proxy.send_event(UserEvent::Updates(latest)); } @@ -352,6 +356,7 @@ pub fn run(bind: SocketAddr, dir: &Path, start: impl FnOnce(EventLoopProxy = None; + let mut staging = false; let mut announced = String::new(); start(proxy); @@ -378,8 +383,9 @@ pub fn run(bind: SocketAddr, dir: &Path, start: impl FnOnce(EventLoopProxy { + Event::UserEvent(UserEvent::Updates(latest)) if !staging => { if let Some(r) = &latest { update_now.set_text(format!("Update now to {}", r.tag)); update_now.set_enabled(true); @@ -439,6 +445,7 @@ pub fn run(bind: SocketAddr, dir: &Path, start: impl FnOnce(EventLoopProxy { + staging = false; crate::log::err(&format!("[update] {e}")); toast(&format!("Update failed: {e}")); update_now.set_text("Update now (try again)"); diff --git a/src/web.rs b/src/web.rs index cfc4375..b5b63af 100644 --- a/src/web.rs +++ b/src/web.rs @@ -305,7 +305,7 @@ fn discord_configured() -> bool { } fn send_discord_failure(error: &str) -> Result<(), String> { - let message = format!("AltKeeper: automatic renewal failed after retries.\\n{}", + let message = format!("AltKeeper: automatic renewal failed after retries.\n{}", error.chars().take(240).collect::()); send_discord_message(&message) } @@ -463,11 +463,11 @@ pub async fn serve( tokio::spawn(async { loop { match profile::cleanup_backups_if_due(SystemTime::now()) { - Ok(Some(removed)) => println!( + Ok(Some(removed)) => crate::log::line(&format!( "[backup-cleanup] Monthly sweep: removed {removed} profiles expired over 30 days ago" - ), + )), Ok(None) => {} - Err(e) => eprintln!("[backup-cleanup] Failed: {e}"), + Err(e) => crate::log::err(&format!("[backup-cleanup] Failed: {e}")), } tokio::time::sleep(Duration::from_secs(24 * 60 * 60)).await; } diff --git a/src/web/index.html b/src/web/index.html index fed1a97..db4902d 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -892,7 +892,8 @@

        Logs

        let logSeq = 0; async function pollLogs() { try { - const r = await api("/api/logs?since=" + logSeq); + let r = await api("/api/logs?since=" + logSeq); + if (r.seq < logSeq) r = await api("/api/logs?since=0"); if (r.lines.length) { const v = $("logs-view"); const atEnd = v.scrollTop + v.clientHeight >= v.scrollHeight - 4; diff --git a/tests/localized_identity.rs b/tests/localized_identity.rs new file mode 100644 index 0000000..6450a35 --- /dev/null +++ b/tests/localized_identity.rs @@ -0,0 +1,41 @@ +#![cfg(unix)] + +use isideload::sideload::{application::Application, bundle::Bundle}; +use std::{fs, os::unix::fs::symlink}; + +#[test] +fn display_name_rewrite_skips_symlinks_and_non_regular_files() { + let temp = tempfile::tempdir().unwrap(); + let app = temp.path().join("Test.app"); + fs::create_dir(&app).unwrap(); + let mut info = plist::Dictionary::new(); + info.insert("CFBundleIdentifier".into(), "com.example.test".into()); + info.insert("CFBundleName".into(), "Old".into()); + plist::to_file_xml(app.join("Info.plist"), &info).unwrap(); + let old = b"\"CFBundleDisplayName\" = \"Old\";\n"; + + let localized = app.join("en.lproj"); + fs::create_dir(&localized).unwrap(); + fs::write(localized.join("InfoPlist.strings"), old).unwrap(); + let external_dir = temp.path().join("external"); + fs::create_dir(&external_dir).unwrap(); + fs::write(external_dir.join("InfoPlist.strings"), old).unwrap(); + symlink(&external_dir, app.join("linked.lproj")).unwrap(); + let linked_file_dir = app.join("fr.lproj"); + fs::create_dir(&linked_file_dir).unwrap(); + symlink(external_dir.join("InfoPlist.strings"), linked_file_dir.join("InfoPlist.strings")).unwrap(); + fs::write(app.join("file.lproj"), b"not a directory").unwrap(); + fs::create_dir_all(app.join("directory.lproj/InfoPlist.strings")).unwrap(); + fs::create_dir(app.join("unrelated")).unwrap(); + fs::write(app.join("unrelated/InfoPlist.strings"), old).unwrap(); + + let mut application = Application { bundle: Bundle::new(app.clone()).unwrap() }; + application.apply_install_identity(Some("New"), None).unwrap(); + + assert!(fs::read_to_string(localized.join("InfoPlist.strings")).unwrap().contains("\"New\"")); + assert_eq!(fs::read(external_dir.join("InfoPlist.strings")).unwrap(), old); + assert_eq!(fs::read(app.join("unrelated/InfoPlist.strings")).unwrap(), old); + assert!(fs::symlink_metadata(app.join("linked.lproj")).unwrap().is_symlink()); + assert!(fs::symlink_metadata(linked_file_dir.join("InfoPlist.strings")).unwrap().is_symlink()); + assert!(app.join("directory.lproj/InfoPlist.strings").is_dir()); +} diff --git a/vendor/isideload/src/sideload/application.rs b/vendor/isideload/src/sideload/application.rs index bcebf48..9696b92 100644 --- a/vendor/isideload/src/sideload/application.rs +++ b/vendor/isideload/src/sideload/application.rs @@ -326,8 +326,9 @@ impl Application { let entry = entry?; let localized = entry.path(); if localized.extension().is_none_or(|ext| ext != "lproj") { continue; } + if !entry.metadata().is_ok_and(|meta| meta.is_dir()) { continue; } let path = localized.join("InfoPlist.strings"); - if !isideload_vfs::fs::metadata(&path).is_ok() { continue; } + if !isideload_vfs::fs::symlink_metadata(&path).is_ok_and(|meta| meta.is_file()) { continue; } let old = isideload_vfs::fs::read(&path)?; if old.len() > 1024 * 1024 { bail!(SideloadError::InvalidBundle("Localized display-name file too large".into()));