From 7d74526d826c63629557bfdf82cbc198a2401681 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 02:58:55 -0700 Subject: [PATCH 01/31] fix(upstream): preserve verified API 21/23 Gradle SDK contracts (#45) --- scripts/apply-integration.ps1 | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/apply-integration.ps1 b/scripts/apply-integration.ps1 index a57bbf8..a2edb93 100644 --- a/scripts/apply-integration.ps1 +++ b/scripts/apply-integration.ps1 @@ -203,14 +203,22 @@ if ($coreBuild -notmatch [regex]::Escape('tgwsproxy-core.aar')) { $coreBuild = $coreBuild.Replace($coreDependencyMarker, $coreDependencyBlock) } -$apiGradleMarker = @' - defaultConfig { - minSdkVersion 21 - targetSdkVersion 36 -'@.TrimEnd() +# Both confirmed Telegram baselines are supported; unknown SDK contracts fail closed. +# Telegram 12.10.6 requires API 21, while Telegram 13.0.3 requires API 23. +$apiGradlePattern = '(?m)^ defaultConfig \{\r?\n minSdkVersion (21|23)\r?\n targetSdkVersion 36' +$coreSdkMatches = [regex]::Matches($coreBuild, $apiGradlePattern) +$appSdkMatches = [regex]::Matches($build, $apiGradlePattern) +if ($coreSdkMatches.Count -ne 1 -or $appSdkMatches.Count -ne 1) { + throw "Telegram minSdk/targetSdk contract is unsupported: core=$($coreSdkMatches.Count), standalone=$($appSdkMatches.Count)." +} +$minimumSdk = [int]$coreSdkMatches[0].Groups[1].Value +if ($minimumSdk -ne [int]$appSdkMatches[0].Groups[1].Value) { + throw 'Telegram core/standalone minSdk versions differ.' +} +$apiGradleMarker = $coreSdkMatches[0].Value $apiGradleBlock = @' defaultConfig { - minSdkVersion 21 + minSdkVersion __MIN_SDK__ targetSdkVersion 36 def telegramApiId = System.getenv('TELEGRAM_API_ID') ?: getProps('TELEGRAM_API_ID') @@ -230,6 +238,7 @@ $apiGradleBlock = @' buildConfigField "int", "TELEGRAM_API_ID", telegramApiId buildConfigField "String", "TELEGRAM_API_HASH", "\"${telegramApiHash}\"" '@.TrimEnd() +$apiGradleBlock = $apiGradleBlock.Replace('__MIN_SDK__', [string]$minimumSdk) if ($coreBuild -notmatch [regex]::Escape('TELEGRAM_API_ID')) { $count = ([regex]::Matches($coreBuild, [regex]::Escape($apiGradleMarker))).Count if ($count -ne 1) { throw "Telegram API Gradle anchor count is $count; expected 1." } From ed03766d3ed9efabfc996bd2f23636e60b263e22 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:02:43 -0700 Subject: [PATCH 02/31] fix(updater): retry after failure, manual and foreground checks, strict stable manifest (#45) --- integration/telegram/TgWsProxyBootstrap.java | 276 +++++++++++++------ 1 file changed, 191 insertions(+), 85 deletions(-) diff --git a/integration/telegram/TgWsProxyBootstrap.java b/integration/telegram/TgWsProxyBootstrap.java index 53f909d..da8653e 100644 --- a/integration/telegram/TgWsProxyBootstrap.java +++ b/integration/telegram/TgWsProxyBootstrap.java @@ -1,5 +1,7 @@ package org.telegram.messenger; +import android.app.Activity; +import android.app.Application; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -10,6 +12,7 @@ import android.os.Build; import android.os.Handler; import android.os.Looper; +import android.os.Bundle; import android.provider.Settings; import android.widget.Toast; @@ -42,7 +45,8 @@ public final class TgWsProxyBootstrap { private static final String KEY_RUNTIME_CONFIG = "runtime_config"; private static final String KEY_MANAGED_PROXY = "managed_proxy"; private static final String KEY_UPDATE_ENABLED = "update_enabled"; - private static final String KEY_UPDATE_LAST_CHECK = "update_last_check"; + private static final String KEY_UPDATE_LAST_SUCCESS = "update_last_success"; + private static final String KEY_UPDATE_LAST_ATTEMPT = "update_last_attempt"; private static final String HOST = "127.0.0.1"; private static final int PORT = 1443; private static final String LEGACY_RUNTIME_CONFIG = "@mtproto_worker_preconnect=1"; @@ -53,6 +57,8 @@ public final class TgWsProxyBootstrap { private static final String UPDATE_APK_URL_PREFIX = "https://github.com/Regstar2/telegram-wsp/releases/download/"; private static final long UPDATE_CHECK_INTERVAL_MS = 12L * 60L * 60L * 1000L; + private static final long UPDATE_RETRY_DELAY_MS = 10L * 60L * 1000L; + private static final String RELEASES_URL = "https://github.com/Regstar2/telegram-wsp/releases"; private static final int NETWORK_CONNECT_TIMEOUT_MS = 15_000; private static final int NETWORK_READ_TIMEOUT_MS = 30_000; private static final int UPDATE_METADATA_LIMIT_BYTES = 64 * 1024; @@ -60,6 +66,9 @@ public final class TgWsProxyBootstrap { private static final char[] HEX = "0123456789abcdef".toCharArray(); private static boolean initialized; + private static boolean checking; + private static UpdateInfo pendingUpdate; + private static long promptedVersionCode = -1L; private TgWsProxyBootstrap() { } @@ -81,58 +90,179 @@ public static void start(Context context) { bootstrapThread.setDaemon(true); bootstrapThread.start(); - Thread updateThread = new Thread( - () -> checkForUpdatesInBackground(appContext), - "TelegramWSPUpdateCheck" - ); - updateThread.setDaemon(true); - updateThread.start(); + if (appContext instanceof Application) { + ((Application) appContext).registerActivityLifecycleCallbacks( + new Application.ActivityLifecycleCallbacks() { + @Override public void onActivityCreated(Activity a, Bundle state) {} + @Override public void onActivityStarted(Activity a) {} + @Override public void onActivityPaused(Activity a) {} + @Override public void onActivityStopped(Activity a) {} + @Override public void onActivitySaveInstanceState(Activity a, Bundle state) {} + @Override public void onActivityDestroyed(Activity a) {} + @Override public void onActivityResumed(Activity a) { + if (a instanceof LaunchActivity) { + postToMain(() -> offerPendingUpdate(a)); + requestUpdateCheck(appContext, false); + } + } + }); + } + requestUpdateCheck(appContext, false); } - private static void startInBackground(Context appContext) { + private static void startInBackground(Context context) { try { - TgWsProxyController.start(appContext); + TgWsProxyController.start(context); } catch (Throwable error) { FileLog.e("TgWsProxy bootstrap failed: " + error.getClass().getSimpleName()); } } - private static void checkForUpdatesInBackground(Context appContext) { + private static void postToMain(Runnable runnable) { + Handler handler = ApplicationLoader.applicationHandler; + (handler == null ? new Handler(Looper.getMainLooper()) : handler).post(runnable); + } + + private static void message(Context context, String text) { + postToMain(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show()); + } + + public static void checkForUpdates(Context context) { + requestUpdateCheck(context.getApplicationContext(), true); + } + + public static void openReleases(Context context) { try { - SharedPreferences preferences = appContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE); - if (!preferences.getBoolean(KEY_UPDATE_ENABLED, true)) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(RELEASES_URL)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } catch (Throwable error) { + FileLog.e("Could not open Telegram-WSP releases: " + error); + message(context, "Could not open GitHub Releases."); + } + } + + private static boolean isCoolingDown(long now, long previous, long interval) { + return previous > 0L && now >= previous && now - previous < interval; + } + + private static void requestUpdateCheck(Context context, boolean manual) { + SharedPreferences prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE); + if (!manual && !prefs.getBoolean(KEY_UPDATE_ENABLED, true)) { + return; + } + synchronized (TgWsProxyBootstrap.class) { + if (checking) { + if (manual) message(context, "Telegram-WSP: update check already in progress."); return; } - long now = System.currentTimeMillis(); - long lastCheck = preferences.getLong(KEY_UPDATE_LAST_CHECK, 0L); - if (lastCheck > 0L && now - lastCheck < UPDATE_CHECK_INTERVAL_MS) { + if (!manual && (isCoolingDown(now, + prefs.getLong(KEY_UPDATE_LAST_SUCCESS, 0L), UPDATE_CHECK_INTERVAL_MS) + || isCoolingDown(now, + prefs.getLong(KEY_UPDATE_LAST_ATTEMPT, 0L), UPDATE_RETRY_DELAY_MS))) { return; } - preferences.edit().putLong(KEY_UPDATE_LAST_CHECK, now).apply(); + checking = true; + prefs.edit().putLong(KEY_UPDATE_LAST_ATTEMPT, now).apply(); + } - String metadataJson = readSmallHttpsResource(UPDATE_FEED_URL); - JSONObject metadata = new JSONObject(metadataJson); - long remoteVersionCode = metadata.getLong("versionCode"); - long currentVersionCode = getCurrentVersionCode(appContext); - if (remoteVersionCode <= currentVersionCode) { - return; + Thread thread = new Thread(() -> { + try { + JSONObject metadata = new JSONObject(readSmallHttpsResource(UPDATE_FEED_URL)); + UpdateInfo update = validateUpdate(metadata); + long remoteVersionCode = update.versionCode; + long currentVersionCode = getCurrentVersionCode(context); + prefs.edit().putLong(KEY_UPDATE_LAST_SUCCESS, System.currentTimeMillis()).apply(); + if (remoteVersionCode <= currentVersionCode) { + synchronized (TgWsProxyBootstrap.class) { pendingUpdate = null; } + if (manual) message(context, "Telegram-WSP is up to date."); + } else if (update.minimumSdk > Build.VERSION.SDK_INT) { + if (manual) message(context, update.tag + " requires Android API " + update.minimumSdk); + } else { + synchronized (TgWsProxyBootstrap.class) { + pendingUpdate = update; + if (manual) promptedVersionCode = -1L; + } + postToMain(() -> { + if (LaunchActivity.instance != null) { + offerPendingUpdate(LaunchActivity.instance); + } else if (manual) { + message(context, "Update found. Open Telegram to see the update prompt."); + } + }); + } + } catch (Throwable error) { + FileLog.e("Telegram-WSP update check failed: " + error); + if (manual) { + message(context, "Update check failed. Retry or open GitHub Releases."); + } + } finally { + synchronized (TgWsProxyBootstrap.class) { checking = false; } } + }, "TelegramWSPUpdateCheck"); + thread.setDaemon(true); + thread.start(); + } - String versionName = metadata.optString("versionName", String.valueOf(remoteVersionCode)); - String apkUrl = metadata.getString("apk"); - String apkSha256 = metadata.getString("apkSha256").toLowerCase(java.util.Locale.US); - if (!apkUrl.startsWith(UPDATE_APK_URL_PREFIX)) { - throw new IllegalStateException("Unexpected update APK URL: " + apkUrl); - } - if (!apkSha256.matches("[0-9a-f]{64}")) { - throw new IllegalStateException("Invalid update APK SHA-256"); + private static UpdateInfo validateUpdate(JSONObject metadata) throws Exception { + String version = metadata.getString("versionName"); + String tag = metadata.getString("tag"); + int revision = metadata.getInt("revision"); + long build = metadata.getLong("telegramBuild"); + long code = metadata.getLong("versionCode"); + int minSdk = metadata.getInt("minimumSdk"); + String apk = metadata.getString("apk"); + String hash = metadata.getString("apkSha256").toLowerCase(java.util.Locale.US); + if (!version.matches("[0-9]+\\.[0-9]+\\.[0-9]+") + || revision < 1 || revision > 99 + || !tag.equals("v" + version + "-wsp." + revision) + || build <= 0L || build > 2_000_000L + || code != build * 1000L + revision * 10L + 9L + || minSdk < 21 || minSdk > 100 + || !"org.telegram.messenger.web".equals(metadata.getString("packageId")) + || !apk.equals(UPDATE_APK_URL_PREFIX + tag + "/Telegram-WSP-release.apk") + || !hash.matches("[0-9a-f]{64}")) { + throw new SecurityException("Invalid stable Telegram-WSP update manifest"); + } + return new UpdateInfo(code, version, tag, apk, hash, minSdk); + } + + private static void offerPendingUpdate(Activity activity) { + if (!(activity instanceof LaunchActivity) || activity.isFinishing() + || (Build.VERSION.SDK_INT >= 17 && activity.isDestroyed())) return; + final UpdateInfo update; + synchronized (TgWsProxyBootstrap.class) { + update = pendingUpdate; + if (update == null || promptedVersionCode == update.versionCode) return; + promptedVersionCode = update.versionCode; + } + try { + new AlertDialog.Builder(activity) + .setTitle(LocaleController.getString(R.string.AppUpdate)) + .setMessage("Telegram-WSP " + update.tag + + " is available. Android will request installation after APK verification.") + .setPositiveButton(LocaleController.getString(R.string.AppUpdateNow), + (dialog, which) -> downloadAndInstallInBackground(activity.getApplicationContext(), update)) + .setNeutralButton("Release notes", (dialog, which) -> openReleaseNotes(activity, update)) + .setNegativeButton(LocaleController.getString(R.string.Cancel), null) + .show(); + } catch (Throwable error) { + synchronized (TgWsProxyBootstrap.class) { + if (promptedVersionCode == update.versionCode) promptedVersionCode = -1L; } + FileLog.e("Telegram-WSP could not show update prompt: " + error); + } + } - UpdateInfo update = new UpdateInfo(remoteVersionCode, versionName, apkUrl, apkSha256); - offerUpdateWhenActivityReady(appContext, update, 0); + private static void openReleaseNotes(Context context, UpdateInfo update) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(RELEASES_URL + "/tag/" + update.tag)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); } catch (Throwable error) { - FileLog.e("Telegram-WSP update check failed: " + error); + FileLog.e("Telegram-WSP could not open release notes: " + error); + message(context, "Could not open release notes."); } } @@ -190,55 +320,17 @@ private static long getCurrentVersionCode(Context context) throws Exception { return packageInfo.versionCode; } - private static void offerUpdateWhenActivityReady( - Context appContext, - UpdateInfo update, - int attempt - ) { - Handler handler = ApplicationLoader.applicationHandler; - if (handler == null) { - handler = new Handler(Looper.getMainLooper()); - } - - Handler mainHandler = handler; - mainHandler.postDelayed(() -> { - if (LaunchActivity.instance == null) { - if (attempt < 30) { - offerUpdateWhenActivityReady(appContext, update, attempt + 1); - } - return; - } - - try { - new AlertDialog.Builder(LaunchActivity.instance) - .setTitle(LocaleController.getString(R.string.AppUpdate)) - .setMessage( - "Telegram-WSP " + update.versionName - + " is available. The APK will be verified before Android asks to install it." - ) - .setPositiveButton( - LocaleController.getString(R.string.AppUpdateNow), - (dialog, which) -> downloadAndInstallInBackground(appContext, update) - ) - .setNegativeButton(LocaleController.getString(R.string.Cancel), null) - .show(); - } catch (Throwable error) { - FileLog.e("Telegram-WSP could not show update prompt: " + error); - } - }, attempt == 0 ? 2500L : 1000L); - } - private static void downloadAndInstallInBackground(Context appContext, UpdateInfo update) { - Toast.makeText(appContext, "Telegram-WSP: downloading update...", Toast.LENGTH_SHORT).show(); + message(appContext, "Telegram-WSP: downloading update..."); Thread downloadThread = new Thread(() -> { try { File apk = getUpdateApkFile(appContext, update.versionCode); - if (!isVerifiedUpdateApk(appContext, apk, update.sha256)) { + if (!isVerifiedUpdateApk(appContext, apk, update)) { downloadUpdateApk(update.url, apk); } - if (!isVerifiedUpdateApk(appContext, apk, update.sha256)) { + if (!isVerifiedUpdateApk(appContext, apk, update)) { throw new SecurityException("Downloaded update APK did not pass verification"); } @@ -246,7 +338,13 @@ private static void downloadAndInstallInBackground(Context appContext, UpdateInf if (handler == null) { handler = new Handler(Looper.getMainLooper()); } - handler.post(() -> openUpdateInstaller(appContext, apk)); + handler.post(() -> { + if (isVerifiedUpdateApk(appContext, apk, update)) { + openUpdateInstaller(appContext, apk); + } else { + message(appContext, "Update verification failed; installation cancelled."); + } + }); } catch (Throwable error) { FileLog.e("Telegram-WSP update download failed: " + error); Handler handler = ApplicationLoader.applicationHandler; @@ -297,7 +395,7 @@ private static void downloadUpdateApk(String url, File destination) throws Excep throw new IllegalStateException("Update APK HTTP status " + status); } - int declaredLength = connection.getContentLength(); + long declaredLength = connection.getContentLengthLong(); if (declaredLength > UPDATE_APK_LIMIT_BYTES) { throw new IllegalStateException("Update APK exceeds size limit"); } @@ -317,8 +415,8 @@ private static void downloadUpdateApk(String url, File destination) throws Excep output.getFD().sync(); } - if (total <= 0L) { - throw new IllegalStateException("Downloaded update APK is empty"); + if (total <= 0L || (declaredLength >= 0L && total != declaredLength)) { + throw new IllegalStateException("Downloaded update APK is empty or truncated"); } if (destination.exists() && !destination.delete()) { @@ -329,25 +427,29 @@ private static void downloadUpdateApk(String url, File destination) throws Excep } } finally { connection.disconnect(); - if (partial.exists() && !destination.exists()) { - partial.delete(); - } + if (partial.exists()) partial.delete(); } } private static boolean isVerifiedUpdateApk( Context context, File apk, - String expectedSha256 + UpdateInfo update ) { try { if (!apk.isFile()) { return false; } - if (!expectedSha256.equalsIgnoreCase(sha256(apk))) { + if (apk.length() <= 0L || apk.length() > UPDATE_APK_LIMIT_BYTES + || !update.sha256.equalsIgnoreCase(sha256(apk))) { return false; } - return hasSameSigningCertificate(context, apk); + PackageInfo candidate = context.getPackageManager().getPackageArchiveInfo(apk.getAbsolutePath(), 0); + long version = candidate == null ? -1L + : (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + ? candidate.getLongVersionCode() : candidate.versionCode); + return version == update.versionCode && version > getCurrentVersionCode(context) + && hasSameSigningCertificate(context, apk); } catch (Throwable error) { FileLog.e("Telegram-WSP update verification failed: " + error); return false; @@ -530,14 +632,18 @@ private static boolean supportsArm64() { private static final class UpdateInfo { final long versionCode; final String versionName; + final String tag; final String url; final String sha256; + final int minimumSdk; - UpdateInfo(long versionCode, String versionName, String url, String sha256) { + UpdateInfo(long versionCode, String versionName, String tag, String url, String sha256, int minimumSdk) { this.versionCode = versionCode; this.versionName = versionName; + this.tag = tag; this.url = url; this.sha256 = sha256; + this.minimumSdk = minimumSdk; } } } From 7d4ad120278b33e236a71435f2778955071c6447 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:03:02 -0700 Subject: [PATCH 03/31] fix(updater): reject insecure and untrusted release redirects (#45) --- integration/telegram/TgWsProxyBootstrap.java | 47 ++++++++++++++------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/integration/telegram/TgWsProxyBootstrap.java b/integration/telegram/TgWsProxyBootstrap.java index da8653e..ba52b81 100644 --- a/integration/telegram/TgWsProxyBootstrap.java +++ b/integration/telegram/TgWsProxyBootstrap.java @@ -294,19 +294,40 @@ private static String readSmallHttpsResource(String url) throws Exception { } private static HttpURLConnection openHttpsConnection(String url) throws Exception { - URL parsed = new URL(url); - if (!"https".equalsIgnoreCase(parsed.getProtocol())) { - throw new IllegalArgumentException("Only HTTPS update URLs are allowed"); - } - - HttpURLConnection connection = (HttpURLConnection) parsed.openConnection(); - connection.setInstanceFollowRedirects(true); - connection.setConnectTimeout(NETWORK_CONNECT_TIMEOUT_MS); - connection.setReadTimeout(NETWORK_READ_TIMEOUT_MS); - connection.setUseCaches(false); - connection.setRequestProperty("Cache-Control", "no-cache"); - connection.setRequestProperty("User-Agent", "Telegram-WSP Android Updater"); - return connection; + URL current = new URL(url); + // Only GitHub release paths and GitHub's release asset CDN can serve updater bytes. + for (int redirects = 0; redirects <= 5; redirects++) { + if (!"https".equalsIgnoreCase(current.getProtocol()) + || current.getUserInfo() != null + || (current.getPort() != -1 && current.getPort() != 443)) { + throw new SecurityException("Update URL must use HTTPS on port 443"); + } + String host = current.getHost().toLowerCase(java.util.Locale.US); + if (!(("github.com".equals(host) + && current.getPath().startsWith("/Regstar2/telegram-wsp/releases/")) + || "release-assets.githubusercontent.com".equals(host))) { + throw new SecurityException("Update redirect leads to an untrusted host/path"); + } + HttpURLConnection connection = (HttpURLConnection) current.openConnection(); + connection.setInstanceFollowRedirects(false); + connection.setConnectTimeout(NETWORK_CONNECT_TIMEOUT_MS); + connection.setReadTimeout(NETWORK_READ_TIMEOUT_MS); + connection.setUseCaches(false); + connection.setRequestProperty("Cache-Control", "no-cache"); + connection.setRequestProperty("User-Agent", "Telegram-WSP Android Updater"); + int response = connection.getResponseCode(); + if (response != 301 && response != 302 && response != 303 + && response != 307 && response != 308) { + return connection; + } + String location = connection.getHeaderField("Location"); + connection.disconnect(); + if (location == null || location.isEmpty()) { + throw new SecurityException("Update redirect has no Location"); + } + current = new URL(current, location); + } + throw new SecurityException("Too many update redirects"); } private static long getCurrentVersionCode(Context context) throws Exception { From 0d1d33fd0a30e3d965a14b62220754f5fb977c8c Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:03:47 -0700 Subject: [PATCH 04/31] feat(updater): manual check and trusted browser fallback in proxy settings (#45) --- .../telegram/TgWsProxySettingsActivity.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/integration/telegram/TgWsProxySettingsActivity.java b/integration/telegram/TgWsProxySettingsActivity.java index a3d22c9..97c27f2 100644 --- a/integration/telegram/TgWsProxySettingsActivity.java +++ b/integration/telegram/TgWsProxySettingsActivity.java @@ -20,6 +20,7 @@ import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.messenger.TgWsProxyController; +import org.telegram.messenger.TgWsProxyBootstrap; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.BaseFragment; @@ -103,6 +104,21 @@ public void onItemClick(int id) { LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT )); + addShadow(context, content); + addHeader(context, content, tr(context, "ОБНОВЛЕНИЯ")); + + TextSettingsCell checkUpdates = actionCell(context, tr(context, "Проверить обновления Telegram-WSP")); + checkUpdates.setOnClickListener(v -> TgWsProxyBootstrap.checkForUpdates(context)); + content.addView(checkUpdates, LayoutHelper.createLinear( + LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT + )); + + TextSettingsCell releases = actionCell(context, tr(context, "Открыть релизы Telegram-WSP")); + releases.setOnClickListener(v -> TgWsProxyBootstrap.openReleases(context)); + content.addView(releases, LayoutHelper.createLinear( + LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT + )); + addShadow(context, content); addHeader(context, content, tr(context, "МАРШРУТИЗАЦИЯ")); @@ -768,6 +784,9 @@ private static String tr(Context context, String key) { private static String trEn(String key) { switch (key) { + case "ОБНОВЛЕНИЯ": return "UPDATES"; + case "Проверить обновления Telegram-WSP": return "Check for Telegram-WSP updates"; + case "Открыть релизы Telegram-WSP": return "Open Telegram-WSP releases"; case "МАРШРУТИЗАЦИЯ": return "ROUTING"; case "Мобильная сеть": return "Mobile network"; case "AWG и Worker используются только когда настроены и доступны.": return "AWG and Worker are used only when configured and available."; From 8e46a3d29a9ac41bdefd58427fadc44993cc5cd6 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:04:14 -0700 Subject: [PATCH 05/31] fix(release): derive manifest minimumSdk from validated Telegram Gradle (#45) --- .github/workflows/release.yml | 115 ++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f10ce4f..d717351 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -143,6 +143,121 @@ jobs: $commit = (git rev-parse HEAD).Trim() $download = "https://github.com/$($env:REPOSITORY)/releases/download/$tag/Telegram-WSP-release.apk" + # The release feed must report the SDK floor of the actual pinned Telegram build. + $sdkVersions = @() + foreach ($module in @('TMessagesProj', 'TMessagesProj_AppStandalone')) { + $gradle = Get-Content ".work/telegram/$module/build.gradle" -Raw + $matchesSdk = [regex]::Matches($gradle, '(?m)^\s*minSdkVersion\s+(\d+)\s* + + $latest = [ordered]@{ + versionName = [string]$upstream.telegramVersion + versionCode = $versionCode + telegramBuild = [int]$upstream.telegramBuild + revision = [int]$env:RELEASE_REVISION + tag = $tag + projectCommit = $commit + telegramCommit = [string]$upstream.pinnedCommit + coreCommit = [string]$core.pinnedCommit + apk = $download + apkSha256 = $apkSha + publishedAt = (Get-Date).ToUniversalTime().ToString('o') + minimumSdk = $sdkVersions[0] + packageId = 'org.telegram.messenger.web' + } + [IO.File]::WriteAllText('dist/latest.json', ($latest | ConvertTo-Json -Depth 4) + [Environment]::NewLine) + + $notes = @" + # Telegram-WSP $($upstream.telegramVersion) + + - Telegram upstream: $($upstream.telegramVersion) ($($upstream.telegramBuild)) + - Telegram commit: $($upstream.pinnedCommit) + - tgwsproxy-core commit: $($core.pinnedCommit) + - Android package: org.telegram.messenger.web + - APK SHA-256: $apkSha + - Release revision: $($env:RELEASE_REVISION) + + ## Telegram-WSP changes + + - Added the built-in proxy control screen in Telegram proxy settings. + - Added Auto / Cloudflare Proxy / WARP-AmneziaWG / Cloudflare Worker / Direct routing. + - Auto routing: CF Proxy → AWG → Worker → Direct on Wi-Fi; CF Proxy → AWG → Worker on mobile. + - Added Cloudflare domain update/check controls. + - Added WARP/AmneziaWG profile creation, import, export and deletion. + - Added independent Proxy Worker and Amnezia provisioning Worker pools. + - Added translations for all built-in locales of the pinned Telegram upstream. + + The APK is signed with the persistent Telegram-WSP release key. Exact source components used to reproduce this build are attached to the release. + "@ + [IO.File]::WriteAllText('dist/release-notes.md', $notes.Trim() + [Environment]::NewLine) + + $checksumFiles = @( + Get-ChildItem 'dist' -File | + Where-Object { $_.Name -notin @('SHA256SUMS.txt', 'release-notes.md') } + Get-ChildItem 'dist/source' -File + ) + $checksumLines = foreach ($file in $checksumFiles) { + $hash = (Get-FileHash $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash $($file.Name)" + } + [IO.File]::WriteAllLines('dist/SHA256SUMS.txt', $checksumLines) + + "tag=$tag" >> $env:GITHUB_OUTPUT + "commit=$commit" >> $env:GITHUB_OUTPUT + "title=Telegram-WSP $($upstream.telegramVersion) (WSP $($env:RELEASE_REVISION))" >> $env:GITHUB_OUTPUT + + - name: Publish GitHub Release + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.metadata.outputs.tag }} + RELEASE_COMMIT: ${{ steps.metadata.outputs.commit }} + RELEASE_TITLE: ${{ steps.metadata.outputs.title }} + PRERELEASE: ${{ inputs.prerelease }} + run: | + $assets = @( + (Resolve-Path 'dist/Telegram-WSP-release.apk').Path + (Resolve-Path 'dist/latest.json').Path + (Resolve-Path 'dist/SHA256SUMS.txt').Path + ) + $assets += @(Get-ChildItem 'dist/source' -File | ForEach-Object { $_.FullName }) + + gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY *> $null + $exists = $LASTEXITCODE -eq 0 + + if ($exists) { + gh release upload $env:RELEASE_TAG @assets --clobber --repo $env:GITHUB_REPOSITORY + if ($LASTEXITCODE -ne 0) { throw 'gh release upload failed.' } + gh release edit $env:RELEASE_TAG --title $env:RELEASE_TITLE --notes-file 'dist/release-notes.md' --repo $env:GITHUB_REPOSITORY + if ($LASTEXITCODE -ne 0) { throw 'gh release edit failed.' } + } else { + $arguments = @('release', 'create', $env:RELEASE_TAG) + $assets + @( + '--title', $env:RELEASE_TITLE, + '--notes-file', 'dist/release-notes.md', + '--target', $env:RELEASE_COMMIT, + '--repo', $env:GITHUB_REPOSITORY + ) + if ($env:PRERELEASE -eq 'true') { $arguments += '--prerelease' } + & gh @arguments + if ($LASTEXITCODE -ne 0) { throw 'gh release create failed.' } + } + + - name: Release summary + shell: pwsh + env: + RELEASE_TAG: ${{ steps.metadata.outputs.tag }} + run: | + "## Telegram-WSP release" >> $env:GITHUB_STEP_SUMMARY + "- Tag: ``$env:RELEASE_TAG``" >> $env:GITHUB_STEP_SUMMARY + "- APK: ``Telegram-WSP-release.apk``" >> $env:GITHUB_STEP_SUMMARY + "- Update feed: ``https://github.com/$env:GITHUB_REPOSITORY/releases/latest/download/latest.json``" >> $env:GITHUB_STEP_SUMMARY +) + if ($matchesSdk.Count -ne 1) { throw "Ambiguous upstream minimum SDK in $module." } + $sdkVersions += [int]$matchesSdk[0].Groups[1].Value + } + if ($sdkVersions[0] -ne $sdkVersions[1] -or $sdkVersions[0] -notin @(21, 23)) { + throw "Unsupported or inconsistent minimum SDK: $($sdkVersions -join ', ')." + } + $latest = [ordered]@{ versionName = [string]$upstream.telegramVersion versionCode = $versionCode From 13dc2044105a28bbaacba3a5ccdf2c59bbdc30bd Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:04:25 -0700 Subject: [PATCH 06/31] test(release): validate packaged APK SDK against upstream floor (#45) --- scripts/build-release.ps1 | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index a939480..35421a9 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -286,6 +286,49 @@ try { throw "Release APK versionCode does not match WSP revision. Expected $expectedVersionCode." } + # An accidental SDK downgrade in the overlay would publish a false compatibility claim. + $standaloneGradle = Get-Content (Join-Path $telegramPath 'TMessagesProj_AppStandalone/build.gradle') -Raw + $sdkMatches = [regex]::Matches($standaloneGradle, '(?m)^\s*minSdkVersion\s+(\d+)\s* + $badgingLines | + Select-String -Pattern 'package:|application-label:|application-icon' | + Select-Object -First 12 | + ForEach-Object { Write-Host $_.Line } + + Write-Host 'APK signature:' + & $apkSigner verify --verbose --print-certs $outputPath + if ($LASTEXITCODE -ne 0) { + throw 'APK signature verification failed.' + } + + $hash = Get-FileHash $outputPath -Algorithm SHA256 + $apkItem = Get-Item $outputPath + + Write-Host '' + Write-Host 'Telegram-WSP release ready.' + Write-Host "APK: $($apkItem.FullName)" + Write-Host "Size: $($apkItem.Length) bytes" + Write-Host "SHA256: $($hash.Hash)" + Write-Host "Keystore: $keystorePath" + Write-Host "Alias: $KeyAlias" + Write-Host "Release revision: $ReleaseRevision" + Write-Host "Android versionCode: $expectedVersionCode" +} +finally { + $password = $null + Remove-Item Env:TELEGRAM_WSP_KEYSTORE_PASSWORD -ErrorAction SilentlyContinue + Remove-Item Env:TELEGRAM_WSP_KEYSTORE -ErrorAction SilentlyContinue + Remove-Item Env:TELEGRAM_WSP_KEY_ALIAS -ErrorAction SilentlyContinue + Remove-Item Env:TELEGRAM_WSP_RELEASE_REVISION -ErrorAction SilentlyContinue +} +) + if ($sdkMatches.Count -ne 1) { + throw 'Ambiguous minSdkVersion in prepared standalone build.' + } + $minimumSdk = [int]$sdkMatches[0].Groups[1].Value + if ($badgingText -notmatch ("sdkVersion:'" + $minimumSdk + "'")) { + throw "Release APK SDK floor does not match upstream: expected API $minimumSdk." + } + Write-Host 'APK metadata:' $badgingLines | Select-String -Pattern 'package:|application-label:|application-icon' | From 8cde646a9fc448c29ac1f8fa9e213bf8a539e43f Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:06:55 -0700 Subject: [PATCH 07/31] fix(updater): persist pending offer across process restart without 12h loss (#45) --- integration/telegram/TgWsProxyBootstrap.java | 38 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/integration/telegram/TgWsProxyBootstrap.java b/integration/telegram/TgWsProxyBootstrap.java index ba52b81..5f3b85a 100644 --- a/integration/telegram/TgWsProxyBootstrap.java +++ b/integration/telegram/TgWsProxyBootstrap.java @@ -47,6 +47,7 @@ public final class TgWsProxyBootstrap { private static final String KEY_UPDATE_ENABLED = "update_enabled"; private static final String KEY_UPDATE_LAST_SUCCESS = "update_last_success"; private static final String KEY_UPDATE_LAST_ATTEMPT = "update_last_attempt"; + private static final String KEY_UPDATE_PENDING_MANIFEST = "update_pending_manifest"; private static final String HOST = "127.0.0.1"; private static final int PORT = 1443; private static final String LEGACY_RUNTIME_CONFIG = "@mtproto_worker_preconnect=1"; @@ -107,9 +108,34 @@ public static void start(Context context) { } }); } + restorePendingUpdate(appContext); requestUpdateCheck(appContext, false); } + private static void restorePendingUpdate(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE); + long now = System.currentTimeMillis(); + if (!isCoolingDown(now, prefs.getLong(KEY_UPDATE_LAST_SUCCESS, 0L), + UPDATE_CHECK_INTERVAL_MS)) { + prefs.edit().remove(KEY_UPDATE_PENDING_MANIFEST).apply(); + return; + } + String saved = prefs.getString(KEY_UPDATE_PENDING_MANIFEST, null); + if (saved == null) return; + try { + UpdateInfo update = validateUpdate(new JSONObject(saved)); + if (update.versionCode > getCurrentVersionCode(context) + && update.minimumSdk <= Build.VERSION.SDK_INT) { + synchronized (TgWsProxyBootstrap.class) { pendingUpdate = update; } + } else { + prefs.edit().remove(KEY_UPDATE_PENDING_MANIFEST).apply(); + } + } catch (Throwable error) { + prefs.edit().remove(KEY_UPDATE_PENDING_MANIFEST).apply(); + FileLog.e("Telegram-WSP saved update manifest invalid: " + error); + } + } + private static void startInBackground(Context context) { try { TgWsProxyController.start(context); @@ -173,7 +199,15 @@ private static void requestUpdateCheck(Context context, boolean manual) { UpdateInfo update = validateUpdate(metadata); long remoteVersionCode = update.versionCode; long currentVersionCode = getCurrentVersionCode(context); - prefs.edit().putLong(KEY_UPDATE_LAST_SUCCESS, System.currentTimeMillis()).apply(); + SharedPreferences.Editor success = prefs.edit() + .putLong(KEY_UPDATE_LAST_SUCCESS, System.currentTimeMillis()); + if (remoteVersionCode > currentVersionCode + && update.minimumSdk <= Build.VERSION.SDK_INT) { + success.putString(KEY_UPDATE_PENDING_MANIFEST, metadata.toString()); + } else { + success.remove(KEY_UPDATE_PENDING_MANIFEST); + } + success.apply(); if (remoteVersionCode <= currentVersionCode) { synchronized (TgWsProxyBootstrap.class) { pendingUpdate = null; } if (manual) message(context, "Telegram-WSP is up to date."); @@ -416,7 +450,7 @@ private static void downloadUpdateApk(String url, File destination) throws Excep throw new IllegalStateException("Update APK HTTP status " + status); } - long declaredLength = connection.getContentLengthLong(); + long declaredLength = connection.getContentLength(); if (declaredLength > UPDATE_APK_LIMIT_BYTES) { throw new IllegalStateException("Update APK exceeds size limit"); } From 7c8328c9bd786addc2d61308ccaccbdbdf2c82fb Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:08:07 -0700 Subject: [PATCH 08/31] test(ci): pinned/new Telegram matrix, fail-closed SDK and APK compile (#45) --- .github/workflows/trusted-ci.yml | 52 ++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/.github/workflows/trusted-ci.yml b/.github/workflows/trusted-ci.yml index 4bd2a98..271ae3f 100644 --- a/.github/workflows/trusted-ci.yml +++ b/.github/workflows/trusted-ci.yml @@ -32,9 +32,13 @@ jobs: run: ./scripts/ci.ps1 integration: - name: Reproduce Telegram overlay + name: Overlay and APK (${{ matrix.telegram }}) runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + telegram: [pinned, telegram-13-0-3] steps: - name: Checkout @@ -42,6 +46,16 @@ jobs: with: persist-credentials: false + - name: Select Telegram 13.0.3 regression fixture + if: matrix.telegram == 'telegram-13-0-3' + shell: pwsh + run: | + $upstream = Get-Content config/upstream.json -Raw | ConvertFrom-Json + $upstream.pinnedCommit = '0aae959806c57be8ad457980f2a5c6277ff18635' + $upstream.telegramVersion = '13.0.3' + $upstream.telegramBuild = 7183 + $upstream | ConvertTo-Json -Depth 4 | Set-Content config/upstream.json + - name: Set up Java uses: actions/setup-java@v5 with: @@ -59,7 +73,7 @@ jobs: - name: Install Android SDK components shell: bash - run: sdkmanager "platforms;android-35" "ndk;27.2.12479018" + run: sdkmanager "platforms;android-35" "platforms;android-36" "ndk;27.2.12479018" - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 @@ -73,3 +87,35 @@ jobs: - name: Re-run repository checks on prepared tree shell: pwsh run: ./scripts/ci.ps1 + + - name: Reject incompatible SDK anchor + shell: pwsh + run: | + $path = '.work/telegram/TMessagesProj/build.gradle' + $original = Get-Content $path -Raw + $pattern = '(?m)^ minSdkVersion (21|23)$' + $modified = [regex]::Replace($original, $pattern, ' minSdkVersion 42') + if ($modified -eq $original) { throw 'Test fixture could not change minSdk.' } + try { + Set-Content $path $modified -NoNewline + $rejected = $false + try { + ./scripts/apply-integration.ps1 + } catch { + if ($_.Exception.Message -notmatch 'minSdk/targetSdk contract is unsupported') { + throw "Unexpected rejection: $($_.Exception.Message)" + } + $rejected = $true + } + if (-not $rejected) { throw 'Unsupported API 42 integration was accepted.' } + } finally { + Set-Content $path $original -NoNewline + } + + - name: Compile Telegram-WSP prototype APK + shell: pwsh + env: + # Public Telegram upstream fallback values; compile-only CI, no release signing. + TELEGRAM_API_ID: '4' + TELEGRAM_API_HASH: '014b35b6184100b085b0d0572f9b5103' + run: ./scripts/build-apk.ps1 -SkipPrepare From 1ee17768a23f85972aa2ab37f40e522b01835742 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:09:38 -0700 Subject: [PATCH 09/31] ci(upstream): compile candidate before updating main pin (#45) --- .github/workflows/upstream-sync.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml index 3151380..5891f97 100644 --- a/.github/workflows/upstream-sync.yml +++ b/.github/workflows/upstream-sync.yml @@ -105,6 +105,14 @@ jobs: shell: pwsh run: ./scripts/ci.ps1 + - name: Compile candidate APK before changing main + if: steps.upstream.outputs.changed == 'true' + shell: pwsh + env: + TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }} + TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }} + run: ./scripts/build-apk.ps1 -SkipPrepare + - name: Commit validated upstream pin if: steps.upstream.outputs.changed == 'true' id: commit From 50de13981d1a0dedbe2aaf4a06de199f23f52538 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:10:42 -0700 Subject: [PATCH 10/31] docs: correct stable release, API 21/23 and updater recovery paths (#45) --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cb8e4e1..cef8fa9 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,11 @@ Telegram-WSP использует собственные название, packa Стадия: **MVP**. -Текущий публичный релиз: **[v12.10.5-wsp.2](../../releases/tag/v12.10.5-wsp.2)**. +Текущий публичный релиз: **[v12.10.6-wsp.2](../../releases/tag/v12.10.6-wsp.2)**. | Компонент | Текущее состояние | |---|---| -| Telegram upstream | 12.10.5, build 7105 | +| Telegram upstream | 12.10.6, build 7112 | | Telegram commit | `dc780e81ed1261c369c27870e8e0999a1eb0b600` | | tgwsproxy-core commit | `8016d1b56210e8edc69da9d637416b70b30971a8` | | Android package | `org.telegram.messenger.web` | @@ -50,7 +50,7 @@ Telegram-WSP использует собственные название, packa | Автообновление приложения | Реализовано через GitHub Releases | | Upstream sync | Автоматическая проверка DrKLO/Telegram с integration gates | -Релиз `v12.10.5-wsp.2` включает новый экран управления маршрутами, поддержку Cloudflare-доменов, WARP/AmneziaWG-профиля и Worker pools. Production APK публикуется вместе с контрольными суммами и точными исходными компонентами сборки. +Релиз `v12.10.6-wsp.2` включает новый экран управления маршрутами, поддержку Cloudflare-доменов, WARP/AmneziaWG-профиля и Worker pools. Production APK публикуется вместе с контрольными суммами и точными исходными компонентами сборки. ## Возможности @@ -83,7 +83,7 @@ Telegram-WSP использует собственные название, packa Для готового APK: -- Android **5.0 / API 21** или новее; +- Для последней опубликованной сборки Telegram 12.10.6: Android **5.0 / API 21** или новее. Telegram upstream 13.0.3 требует **Android 6.0 / API 23**; будущие релизы на его основе не должны заявлять поддержку Android 5.0; - доступ в интернет; - разрешение Android на установку APK не из магазина. @@ -221,9 +221,11 @@ Telegram-WSP проверяет: https://github.com/Regstar2/telegram-wsp/releases/latest/download/latest.json ``` -не чаще одного раза в 12 часов. +автоматически не чаще одного раза в 12 часов **после успешной проверки**. После сетевой ошибки повторная автоматическая попытка допускается через 10 минут; кнопка ручной проверки не зависит от этого интервала. -Если `versionCode` новой сборки выше установленного, приложение предлагает обновление. После согласия APK скачивается, проверяется и передаётся системному installer. +В интерфейсе «Встроенный прокси» доступны «Проверить обновления Telegram-WSP» и «Открыть релизы Telegram-WSP». Последний пункт открывает официальный GitHub Releases в браузере, если прямой HTTPS-доступ updater отсутствует: локальный MTProto frontend не является HTTP(S)-прокси. Автоматическая проверка возобновляется при возврате Telegram на экран. + +Если `versionCode` новой сборки выше установленного, приложение предлагает обновление (с точным WSP release tag и ссылкой на release notes). После согласия APK скачивается, проверяется и передаётся системному installer. Нумерация Telegram-WSP учитывает как Telegram build, так и WSP revision: From 219c2fffb250e88fbcd65c125f67f8667c27f9df Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:11:01 -0700 Subject: [PATCH 11/31] test(ci): protect updater lifecycle, stable metadata and SDK invariants (#45) --- scripts/ci.ps1 | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/scripts/ci.ps1 b/scripts/ci.ps1 index c74b8df..afab448 100644 --- a/scripts/ci.ps1 +++ b/scripts/ci.ps1 @@ -353,6 +353,31 @@ if ($applyScript -notmatch 'TELEGRAM_WSP_RELEASE_REVISION' -or $applyScript -not throw 'Integration script must encode the Telegram-WSP release revision into Android versionCode.' } +$updater = Get-Content (Join-Path $root 'integration/telegram/TgWsProxyBootstrap.java') -Raw +$settings = Get-Content (Join-Path $root 'integration/telegram/TgWsProxySettingsActivity.java') -Raw +if ($updater -match 'KEY_UPDATE_LAST_CHECK' -or + $updater -notmatch 'KEY_UPDATE_LAST_SUCCESS' -or + $updater -notmatch 'KEY_UPDATE_LAST_ATTEMPT' -or + $updater -notmatch 'UPDATE_RETRY_DELAY_MS') { + throw 'Update check must throttle successful checks and failed attempts separately.' +} +if ($updater -notmatch 'onActivityResumed' -or + $updater -notmatch 'KEY_UPDATE_PENDING_MANIFEST' -or + $settings -notmatch 'TgWsProxyBootstrap\.checkForUpdates' -or + $settings -notmatch 'TgWsProxyBootstrap\.openReleases') { + throw 'Updater must support foreground checks, pending offers and manual/browser actions.' +} +if ($updater -notmatch 'validateUpdate\(JSONObject' -or + $updater -notmatch 'hasSameSigningCertificate' -or + $updater -notmatch 'getPackageArchiveInfo' -or + $updater -notmatch 'version > getCurrentVersionCode' -or + $updater -notmatch 'release-assets\.githubusercontent\.com') { + throw 'Updater must validate stable metadata, new APK version and signing/redirect boundaries.' +} +if ($releaseWorkflow -notmatch 'minimumSdk = \$sdkVersions\[0\]') { + throw 'Update feed must describe the actual Telegram minimum SDK.' +} + $releaseBuildScript = Get-Content (Join-Path $root 'scripts/build-release.ps1') -Raw if ($releaseBuildScript -notmatch 'build-apk\.ps1' -or $releaseBuildScript -notmatch 'Full\s*=\s*\$true') { throw 'Release build script must delegate to the full afatStandalone build.' From c84ffd4080ca3287ac765dcc3e52326b76a07e9b Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:12:17 -0700 Subject: [PATCH 12/31] fix(ci): repair complete release metadata SDK validation block (#45) --- .github/workflows/release.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d717351..5d97c3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -147,7 +147,13 @@ jobs: $sdkVersions = @() foreach ($module in @('TMessagesProj', 'TMessagesProj_AppStandalone')) { $gradle = Get-Content ".work/telegram/$module/build.gradle" -Raw - $matchesSdk = [regex]::Matches($gradle, '(?m)^\s*minSdkVersion\s+(\d+)\s* + $matchesSdk = [regex]::Matches($gradle, '(?m)^[ \t]*minSdkVersion[ \t]+(\d+)[ \t]*\r?\n') + if ($matchesSdk.Count -ne 1) { throw "Ambiguous upstream minimum SDK in $module." } + $sdkVersions += [int]$matchesSdk[0].Groups[1].Value + } + if ($sdkVersions[0] -ne $sdkVersions[1] -or $sdkVersions[0] -notin @(21, 23)) { + throw "Unsupported or inconsistent minimum SDK: $($sdkVersions -join ', ')." + } $latest = [ordered]@{ versionName = [string]$upstream.telegramVersion From 238266a4aef79a6c9c85145f8656f28523dd9f31 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:12:29 -0700 Subject: [PATCH 13/31] fix(release): complete APK minimum SDK check (#45) --- scripts/build-release.ps1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index 35421a9..963b26a 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -288,7 +288,16 @@ try { # An accidental SDK downgrade in the overlay would publish a false compatibility claim. $standaloneGradle = Get-Content (Join-Path $telegramPath 'TMessagesProj_AppStandalone/build.gradle') -Raw - $sdkMatches = [regex]::Matches($standaloneGradle, '(?m)^\s*minSdkVersion\s+(\d+)\s* + $sdkMatches = [regex]::Matches($standaloneGradle, '(?m)^[ \t]*minSdkVersion[ \t]+(\d+)[ \t]*\r?\n') + if ($sdkMatches.Count -ne 1) { + throw 'Ambiguous minSdkVersion in prepared standalone build.' + } + $minimumSdk = [int]$sdkMatches[0].Groups[1].Value + if ($badgingText -notmatch ("sdkVersion:'" + $minimumSdk + "'")) { + throw "Release APK SDK floor does not match upstream: expected API $minimumSdk." + } + + Write-Host 'APK metadata:' $badgingLines | Select-String -Pattern 'package:|application-label:|application-icon' | Select-Object -First 12 | From 3caa34df4b4ee81bbc666ac521928c15d42a23b7 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:13:53 -0700 Subject: [PATCH 14/31] fix(release): remove accidental duplicate tail; validate SDK cleanly (#45) --- .github/workflows/release.yml | 120 ++-------------------------------- 1 file changed, 5 insertions(+), 115 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d97c3c..44e1fc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -142,17 +142,16 @@ jobs: $apkSha = (Get-FileHash $apk -Algorithm SHA256).Hash.ToLowerInvariant() $commit = (git rev-parse HEAD).Trim() $download = "https://github.com/$($env:REPOSITORY)/releases/download/$tag/Telegram-WSP-release.apk" - - # The release feed must report the SDK floor of the actual pinned Telegram build. + # Feed SDK is derived from the actual prepared Telegram modules. $sdkVersions = @() foreach ($module in @('TMessagesProj', 'TMessagesProj_AppStandalone')) { $gradle = Get-Content ".work/telegram/$module/build.gradle" -Raw - $matchesSdk = [regex]::Matches($gradle, '(?m)^[ \t]*minSdkVersion[ \t]+(\d+)[ \t]*\r?\n') - if ($matchesSdk.Count -ne 1) { throw "Ambiguous upstream minimum SDK in $module." } - $sdkVersions += [int]$matchesSdk[0].Groups[1].Value + $sdkMatches = [regex]::Matches($gradle, '(?m)^[ \t]*minSdkVersion[ \t]+(\d+)[ \t]*\r?\n') + if ($sdkMatches.Count -ne 1) { throw "Ambiguous minSdkVersion in $module." } + $sdkVersions += [int]$sdkMatches[0].Groups[1].Value } if ($sdkVersions[0] -ne $sdkVersions[1] -or $sdkVersions[0] -notin @(21, 23)) { - throw "Unsupported or inconsistent minimum SDK: $($sdkVersions -join ', ')." + throw "Unsupported or inconsistent minSdkVersion: $($sdkVersions -join ', ')." } $latest = [ordered]@{ @@ -256,112 +255,3 @@ jobs: "- Tag: ``$env:RELEASE_TAG``" >> $env:GITHUB_STEP_SUMMARY "- APK: ``Telegram-WSP-release.apk``" >> $env:GITHUB_STEP_SUMMARY "- Update feed: ``https://github.com/$env:GITHUB_REPOSITORY/releases/latest/download/latest.json``" >> $env:GITHUB_STEP_SUMMARY -) - if ($matchesSdk.Count -ne 1) { throw "Ambiguous upstream minimum SDK in $module." } - $sdkVersions += [int]$matchesSdk[0].Groups[1].Value - } - if ($sdkVersions[0] -ne $sdkVersions[1] -or $sdkVersions[0] -notin @(21, 23)) { - throw "Unsupported or inconsistent minimum SDK: $($sdkVersions -join ', ')." - } - - $latest = [ordered]@{ - versionName = [string]$upstream.telegramVersion - versionCode = $versionCode - telegramBuild = [int]$upstream.telegramBuild - revision = [int]$env:RELEASE_REVISION - tag = $tag - projectCommit = $commit - telegramCommit = [string]$upstream.pinnedCommit - coreCommit = [string]$core.pinnedCommit - apk = $download - apkSha256 = $apkSha - publishedAt = (Get-Date).ToUniversalTime().ToString('o') - minimumSdk = 21 - packageId = 'org.telegram.messenger.web' - } - [IO.File]::WriteAllText('dist/latest.json', ($latest | ConvertTo-Json -Depth 4) + [Environment]::NewLine) - - $notes = @" - # Telegram-WSP $($upstream.telegramVersion) - - - Telegram upstream: $($upstream.telegramVersion) ($($upstream.telegramBuild)) - - Telegram commit: $($upstream.pinnedCommit) - - tgwsproxy-core commit: $($core.pinnedCommit) - - Android package: org.telegram.messenger.web - - APK SHA-256: $apkSha - - Release revision: $($env:RELEASE_REVISION) - - ## Telegram-WSP changes - - - Added the built-in proxy control screen in Telegram proxy settings. - - Added Auto / Cloudflare Proxy / WARP-AmneziaWG / Cloudflare Worker / Direct routing. - - Auto routing: CF Proxy → AWG → Worker → Direct on Wi-Fi; CF Proxy → AWG → Worker on mobile. - - Added Cloudflare domain update/check controls. - - Added WARP/AmneziaWG profile creation, import, export and deletion. - - Added independent Proxy Worker and Amnezia provisioning Worker pools. - - Added translations for all built-in locales of the pinned Telegram upstream. - - The APK is signed with the persistent Telegram-WSP release key. Exact source components used to reproduce this build are attached to the release. - "@ - [IO.File]::WriteAllText('dist/release-notes.md', $notes.Trim() + [Environment]::NewLine) - - $checksumFiles = @( - Get-ChildItem 'dist' -File | - Where-Object { $_.Name -notin @('SHA256SUMS.txt', 'release-notes.md') } - Get-ChildItem 'dist/source' -File - ) - $checksumLines = foreach ($file in $checksumFiles) { - $hash = (Get-FileHash $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant() - "$hash $($file.Name)" - } - [IO.File]::WriteAllLines('dist/SHA256SUMS.txt', $checksumLines) - - "tag=$tag" >> $env:GITHUB_OUTPUT - "commit=$commit" >> $env:GITHUB_OUTPUT - "title=Telegram-WSP $($upstream.telegramVersion) (WSP $($env:RELEASE_REVISION))" >> $env:GITHUB_OUTPUT - - - name: Publish GitHub Release - shell: pwsh - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ steps.metadata.outputs.tag }} - RELEASE_COMMIT: ${{ steps.metadata.outputs.commit }} - RELEASE_TITLE: ${{ steps.metadata.outputs.title }} - PRERELEASE: ${{ inputs.prerelease }} - run: | - $assets = @( - (Resolve-Path 'dist/Telegram-WSP-release.apk').Path - (Resolve-Path 'dist/latest.json').Path - (Resolve-Path 'dist/SHA256SUMS.txt').Path - ) - $assets += @(Get-ChildItem 'dist/source' -File | ForEach-Object { $_.FullName }) - - gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY *> $null - $exists = $LASTEXITCODE -eq 0 - - if ($exists) { - gh release upload $env:RELEASE_TAG @assets --clobber --repo $env:GITHUB_REPOSITORY - if ($LASTEXITCODE -ne 0) { throw 'gh release upload failed.' } - gh release edit $env:RELEASE_TAG --title $env:RELEASE_TITLE --notes-file 'dist/release-notes.md' --repo $env:GITHUB_REPOSITORY - if ($LASTEXITCODE -ne 0) { throw 'gh release edit failed.' } - } else { - $arguments = @('release', 'create', $env:RELEASE_TAG) + $assets + @( - '--title', $env:RELEASE_TITLE, - '--notes-file', 'dist/release-notes.md', - '--target', $env:RELEASE_COMMIT, - '--repo', $env:GITHUB_REPOSITORY - ) - if ($env:PRERELEASE -eq 'true') { $arguments += '--prerelease' } - & gh @arguments - if ($LASTEXITCODE -ne 0) { throw 'gh release create failed.' } - } - - - name: Release summary - shell: pwsh - env: - RELEASE_TAG: ${{ steps.metadata.outputs.tag }} - run: | - "## Telegram-WSP release" >> $env:GITHUB_STEP_SUMMARY - "- Tag: ``$env:RELEASE_TAG``" >> $env:GITHUB_STEP_SUMMARY - "- APK: ``Telegram-WSP-release.apk``" >> $env:GITHUB_STEP_SUMMARY - "- Update feed: ``https://github.com/$env:GITHUB_REPOSITORY/releases/latest/download/latest.json``" >> $env:GITHUB_STEP_SUMMARY From f2f523424d17038512aef545a46725f4907ba155 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:14:07 -0700 Subject: [PATCH 15/31] fix(release): remove accidental duplicate tail and preserve APK SDK assertion (#45) --- scripts/build-release.ps1 | 50 +++------------------------------------ 1 file changed, 3 insertions(+), 47 deletions(-) diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index 963b26a..42aeefb 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -286,58 +286,14 @@ try { throw "Release APK versionCode does not match WSP revision. Expected $expectedVersionCode." } - # An accidental SDK downgrade in the overlay would publish a false compatibility claim. + # The produced APK must not silently lower the pinned Telegram SDK floor. $standaloneGradle = Get-Content (Join-Path $telegramPath 'TMessagesProj_AppStandalone/build.gradle') -Raw $sdkMatches = [regex]::Matches($standaloneGradle, '(?m)^[ \t]*minSdkVersion[ \t]+(\d+)[ \t]*\r?\n') - if ($sdkMatches.Count -ne 1) { - throw 'Ambiguous minSdkVersion in prepared standalone build.' - } - $minimumSdk = [int]$sdkMatches[0].Groups[1].Value - if ($badgingText -notmatch ("sdkVersion:'" + $minimumSdk + "'")) { - throw "Release APK SDK floor does not match upstream: expected API $minimumSdk." - } - - Write-Host 'APK metadata:' - $badgingLines | - Select-String -Pattern 'package:|application-label:|application-icon' | - Select-Object -First 12 | - ForEach-Object { Write-Host $_.Line } - - Write-Host 'APK signature:' - & $apkSigner verify --verbose --print-certs $outputPath - if ($LASTEXITCODE -ne 0) { - throw 'APK signature verification failed.' - } - - $hash = Get-FileHash $outputPath -Algorithm SHA256 - $apkItem = Get-Item $outputPath - - Write-Host '' - Write-Host 'Telegram-WSP release ready.' - Write-Host "APK: $($apkItem.FullName)" - Write-Host "Size: $($apkItem.Length) bytes" - Write-Host "SHA256: $($hash.Hash)" - Write-Host "Keystore: $keystorePath" - Write-Host "Alias: $KeyAlias" - Write-Host "Release revision: $ReleaseRevision" - Write-Host "Android versionCode: $expectedVersionCode" -} -finally { - $password = $null - Remove-Item Env:TELEGRAM_WSP_KEYSTORE_PASSWORD -ErrorAction SilentlyContinue - Remove-Item Env:TELEGRAM_WSP_KEYSTORE -ErrorAction SilentlyContinue - Remove-Item Env:TELEGRAM_WSP_KEY_ALIAS -ErrorAction SilentlyContinue - Remove-Item Env:TELEGRAM_WSP_RELEASE_REVISION -ErrorAction SilentlyContinue -} -) - if ($sdkMatches.Count -ne 1) { - throw 'Ambiguous minSdkVersion in prepared standalone build.' - } + if ($sdkMatches.Count -ne 1) { throw 'Ambiguous standalone minSdkVersion.' } $minimumSdk = [int]$sdkMatches[0].Groups[1].Value if ($badgingText -notmatch ("sdkVersion:'" + $minimumSdk + "'")) { - throw "Release APK SDK floor does not match upstream: expected API $minimumSdk." + throw "APK minimum SDK does not match upstream: expected API $minimumSdk." } - Write-Host 'APK metadata:' $badgingLines | Select-String -Pattern 'package:|application-label:|application-icon' | From 6696deadf2c574f5adcfdd15d6b77c0587199dcb Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:14:38 -0700 Subject: [PATCH 16/31] docs: align current release source pin with upstream config (#45) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cef8fa9..f5abb0a 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Telegram-WSP использует собственные название, packa | Компонент | Текущее состояние | |---|---| | Telegram upstream | 12.10.6, build 7112 | -| Telegram commit | `dc780e81ed1261c369c27870e8e0999a1eb0b600` | +| Telegram commit | `f2908b14133bbffbf7ab04f641ecb5bfaf533242` | | tgwsproxy-core commit | `8016d1b56210e8edc69da9d637416b70b30971a8` | | Android package | `org.telegram.messenger.web` | | Встроенный TgWsProxy | Работает через локальный listener | From 183afab85c32fe6b799cb7c2dd3837dbef63c014 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:16:59 -0700 Subject: [PATCH 17/31] docs(updater): document Android HTTP routing, trust policy and acceptance limits (#45) --- integration/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/integration/README.md b/integration/README.md index 8de812f..c666b6d 100644 --- a/integration/README.md +++ b/integration/README.md @@ -162,6 +162,15 @@ TELEGRAM_API_HASH= `afatDebug` не используется: соответствующий library build включает `DEBUG_VERSION=true` и `DEBUG_PRIVATE_VERSION=true`. +## Android updater и сетевой маршрут + +- `TgWsProxyBootstrap` проверяет `latest.json` и скачивает APK стандартным `HttpURLConnection` поверх HTTPS. Это обычный Android/system HTTP(S) route; он **не использует** локальный MTProto frontend `127.0.0.1:1443` как HTTP proxy. +- Разрешены GitHub Releases и GitHub release asset CDN; прочие HTTPS redirect destinations блокируются, а SHA-256, package, signing certificate и `versionCode` повторно проверяются перед installer. +- Если GitHub недоступен в текущей сети, ручная проверка сообщает ошибку. Пункт «Открыть релизы Telegram-WSP» переводит на официальные GitHub Releases через установленный браузер, который может использовать собственную поддерживаемую сетевую маршрутизацию. Успешность этого обходного пути зависит от среды пользователя. +- Автоматическая проверка: интервал 12 часов после успешно проверенного feed и минимум 10 минут после попытки; ручная проверка доступна в настройках встроенного прокси. Найденный manifest хранится локально на время интервала и повторно валидируется после перезапуска процесса. +- Для Telegram 12.10.6 принят minSdk 21, для Telegram 13.0.3 — minSdk 23. Overlay не понижает upstream SDK. Модель совместимости release проверяется по `aapt`, но upgrade и сохранность данных требуют Android device acceptance. +- Эти проверки в `scripts/ci.ps1` защищают контракт исходников. Они не заменяют instrumented UI/network tests, подписанную release-сборку и установку поверх предыдущего release. + ## Release signing Постоянный release key создаётся один раз: From 5b4c8a24fd9ad5e021a1d95ba3a6eee96bd28bbb Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:18:24 -0700 Subject: [PATCH 18/31] fix(android): bridge core settings to standalone updater without reverse dependency (#45) --- integration/telegram/TgWsProxyController.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/integration/telegram/TgWsProxyController.java b/integration/telegram/TgWsProxyController.java index 88492f1..f0569a0 100644 --- a/integration/telegram/TgWsProxyController.java +++ b/integration/telegram/TgWsProxyController.java @@ -46,6 +46,41 @@ * UI code talks only to this class. Native/JNA details stay inside tgwsproxy-core. */ public final class TgWsProxyController { + + // The core UI cannot depend on the standalone application's Bootstrap class. + public interface UpdateActions { + void check(Context context); + void openReleases(Context context); + } + + private static volatile UpdateActions updateActions; + + public static void registerUpdateActions(UpdateActions actions) { + updateActions = actions; + } + + public static void checkForUpdates(Context context) { + UpdateActions actions = updateActions; + if (actions != null) { + actions.check(context); + } else { + new Handler(Looper.getMainLooper()).post(() -> + android.widget.Toast.makeText(context, "Telegram-WSP updater is unavailable.", + android.widget.Toast.LENGTH_LONG).show()); + } + } + + public static void openReleases(Context context) { + UpdateActions actions = updateActions; + if (actions != null) { + actions.openReleases(context); + } else { + new Handler(Looper.getMainLooper()).post(() -> + android.widget.Toast.makeText(context, "Telegram-WSP updater is unavailable.", + android.widget.Toast.LENGTH_LONG).show()); + } + } + public static final String ROUTE_AUTO = "auto"; public static final String ROUTE_CF_PROXY = "cf_proxy"; public static final String ROUTE_AWG = "awg_warp"; From b511d367aac4ad797860b202b6aa143a51c07cde Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:18:33 -0700 Subject: [PATCH 19/31] fix(android): register standalone update actions during Application bootstrap (#45) --- integration/telegram/TgWsProxyBootstrap.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/integration/telegram/TgWsProxyBootstrap.java b/integration/telegram/TgWsProxyBootstrap.java index 5f3b85a..2527f2d 100644 --- a/integration/telegram/TgWsProxyBootstrap.java +++ b/integration/telegram/TgWsProxyBootstrap.java @@ -83,6 +83,15 @@ public static void start(Context context) { } Context appContext = context.getApplicationContext(); + TgWsProxyController.registerUpdateActions(new TgWsProxyController.UpdateActions() { + @Override public void check(Context context) { + TgWsProxyBootstrap.checkForUpdates(context); + } + @Override public void openReleases(Context context) { + TgWsProxyBootstrap.openReleases(context); + } + }); + Thread bootstrapThread = new Thread( () -> startInBackground(appContext), From c0e3dca694fddea41e748ec5ff84bf4f7b94c760 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:18:42 -0700 Subject: [PATCH 20/31] fix(android): route settings updater actions through TgWsProxyController (#45) --- integration/telegram/TgWsProxySettingsActivity.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/integration/telegram/TgWsProxySettingsActivity.java b/integration/telegram/TgWsProxySettingsActivity.java index 97c27f2..1b0a7db 100644 --- a/integration/telegram/TgWsProxySettingsActivity.java +++ b/integration/telegram/TgWsProxySettingsActivity.java @@ -20,7 +20,6 @@ import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.messenger.TgWsProxyController; -import org.telegram.messenger.TgWsProxyBootstrap; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.BaseFragment; @@ -108,13 +107,13 @@ public void onItemClick(int id) { addHeader(context, content, tr(context, "ОБНОВЛЕНИЯ")); TextSettingsCell checkUpdates = actionCell(context, tr(context, "Проверить обновления Telegram-WSP")); - checkUpdates.setOnClickListener(v -> TgWsProxyBootstrap.checkForUpdates(context)); + checkUpdates.setOnClickListener(v -> TgWsProxyController.checkForUpdates(context)); content.addView(checkUpdates, LayoutHelper.createLinear( LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT )); TextSettingsCell releases = actionCell(context, tr(context, "Открыть релизы Telegram-WSP")); - releases.setOnClickListener(v -> TgWsProxyBootstrap.openReleases(context)); + releases.setOnClickListener(v -> TgWsProxyController.openReleases(context)); content.addView(releases, LayoutHelper.createLinear( LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT )); From a373f2de05f4f8bfa6e1376a988dc323bebc15e1 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:19:01 -0700 Subject: [PATCH 21/31] test(ci): assert core-to-standalone updater action bridge (#45) --- scripts/ci.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/ci.ps1 b/scripts/ci.ps1 index afab448..0c04663 100644 --- a/scripts/ci.ps1 +++ b/scripts/ci.ps1 @@ -355,6 +355,7 @@ if ($applyScript -notmatch 'TELEGRAM_WSP_RELEASE_REVISION' -or $applyScript -not $updater = Get-Content (Join-Path $root 'integration/telegram/TgWsProxyBootstrap.java') -Raw $settings = Get-Content (Join-Path $root 'integration/telegram/TgWsProxySettingsActivity.java') -Raw +$controller = Get-Content (Join-Path $root 'integration/telegram/TgWsProxyController.java') -Raw if ($updater -match 'KEY_UPDATE_LAST_CHECK' -or $updater -notmatch 'KEY_UPDATE_LAST_SUCCESS' -or $updater -notmatch 'KEY_UPDATE_LAST_ATTEMPT' -or @@ -363,8 +364,10 @@ if ($updater -match 'KEY_UPDATE_LAST_CHECK' -or } if ($updater -notmatch 'onActivityResumed' -or $updater -notmatch 'KEY_UPDATE_PENDING_MANIFEST' -or - $settings -notmatch 'TgWsProxyBootstrap\.checkForUpdates' -or - $settings -notmatch 'TgWsProxyBootstrap\.openReleases') { + $settings -notmatch 'TgWsProxyController\.checkForUpdates' -or + $settings -notmatch 'TgWsProxyController\.openReleases' -or + $controller -notmatch 'registerUpdateActions' -or + $updater -notmatch 'TgWsProxyController\.registerUpdateActions') { throw 'Updater must support foreground checks, pending offers and manual/browser actions.' } if ($updater -notmatch 'validateUpdate\(JSONObject' -or From dcc9056d9d012d0dac856ed19b63818a5199a3eb Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:20:06 -0700 Subject: [PATCH 22/31] fix(updater): discard stale pending offer when latest release requires newer Android (#45) --- integration/telegram/TgWsProxyBootstrap.java | 1 + 1 file changed, 1 insertion(+) diff --git a/integration/telegram/TgWsProxyBootstrap.java b/integration/telegram/TgWsProxyBootstrap.java index 2527f2d..9622ccb 100644 --- a/integration/telegram/TgWsProxyBootstrap.java +++ b/integration/telegram/TgWsProxyBootstrap.java @@ -221,6 +221,7 @@ private static void requestUpdateCheck(Context context, boolean manual) { synchronized (TgWsProxyBootstrap.class) { pendingUpdate = null; } if (manual) message(context, "Telegram-WSP is up to date."); } else if (update.minimumSdk > Build.VERSION.SDK_INT) { + synchronized (TgWsProxyBootstrap.class) { pendingUpdate = null; } if (manual) message(context, update.tag + " requires Android API " + update.minimumSdk); } else { synchronized (TgWsProxyBootstrap.class) { From 2b06916f93542978c705d0765d0b379e4ab890a6 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:23:08 -0700 Subject: [PATCH 23/31] fix(ci): preserve bootstrap controller-start contract (#45) --- integration/telegram/TgWsProxyBootstrap.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/telegram/TgWsProxyBootstrap.java b/integration/telegram/TgWsProxyBootstrap.java index 9622ccb..291e50b 100644 --- a/integration/telegram/TgWsProxyBootstrap.java +++ b/integration/telegram/TgWsProxyBootstrap.java @@ -145,9 +145,9 @@ private static void restorePendingUpdate(Context context) { } } - private static void startInBackground(Context context) { + private static void startInBackground(Context appContext) { try { - TgWsProxyController.start(context); + TgWsProxyController.start(appContext); } catch (Throwable error) { FileLog.e("TgWsProxy bootstrap failed: " + error.getClass().getSimpleName()); } From a0ad27bcd016d01e102ec17ef483eb54071d814a Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:34:50 -0700 Subject: [PATCH 24/31] fix(release): retry unpublished Telegram versions without overwriting stable tags (#45) --- scripts/resolve-upstream-release.ps1 | 102 +++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 scripts/resolve-upstream-release.ps1 diff --git a/scripts/resolve-upstream-release.ps1 b/scripts/resolve-upstream-release.ps1 new file mode 100644 index 0000000..7a261bc --- /dev/null +++ b/scripts/resolve-upstream-release.ps1 @@ -0,0 +1,102 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$TelegramVersion, + [Parameter(Mandatory = $true)][bool]$UpstreamChanged, + [Parameter(Mandatory = $true)][bool]$ForceRelease, + [Parameter(Mandatory = $true)][string]$ReleaseListJson, + [Parameter(Mandatory = $true)][string]$ReleaseRef, + [string]$RequestedRevision = '' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if ($TelegramVersion -notmatch '^\d+\.\d+\.\d+$') { + throw "Unsupported Telegram version: $TelegramVersion" +} +$targetVersion = [version]$TelegramVersion + +$releases = @(ConvertFrom-Json -InputObject $ReleaseListJson) +$highestStableVersion = $null +$highestStableRevision = 0 +$highestCurrentRevision = 0 +$currentVersionPublished = $false +$knownTags = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + +foreach ($release in $releases) { + if ($null -eq $release.tagName -or [string]::IsNullOrWhiteSpace([string]$release.tagName)) { + throw 'GitHub release list contains a missing tagName.' + } + [void]$knownTags.Add([string]$release.tagName) + if ($release.isDraft -or $release.isPrerelease) { continue } + if ($release.tagName -notmatch '^v(\d+\.\d+\.\d+)-wsp\.(\d+)$') { continue } + + $version = [version]$Matches[1] + $revision = [int]$Matches[2] + if ($revision -lt 1 -or $revision -gt 99) { + throw "Stable Telegram-WSP release revision is invalid: $($release.tagName)" + } + if ($null -eq $highestStableVersion -or $version -gt $highestStableVersion) { + $highestStableVersion = $version + } + if ($revision -gt $highestStableRevision) { + $highestStableRevision = $revision + } + if ($version -eq $targetVersion) { + $currentVersionPublished = $true + if ($revision -gt $highestCurrentRevision) { + $highestCurrentRevision = $revision + } + } +} + +if ($null -ne $highestStableVersion -and $targetVersion -lt $highestStableVersion) { + throw "Pinned Telegram $TelegramVersion is older than published stable Telegram $highestStableVersion." +} +if ($RequestedRevision -ne '' -and $RequestedRevision -notmatch '^\d+$') { + throw 'Requested Telegram-WSP revision must be decimal digits.' +} +$revision = if ($RequestedRevision -eq '') { + [Math]::Max(1, $highestStableRevision) +} else { + [int]$RequestedRevision +} +if ($revision -lt 1 -or $revision -gt 99) { + throw 'Requested Telegram-WSP revision must be between 1 and 99.' +} + +$tag = "v$TelegramVersion-wsp.$revision" +if ($currentVersionPublished -and $revision -lt $highestCurrentRevision -and + -not $knownTags.Contains($tag)) { + throw "Refusing to publish lower WSP revision $tag after revision $highestCurrentRevision." +} + +$tagExists = $knownTags.Contains($tag) +$missingStableForPin = -not $currentVersionPublished +# A failed Windows signing/publishing job can leave the validated upstream pin +# on main. Retry the missing release on the next scheduled run without requiring +# the Telegram master commit to change again. +$releaseRequired = -not $tagExists -and ( + $ForceRelease -or + ($missingStableForPin -and ($UpstreamChanged -or + $null -eq $highestStableVersion -or $targetVersion -gt $highestStableVersion)) +) +$reason = if ($tagExists) { + 'tag-already-exists' +} elseif ($releaseRequired -and $UpstreamChanged) { + 'new-upstream' +} elseif ($releaseRequired -and $ForceRelease) { + 'explicit-force' +} elseif ($releaseRequired) { + 'retry-unpublished-upstream' +} else { + 'stable-current' +} + +[ordered]@{ + releaseRequired = [bool]$releaseRequired + releaseRef = $ReleaseRef + revision = $revision + tag = $tag + reason = $reason +} | ConvertTo-Json -Compress From fd25874b039bda5a642ad7907f58a1782e3a464a Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:35:55 -0700 Subject: [PATCH 25/31] test(release): cover automatic retry, stable revision and no-overwrite decisions (#45) --- scripts/test-upstream-release-decision.ps1 | 79 ++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 scripts/test-upstream-release-decision.ps1 diff --git a/scripts/test-upstream-release-decision.ps1 b/scripts/test-upstream-release-decision.ps1 new file mode 100644 index 0000000..0c85336 --- /dev/null +++ b/scripts/test-upstream-release-decision.ps1 @@ -0,0 +1,79 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$scriptPath = Join-Path $PSScriptRoot 'resolve-upstream-release.ps1' + +function Test-Decision { + param( + [string]$Name, + [string]$Version, + [bool]$Changed, + [bool]$Force, + [object[]]$Releases, + [string]$Revision = '', + [bool]$ExpectedRequired, + [string]$ExpectedTag, + [string]$ExpectedReason = '' + ) + + $releaseList = ConvertTo-Json -InputObject @($Releases) -Compress -Depth 4 + $rawResult = & $scriptPath -TelegramVersion $Version -UpstreamChanged $Changed ` + -ForceRelease $Force -ReleaseListJson $releaseList -ReleaseRef 'regression-ref' ` + -RequestedRevision $Revision + $result = $rawResult | ConvertFrom-Json + if ([bool]$result.releaseRequired -ne $ExpectedRequired -or $result.tag -ne $ExpectedTag) { + throw "$Name failed: $(ConvertTo-Json $result -Compress)" + } + if ($ExpectedReason -ne '' -and $result.reason -ne $ExpectedReason) { + throw "$Name: unexpected reason $($result.reason)" + } + if ($result.releaseRef -ne 'regression-ref') { + throw "$Name: release ref was not preserved." + } + Write-Host "PASS: $Name" +} + +$stableOld = @{ tagName = 'v12.10.6-wsp.2'; isDraft = $false; isPrerelease = $false } +$stableOldFirst = @{ tagName = 'v12.10.6-wsp.1'; isDraft = $false; isPrerelease = $false } +$stableNew = @{ tagName = 'v13.0.3-wsp.2'; isDraft = $false; isPrerelease = $false } +$stableNewFirst = @{ tagName = 'v13.0.3-wsp.1'; isDraft = $false; isPrerelease = $false } +$draftNew = @{ tagName = 'v13.0.3-wsp.2'; isDraft = $true; isPrerelease = $false } +$prereleaseNew = @{ tagName = 'v13.0.3-wsp.2'; isDraft = $false; isPrerelease = $true } + +Test-Decision -Name 'First publication' -Version '12.10.6' -Changed $true -Force $false ` + -Releases @() -ExpectedRequired $true -ExpectedTag 'v12.10.6-wsp.1' +Test-Decision -Name 'Pinned stable already published' -Version '12.10.6' -Changed $false -Force $false ` + -Releases @($stableOld, $stableOldFirst) -ExpectedRequired $false -ExpectedTag 'v12.10.6-wsp.2' +Test-Decision -Name 'New upstream inherits global WSP revision' -Version '13.0.3' -Changed $true -Force $false ` + -Releases @($stableOld, $stableOldFirst) -ExpectedRequired $true -ExpectedTag 'v13.0.3-wsp.2' +Test-Decision -Name 'Retry missing release after upstream pin commit' -Version '13.0.3' -Changed $false -Force $false ` + -Releases @($stableOld, $stableOldFirst) -ExpectedRequired $true -ExpectedTag 'v13.0.3-wsp.2' ` + -ExpectedReason 'retry-unpublished-upstream' +Test-Decision -Name 'Same version stable already published' -Version '13.0.3' -Changed $false -Force $false ` + -Releases @($stableOld, $stableNew) -ExpectedRequired $false -ExpectedTag 'v13.0.3-wsp.2' +Test-Decision -Name 'Do not invent missing WSP revision hotfix' -Version '13.0.3' -Changed $false -Force $false ` + -Releases @($stableOld, $stableNewFirst) -ExpectedRequired $false -ExpectedTag 'v13.0.3-wsp.2' +Test-Decision -Name 'Explicit newer hotfix revision' -Version '13.0.3' -Changed $false -Force $true ` + -Releases @($stableOld, $stableNewFirst) -Revision '3' -ExpectedRequired $true -ExpectedTag 'v13.0.3-wsp.3' +Test-Decision -Name 'Never overwrite existing stable tag even if forced' -Version '13.0.3' -Changed $false -Force $true ` + -Releases @($stableOld, $stableNew) -ExpectedRequired $false -ExpectedTag 'v13.0.3-wsp.2' +Test-Decision -Name 'Never overwrite an existing draft tag' -Version '13.0.3' -Changed $true -Force $false ` + -Releases @($stableOld, $draftNew) -ExpectedRequired $false -ExpectedTag 'v13.0.3-wsp.2' +Test-Decision -Name 'Never overwrite an existing prerelease tag' -Version '13.0.3' -Changed $true -Force $false ` + -Releases @($stableOld, $prereleaseNew) -ExpectedRequired $false -ExpectedTag 'v13.0.3-wsp.2' + +$rollbackRejected = $false +try { + & $scriptPath -TelegramVersion '12.10.5' -UpstreamChanged $true -ForceRelease $true ` + -ReleaseListJson (ConvertTo-Json -InputObject @($stableOld) -Compress) -ReleaseRef 'test' | Out-Null +} catch { + if ($_.Exception.Message -notmatch 'older than published stable') { throw } + $rollbackRejected = $true +} +if (-not $rollbackRejected) { throw 'Release rollback was not rejected.' } +Write-Host 'PASS: version rollback rejected' + +Write-Host 'Release decision regression tests passed.' From e7d506c9b42bc08e2434608f549e23ec1ae69737 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:36:16 -0700 Subject: [PATCH 26/31] fix(sync): retry missing stable release after validated pin was committed (#45) --- .github/workflows/upstream-sync.yml | 67 ++++++++++------------------- 1 file changed, 23 insertions(+), 44 deletions(-) diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml index 5891f97..faf084b 100644 --- a/.github/workflows/upstream-sync.yml +++ b/.github/workflows/upstream-sync.yml @@ -137,54 +137,33 @@ jobs: REQUESTED_REVISION: ${{ inputs.revision }} run: | $config = Get-Content 'config/upstream.json' -Raw | ConvertFrom-Json - $requestedRevision = if ($null -eq $env:REQUESTED_REVISION) { '' } else { $env:REQUESTED_REVISION.Trim() } - if (-not [string]::IsNullOrWhiteSpace($requestedRevision) -and $requestedRevision -notmatch '^\d+$') { - throw 'revision must contain decimal digits only.' - } - - if ([string]::IsNullOrWhiteSpace($requestedRevision)) { - $releaseJson = & gh release list --repo $env:GITHUB_REPOSITORY --limit 100 --json tagName,isDraft,isPrerelease - if ($LASTEXITCODE -ne 0) { throw 'Failed to list GitHub Releases while resolving the WSP revision.' } - $releaseData = (($releaseJson -join [Environment]::NewLine) | ConvertFrom-Json) - $knownRevisions = @( - foreach ($release in $releaseData) { - if ($release.isDraft -or $release.isPrerelease) { continue } - if ([string]$release.tagName -match '^v.+-wsp\.(\d+)$') { - [int]$Matches[1] - } - } - ) - $revision = if ($knownRevisions.Count -eq 0) { - '1' - } else { - [string](($knownRevisions | Measure-Object -Maximum).Maximum) - } - } else { - $revision = $requestedRevision - } - $changed = '${{ steps.upstream.outputs.changed }}' -eq 'true' $releaseRef = if ($changed) { '${{ steps.commit.outputs.sha }}' } else { (git rev-parse HEAD).Trim() } - $tag = "v$($config.telegramVersion)-wsp.$revision" + if ($releaseRef -notmatch '^[0-9a-f]{40}$') { throw 'Invalid validated release ref.' } - & gh release view $tag --repo $env:GITHUB_REPOSITORY *> $null - $releaseExists = $? + # Include draft and prerelease tags so an occupied tag can never be overwritten. + $releaseJson = & gh release list --repo $env:GITHUB_REPOSITORY --limit 100 --json tagName,isDraft,isPrerelease + if ($LASTEXITCODE -ne 0) { throw 'Failed to enumerate GitHub Releases.' } + $releaseList = $releaseJson -join [Environment]::NewLine $forced = $env:FORCE_RELEASE -eq 'true' - $required = $forced -or ($changed -and -not $releaseExists) - - "release_required=$($required.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT - "release_ref=$releaseRef" >> $env:GITHUB_OUTPUT - "revision=$revision" >> $env:GITHUB_OUTPUT - - Write-Host "Tag: $tag" - Write-Host "Release exists: $releaseExists" - Write-Host "Release required: $required" - Write-Host "Release ref: $releaseRef" - - # A missing release is expected on first publication. Do not propagate - # the non-zero exit code from 'gh release view' to the pwsh process. - exit 0 - + $requested = if ($null -eq $env:REQUESTED_REVISION) { '' } else { $env:REQUESTED_REVISION.Trim() } + $decisionText = & ./scripts/resolve-upstream-release.ps1 ` + -TelegramVersion ([string]$config.telegramVersion) ` + -UpstreamChanged $changed ` + -ForceRelease $forced ` + -RequestedRevision $requested ` + -ReleaseListJson $releaseList ` + -ReleaseRef $releaseRef + $decision = $decisionText | ConvertFrom-Json + + "release_required=$($decision.releaseRequired.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT + "release_ref=$($decision.releaseRef)" >> $env:GITHUB_OUTPUT + "revision=$($decision.revision)" >> $env:GITHUB_OUTPUT + + Write-Host "Release tag: $($decision.tag)" + Write-Host "Reason: $($decision.reason)" + Write-Host "Release required: $($decision.releaseRequired)" + Write-Host "Release ref: $($decision.releaseRef)" release: name: Publish validated Telegram-WSP update needs: sync From 29bd0486a03e93c1fc007fd994264c1bdfb251ab Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:37:06 -0700 Subject: [PATCH 27/31] test(ci): execute release retry and stable-revision regression matrix (#45) --- scripts/ci.ps1 | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/scripts/ci.ps1 b/scripts/ci.ps1 index 0c04663..c384606 100644 --- a/scripts/ci.ps1 +++ b/scripts/ci.ps1 @@ -33,6 +33,8 @@ $required = @( 'scripts/build-release.ps1', 'scripts/create-release-keystore.ps1', 'scripts/sync-upstream.ps1', + 'scripts/resolve-upstream-release.ps1', + 'scripts/test-upstream-release-decision.ps1', 'scripts/package-release-source.ps1', 'scripts/bootstrap-release-actions.ps1', 'scripts/diagnose-xiaomi-dark-mode.ps1', @@ -68,6 +70,8 @@ foreach ($scriptFile in $powerShellScripts) { } } +& (Join-Path $root 'scripts/test-upstream-release-decision.ps1') + $diagnosticScript = Get-Content (Join-Path $root 'scripts/diagnose-xiaomi-dark-mode.ps1') -Raw if ($diagnosticScript -match '\[string\[\]\]\$Args') { throw 'diagnose-xiaomi-dark-mode.ps1 must not shadow the PowerShell automatic $Args variable.' @@ -228,32 +232,25 @@ if ($upstreamWorkflow -notmatch 'schedule:' -or $upstreamWorkflow -notmatch 'syn throw 'Upstream workflow must check Telegram on a schedule and call the reusable release workflow.' } -if ($upstreamWorkflow -notmatch '\$required\s*=\s*\$forced\s+-or\s+\(\$changed\s+-and\s+-not\s+\$releaseExists\)') { - throw 'Upstream release decision must require an upstream change unless force_release is explicit.' -} -if ($upstreamWorkflow -match '\$changed\s+-or\s+\$forced\s+-or\s+-not\s+\$releaseExists') { - throw 'Upstream release decision must not publish merely because the default revision tag is missing.' -} -if (-not $upstreamWorkflow.Contains('gh release list') -or - -not $upstreamWorkflow.Contains('isDraft,isPrerelease') -or - -not $upstreamWorkflow.Contains('Measure-Object -Maximum') -or - -not $upstreamWorkflow.Contains("tagName -match '^v.+-wsp\.(\d+)$'")) { - throw 'Upstream sync must inherit the highest stable WSP revision when no explicit revision is requested.' +if ($upstreamWorkflow -notmatch 'resolve-upstream-release\.ps1' -or + $upstreamWorkflow -notmatch 'gh release list' -or + $upstreamWorkflow -notmatch 'isDraft,isPrerelease' -or + $upstreamWorkflow -notmatch 'release_required=' -or + $upstreamWorkflow -notmatch 'release_ref=' -or + $upstreamWorkflow -notmatch 'revision=') { + throw 'Upstream release decision must use the tested, revision-aware stable resolver.' } -if (-not $upstreamWorkflow.Contains("default: ''")) { - throw 'Upstream sync revision override must default to empty so scheduled runs can inherit the stable WSP revision.' +if ($upstreamWorkflow -notmatch "default: ''") { + throw 'Upstream sync revision override must default to empty so scheduled runs inherit stable WSP revision.' } - -if ($upstreamWorkflow -notmatch 'Validate release secrets before changing main' -or $upstreamWorkflow -notmatch 'RELEASE_KEYSTORE_BASE64') { - throw 'Upstream workflow must fail before changing main when release secrets are not configured.' -} - -if (-not $upstreamWorkflow.Contains('$releaseExists = $?')) { - throw 'Upstream release decision must capture whether the GitHub Release exists.' +if ($upstreamWorkflow -notmatch 'Validate release secrets before changing main' -or + $upstreamWorkflow -notmatch 'RELEASE_KEYSTORE_BASE64') { + throw 'Upstream workflow must validate release secrets before committing the pin.' } -if (-not $upstreamWorkflow.Contains('exit 0')) { - throw 'Upstream release decision must clear the expected missing-release native exit code.' +if ($upstreamWorkflow -notmatch 'Compile candidate APK before changing main' -or + $upstreamWorkflow -notmatch 'build-apk\.ps1 -SkipPrepare') { + throw 'A changed upstream must compile before its pin is pushed to main.' } $releaseBootstrapScript = Get-Content (Join-Path $root 'scripts/bootstrap-release-actions.ps1') -Raw From 0e311489c74f22e46c54f6bfc3cffa7d0f1f5090 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:37:29 -0700 Subject: [PATCH 28/31] fix(release): prevent overwriting existing signed APK and stable manifest (#45) --- .github/workflows/release.yml | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44e1fc0..69885ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -226,26 +226,23 @@ jobs: ) $assets += @(Get-ChildItem 'dist/source' -File | ForEach-Object { $_.FullName }) + # Published tags and their signed APK/feed are immutable. A failed + # build may be retried only while its tag has not been published. gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY *> $null - $exists = $LASTEXITCODE -eq 0 - - if ($exists) { - gh release upload $env:RELEASE_TAG @assets --clobber --repo $env:GITHUB_REPOSITORY - if ($LASTEXITCODE -ne 0) { throw 'gh release upload failed.' } - gh release edit $env:RELEASE_TAG --title $env:RELEASE_TITLE --notes-file 'dist/release-notes.md' --repo $env:GITHUB_REPOSITORY - if ($LASTEXITCODE -ne 0) { throw 'gh release edit failed.' } - } else { - $arguments = @('release', 'create', $env:RELEASE_TAG) + $assets + @( - '--title', $env:RELEASE_TITLE, - '--notes-file', 'dist/release-notes.md', - '--target', $env:RELEASE_COMMIT, - '--repo', $env:GITHUB_REPOSITORY - ) - if ($env:PRERELEASE -eq 'true') { $arguments += '--prerelease' } - & gh @arguments - if ($LASTEXITCODE -ne 0) { throw 'gh release create failed.' } + if ($LASTEXITCODE -eq 0) { + throw "Release $env:RELEASE_TAG already exists; refusing to overwrite signed assets." } + $arguments = @('release', 'create', $env:RELEASE_TAG) + $assets + @( + '--title', $env:RELEASE_TITLE, + '--notes-file', 'dist/release-notes.md', + '--target', $env:RELEASE_COMMIT, + '--repo', $env:GITHUB_REPOSITORY + ) + if ($env:PRERELEASE -eq 'true') { $arguments += '--prerelease' } + & gh @arguments + if ($LASTEXITCODE -ne 0) { throw 'gh release create failed.' } + - name: Release summary shell: pwsh env: From 7c6a5c41a2c769007ced6845f440253f1f49e24d Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:38:11 -0700 Subject: [PATCH 29/31] fix(tests): correct PowerShell interpolated case labels (#45) --- scripts/test-upstream-release-decision.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test-upstream-release-decision.ps1 b/scripts/test-upstream-release-decision.ps1 index 0c85336..d01ef2b 100644 --- a/scripts/test-upstream-release-decision.ps1 +++ b/scripts/test-upstream-release-decision.ps1 @@ -28,10 +28,10 @@ function Test-Decision { throw "$Name failed: $(ConvertTo-Json $result -Compress)" } if ($ExpectedReason -ne '' -and $result.reason -ne $ExpectedReason) { - throw "$Name: unexpected reason $($result.reason)" + throw "$($Name): unexpected reason $($result.reason)" } if ($result.releaseRef -ne 'regression-ref') { - throw "$Name: release ref was not preserved." + throw "$($Name): release ref was not preserved." } Write-Host "PASS: $Name" } From 21aefe55bb95640c1edb775c5b86bc5f4fbe693b Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:40:12 -0700 Subject: [PATCH 30/31] docs: describe fully scheduled Telegram release and recovery without owner steps (#45) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f5abb0a..ae9d97a 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,8 @@ versionCode = telegramBuild * 1000 + wspRevision * 10 + 9 Если overlay не применяется или проверки не проходят, новая версия Telegram не должна становиться публичным Telegram-WSP-релизом. +Workflow запускается по расписанию без участия пользователя. Если upstream pin уже обновлён, но подписанный stable-релиз ещё не опубликован (например, предыдущий Windows release job завершился ошибкой), следующая проверка повторно запускает release-сборку. Уже существующий stable tag/подписанный APK не перезаписывается; WSP revision наследуется от последней опубликованной стабильной ревизии. + ## Разработка Рабочий Telegram checkout и core создаются локально: From 00dc184db024d111ad7185ce62158f05bea2cce9 Mon Sep 17 00:00:00 2001 From: Regstar2 Date: Sun, 25 Oct 2026 03:40:22 -0700 Subject: [PATCH 31/31] test(ci): enforce immutable signed release assets on automatic retries (#45) --- scripts/ci.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/ci.ps1 b/scripts/ci.ps1 index c384606..c0b1340 100644 --- a/scripts/ci.ps1 +++ b/scripts/ci.ps1 @@ -212,6 +212,10 @@ if ($releaseWorkflow -notmatch 'RELEASE_KEYSTORE_BASE64' -or $releaseWorkflow -n if ($releaseWorkflow -notmatch 'latest\.json' -or $releaseWorkflow -notmatch 'package-release-source\.ps1') { throw 'Release workflow must publish the update feed and Corresponding Source assets.' } +if ($releaseWorkflow -match 'gh release upload|gh release edit.*--notes-file' -or + $releaseWorkflow -notmatch 'already exists; refusing to overwrite signed assets') { + throw 'Published stable release assets must never be overwritten on retry.' +} if ($releaseWorkflow -match '(?m)^\s*\$home\s*=') { throw 'Release workflow must not assign to PowerShell automatic HOME on Windows runners.'