From 8374eee8de4520a51bb89346ef507a9890de5c8b Mon Sep 17 00:00:00 2001
From: Isla <5048549+islathehut@users.noreply.github.com>
Date: Fri, 3 Jul 2026 14:27:14 -0400
Subject: [PATCH 01/18] Publish
- @quiet/desktop@8.0.0-alpha.0
- @quiet/mobile@8.0.0-alpha.0
---
packages/desktop/CHANGELOG.md | 28 +++++++++++++++++++
packages/desktop/package-lock.json | 4 +--
packages/desktop/package.json | 2 +-
packages/mobile/CHANGELOG.md | 28 +++++++++++++++++++
packages/mobile/android/app/build.gradle | 4 +--
packages/mobile/ios/Quiet/Info.plist | 4 +--
.../Info.plist | 4 +--
packages/mobile/ios/QuietTests/Info.plist | 4 +--
packages/mobile/package-lock.json | 4 +--
packages/mobile/package.json | 2 +-
10 files changed, 70 insertions(+), 14 deletions(-)
diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md
index dfd9a676e6..f1cb50f85c 100644
--- a/packages/desktop/CHANGELOG.md
+++ b/packages/desktop/CHANGELOG.md
@@ -1,3 +1,31 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [8.0.0-alpha.0](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@7.1.0...@quiet/desktop@8.0.0-alpha.0) (2026-07-03)
+
+
+### Bug Fixes
+
+* broken privacy policy link on join-server screen ([#3186](https://github.com/TryQuiet/quiet/issues/3186)) ([4cdb3a1](https://github.com/TryQuiet/quiet/commit/4cdb3a1c8c8cb30ba481dc6c16f5c05fc5b6bac7))
+* sidebar layout tweaks ([#3184](https://github.com/TryQuiet/quiet/issues/3184)) ([accc53f](https://github.com/TryQuiet/quiet/commit/accc53ff515e302fe07d404827a4dc3a70178b08))
+
+
+### Features
+
+* **3155:** Add private channels with modifiable membership (no removals) to desktop ([#3177](https://github.com/TryQuiet/quiet/issues/3177)) ([9eef40c](https://github.com/TryQuiet/quiet/commit/9eef40c7974f22e9a5bfa51449cfc35b9c2f66f4))
+* **3155:** Private channels for mobile ([#3194](https://github.com/TryQuiet/quiet/issues/3194)) ([c3c89bf](https://github.com/TryQuiet/quiet/commit/c3c89bfdc9758e57b47f6e9630e3a8e3f6f6d7ef))
+* **3277:** Open private channel creation to all users and prep for role-based permissons ([#3276](https://github.com/TryQuiet/quiet/issues/3276)) ([2592f56](https://github.com/TryQuiet/quiet/commit/2592f5689ab1482c61044c8e3c40d759282270e5))
+* **3296:** Update invite links, use team ID everywhere and write randomly generated team name to chain ([#3324](https://github.com/TryQuiet/quiet/issues/3324)) ([c2a850c](https://github.com/TryQuiet/quiet/commit/c2a850c7f0da377b0cdf46e892b36fd74de31c86))
+* **3300:** Allow all members of a private channel to add members to that channel ([#3303](https://github.com/TryQuiet/quiet/issues/3303)) ([5e41f92](https://github.com/TryQuiet/quiet/commit/5e41f921c11836ef8e8031aabdd9e42eada8b43a))
+* **3300:** Allow channel owners to add members to channel ([#3299](https://github.com/TryQuiet/quiet/issues/3299)) ([8599479](https://github.com/TryQuiet/quiet/commit/85994797d82e10c81d2bb26d899233ad5a068c94))
+* **3321:** Use random base58 usernames on sigchain ([#3325](https://github.com/TryQuiet/quiet/issues/3325)) ([698b1fd](https://github.com/TryQuiet/quiet/commit/698b1fd6217151a838bee0919c1623ec292fc76e))
+
+
+
+
+
# Changelog
## [7.3.0]
diff --git a/packages/desktop/package-lock.json b/packages/desktop/package-lock.json
index 7164c6d90c..99f8aebc98 100644
--- a/packages/desktop/package-lock.json
+++ b/packages/desktop/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@quiet/desktop",
- "version": "7.3.0",
+ "version": "8.0.0-alpha.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@quiet/desktop",
- "version": "7.3.0",
+ "version": "8.0.0-alpha.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"@dotenvx/dotenvx": "1.39.0",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 9cc752bc48..226b055a6a 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -79,7 +79,7 @@
},
"homepage": "https://github.com/TryQuiet",
"@comment version": "To build new version for specific platform, just replace platform in version tag to one of following linux, mac, windows",
- "version": "7.3.0",
+ "version": "8.0.0-alpha.0",
"description": "Decentralized team chat",
"main": "dist/main/main.js",
"scripts": {
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index dfd9a676e6..c4b8b937c8 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -1,3 +1,31 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [8.0.0-alpha.0](https://github.com/TryQuiet/quiet/compare/@quiet/mobile@7.1.0...@quiet/mobile@8.0.0-alpha.0) (2026-07-03)
+
+
+### Bug Fixes
+
+* broken privacy policy link on join-server screen ([#3186](https://github.com/TryQuiet/quiet/issues/3186)) ([4cdb3a1](https://github.com/TryQuiet/quiet/commit/4cdb3a1c8c8cb30ba481dc6c16f5c05fc5b6bac7))
+* include flavor prefix in envConfigFiles keys so dotenv picks the right .env ([#3197](https://github.com/TryQuiet/quiet/issues/3197)) ([e3f77d1](https://github.com/TryQuiet/quiet/commit/e3f77d1ec25b673485c74c638cb952ac063b06e4))
+
+
+### Features
+
+* **3155:** Add private channels with modifiable membership (no removals) to desktop ([#3177](https://github.com/TryQuiet/quiet/issues/3177)) ([9eef40c](https://github.com/TryQuiet/quiet/commit/9eef40c7974f22e9a5bfa51449cfc35b9c2f66f4))
+* **3155:** Private channels for mobile ([#3194](https://github.com/TryQuiet/quiet/issues/3194)) ([c3c89bf](https://github.com/TryQuiet/quiet/commit/c3c89bfdc9758e57b47f6e9630e3a8e3f6f6d7ef))
+* **3277:** Open private channel creation to all users and prep for role-based permissons ([#3276](https://github.com/TryQuiet/quiet/issues/3276)) ([2592f56](https://github.com/TryQuiet/quiet/commit/2592f5689ab1482c61044c8e3c40d759282270e5))
+* **3296:** Update invite links, use team ID everywhere and write randomly generated team name to chain ([#3324](https://github.com/TryQuiet/quiet/issues/3324)) ([c2a850c](https://github.com/TryQuiet/quiet/commit/c2a850c7f0da377b0cdf46e892b36fd74de31c86))
+* **3300:** Allow all members of a private channel to add members to that channel ([#3303](https://github.com/TryQuiet/quiet/issues/3303)) ([5e41f92](https://github.com/TryQuiet/quiet/commit/5e41f921c11836ef8e8031aabdd9e42eada8b43a))
+* **3300:** Allow channel owners to add members to channel ([#3299](https://github.com/TryQuiet/quiet/issues/3299)) ([8599479](https://github.com/TryQuiet/quiet/commit/85994797d82e10c81d2bb26d899233ad5a068c94))
+* **mobile:** dev/alpha-only "Share logs" menu item ([#3198](https://github.com/TryQuiet/quiet/issues/3198)) ([9d3457d](https://github.com/TryQuiet/quiet/commit/9d3457d033d06de55e42a615cc15dd682e0fd87f)), closes [#3196](https://github.com/TryQuiet/quiet/issues/3196)
+
+
+
+
+
# Changelog
## [7.3.0]
diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle
index 8db2a27b3e..c7fb8366e0 100644
--- a/packages/mobile/android/app/build.gradle
+++ b/packages/mobile/android/app/build.gradle
@@ -174,8 +174,8 @@ android {
applicationId = "com.quietmobile"
minSdkVersion(rootProject.ext.minSdkVersion)
targetSdkVersion(rootProject.ext.targetSdkVersion)
- versionCode 630
- versionName "7.3.0"
+ versionCode 631
+ versionName "8.0.0-alpha.0"
resValue("string", "build_config_package", "com.quietmobile")
testBuildType = System.getProperty("testBuildType", "debug")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
diff --git a/packages/mobile/ios/Quiet/Info.plist b/packages/mobile/ios/Quiet/Info.plist
index e57d86a224..2fc7485a40 100644
--- a/packages/mobile/ios/Quiet/Info.plist
+++ b/packages/mobile/ios/Quiet/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 7.3.0
+ 8.0.0
CFBundleSignature
????
CFBundleURLTypes
@@ -34,7 +34,7 @@
CFBundleVersion
- 583
+ 584
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist b/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
index 6b3bb0a48b..2638ab0e5f 100644
--- a/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
+++ b/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 7.3.0
+ 8.0.0
CFBundleVersion
- 59
+ 60
FirebaseAppDelegateProxyEnabled
NSAppTransportSecurity
diff --git a/packages/mobile/ios/QuietTests/Info.plist b/packages/mobile/ios/QuietTests/Info.plist
index 94ecd194b9..95f188acab 100644
--- a/packages/mobile/ios/QuietTests/Info.plist
+++ b/packages/mobile/ios/QuietTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 7.3.0
+ 8.0.0
CFBundleSignature
????
CFBundleVersion
- 571
+ 572
diff --git a/packages/mobile/package-lock.json b/packages/mobile/package-lock.json
index ee3742a38b..87c76bd3ff 100644
--- a/packages/mobile/package-lock.json
+++ b/packages/mobile/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@quiet/mobile",
- "version": "7.3.0",
+ "version": "8.0.0-alpha.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@quiet/mobile",
- "version": "7.3.0",
+ "version": "8.0.0-alpha.0",
"dependencies": {
"@d11/react-native-fast-image": "8.11.1",
"@hcaptcha/react-native-hcaptcha": "^2.1.0",
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index f946e9a9fa..5f2a29734d 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@quiet/mobile",
- "version": "7.3.0",
+ "version": "8.0.0-alpha.0",
"scripts": {
"build": "tsc -p tsconfig.build.json --noEmit",
"storybook-android": "react-native run-android --mode=storybookDebug --appIdSuffix=storybook.debug",
From 31219d9e450b94bd5455ceeeaa019db17d83429a Mon Sep 17 00:00:00 2001
From: Isla <5048549+islathehut@users.noreply.github.com>
Date: Fri, 3 Jul 2026 14:27:20 -0400
Subject: [PATCH 02/18] Update packages CHANGELOG.md
---
packages/desktop/CHANGELOG.md | 38 +++++++++++++++++------------------
packages/mobile/CHANGELOG.md | 38 +++++++++++++++++------------------
2 files changed, 36 insertions(+), 40 deletions(-)
diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md
index f1cb50f85c..f762ef469a 100644
--- a/packages/desktop/CHANGELOG.md
+++ b/packages/desktop/CHANGELOG.md
@@ -1,32 +1,30 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-# [8.0.0-alpha.0](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@7.1.0...@quiet/desktop@8.0.0-alpha.0) (2026-07-03)
-
-
-### Bug Fixes
-
-* broken privacy policy link on join-server screen ([#3186](https://github.com/TryQuiet/quiet/issues/3186)) ([4cdb3a1](https://github.com/TryQuiet/quiet/commit/4cdb3a1c8c8cb30ba481dc6c16f5c05fc5b6bac7))
-* sidebar layout tweaks ([#3184](https://github.com/TryQuiet/quiet/issues/3184)) ([accc53f](https://github.com/TryQuiet/quiet/commit/accc53ff515e302fe07d404827a4dc3a70178b08))
+# Changelog
+## [unreleased]
### Features
-* **3155:** Add private channels with modifiable membership (no removals) to desktop ([#3177](https://github.com/TryQuiet/quiet/issues/3177)) ([9eef40c](https://github.com/TryQuiet/quiet/commit/9eef40c7974f22e9a5bfa51449cfc35b9c2f66f4))
-* **3155:** Private channels for mobile ([#3194](https://github.com/TryQuiet/quiet/issues/3194)) ([c3c89bf](https://github.com/TryQuiet/quiet/commit/c3c89bfdc9758e57b47f6e9630e3a8e3f6f6d7ef))
-* **3277:** Open private channel creation to all users and prep for role-based permissons ([#3276](https://github.com/TryQuiet/quiet/issues/3276)) ([2592f56](https://github.com/TryQuiet/quiet/commit/2592f5689ab1482c61044c8e3c40d759282270e5))
-* **3296:** Update invite links, use team ID everywhere and write randomly generated team name to chain ([#3324](https://github.com/TryQuiet/quiet/issues/3324)) ([c2a850c](https://github.com/TryQuiet/quiet/commit/c2a850c7f0da377b0cdf46e892b36fd74de31c86))
-* **3300:** Allow all members of a private channel to add members to that channel ([#3303](https://github.com/TryQuiet/quiet/issues/3303)) ([5e41f92](https://github.com/TryQuiet/quiet/commit/5e41f921c11836ef8e8031aabdd9e42eada8b43a))
-* **3300:** Allow channel owners to add members to channel ([#3299](https://github.com/TryQuiet/quiet/issues/3299)) ([8599479](https://github.com/TryQuiet/quiet/commit/85994797d82e10c81d2bb26d899233ad5a068c94))
-* **3321:** Use random base58 usernames on sigchain ([#3325](https://github.com/TryQuiet/quiet/issues/3325)) ([698b1fd](https://github.com/TryQuiet/quiet/commit/698b1fd6217151a838bee0919c1623ec292fc76e))
+* Allow all users to create private channels [#3277](https://github.com/TryQuiet/quiet/issues/3277)
+* Allow channel members to add members to private channels [#3300](https://github.com/TryQuiet/quiet/issues/3300)
+### Fixes
+* Don't send deletion message for private channels [#3273](https://github.com/TryQuiet/quiet/issues/3273)
+* Ensure notification registration waits for auth handshake [#3289](https://github.com/TryQuiet/quiet/issues/3289)
+* Fixed a race condition that can cause stale data to remain after leaving community [#3253](https://github.com/TryQuiet/quiet/issues/3253)
+* Temporarily disable private channels
+* Validate user ID on decrypted message matches the signature [#3334](https://github.com/TryQuiet/quiet/issues/3334)
+### Chores
+* Enable QSS on prod
+* Add script for cleaning compiled/generated code directories
-# Changelog
+### Breaking
+
+* Include team ID and createdAt in message encryption and validate on consume [#3304](https://github.com/TryQuiet/quiet/issues/3304)
+* Require team ID on invite links, use team ID for all chain operations, and hash team name on sigchains [#3296](https://github.com/TryQuiet/quiet/issues/3296)
+* Use randomly generated Base58 usernames on sigchain [#3321](https://github.com/TryQuiet/quiet/issues/3321)
## [7.3.0]
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index c4b8b937c8..f762ef469a 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -1,32 +1,30 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-# [8.0.0-alpha.0](https://github.com/TryQuiet/quiet/compare/@quiet/mobile@7.1.0...@quiet/mobile@8.0.0-alpha.0) (2026-07-03)
-
-
-### Bug Fixes
-
-* broken privacy policy link on join-server screen ([#3186](https://github.com/TryQuiet/quiet/issues/3186)) ([4cdb3a1](https://github.com/TryQuiet/quiet/commit/4cdb3a1c8c8cb30ba481dc6c16f5c05fc5b6bac7))
-* include flavor prefix in envConfigFiles keys so dotenv picks the right .env ([#3197](https://github.com/TryQuiet/quiet/issues/3197)) ([e3f77d1](https://github.com/TryQuiet/quiet/commit/e3f77d1ec25b673485c74c638cb952ac063b06e4))
+# Changelog
+## [unreleased]
### Features
-* **3155:** Add private channels with modifiable membership (no removals) to desktop ([#3177](https://github.com/TryQuiet/quiet/issues/3177)) ([9eef40c](https://github.com/TryQuiet/quiet/commit/9eef40c7974f22e9a5bfa51449cfc35b9c2f66f4))
-* **3155:** Private channels for mobile ([#3194](https://github.com/TryQuiet/quiet/issues/3194)) ([c3c89bf](https://github.com/TryQuiet/quiet/commit/c3c89bfdc9758e57b47f6e9630e3a8e3f6f6d7ef))
-* **3277:** Open private channel creation to all users and prep for role-based permissons ([#3276](https://github.com/TryQuiet/quiet/issues/3276)) ([2592f56](https://github.com/TryQuiet/quiet/commit/2592f5689ab1482c61044c8e3c40d759282270e5))
-* **3296:** Update invite links, use team ID everywhere and write randomly generated team name to chain ([#3324](https://github.com/TryQuiet/quiet/issues/3324)) ([c2a850c](https://github.com/TryQuiet/quiet/commit/c2a850c7f0da377b0cdf46e892b36fd74de31c86))
-* **3300:** Allow all members of a private channel to add members to that channel ([#3303](https://github.com/TryQuiet/quiet/issues/3303)) ([5e41f92](https://github.com/TryQuiet/quiet/commit/5e41f921c11836ef8e8031aabdd9e42eada8b43a))
-* **3300:** Allow channel owners to add members to channel ([#3299](https://github.com/TryQuiet/quiet/issues/3299)) ([8599479](https://github.com/TryQuiet/quiet/commit/85994797d82e10c81d2bb26d899233ad5a068c94))
-* **mobile:** dev/alpha-only "Share logs" menu item ([#3198](https://github.com/TryQuiet/quiet/issues/3198)) ([9d3457d](https://github.com/TryQuiet/quiet/commit/9d3457d033d06de55e42a615cc15dd682e0fd87f)), closes [#3196](https://github.com/TryQuiet/quiet/issues/3196)
+* Allow all users to create private channels [#3277](https://github.com/TryQuiet/quiet/issues/3277)
+* Allow channel members to add members to private channels [#3300](https://github.com/TryQuiet/quiet/issues/3300)
+### Fixes
+* Don't send deletion message for private channels [#3273](https://github.com/TryQuiet/quiet/issues/3273)
+* Ensure notification registration waits for auth handshake [#3289](https://github.com/TryQuiet/quiet/issues/3289)
+* Fixed a race condition that can cause stale data to remain after leaving community [#3253](https://github.com/TryQuiet/quiet/issues/3253)
+* Temporarily disable private channels
+* Validate user ID on decrypted message matches the signature [#3334](https://github.com/TryQuiet/quiet/issues/3334)
+### Chores
+* Enable QSS on prod
+* Add script for cleaning compiled/generated code directories
-# Changelog
+### Breaking
+
+* Include team ID and createdAt in message encryption and validate on consume [#3304](https://github.com/TryQuiet/quiet/issues/3304)
+* Require team ID on invite links, use team ID for all chain operations, and hash team name on sigchains [#3296](https://github.com/TryQuiet/quiet/issues/3296)
+* Use randomly generated Base58 usernames on sigchain [#3321](https://github.com/TryQuiet/quiet/issues/3321)
## [7.3.0]
From 4c419ed85c57e8c6291e7b870171c3295ab5eb94 Mon Sep 17 00:00:00 2001
From: Isla <5048549+islathehut@users.noreply.github.com>
Date: Mon, 6 Jul 2026 13:44:50 -0400
Subject: [PATCH 03/18] chore: Update directory to Quiet8 and files7 to files8
(#3339)
* Update directory to Quiet8 and files7 to files8
* Update CHANGELOG
---
CHANGELOG.md | 2 +-
packages/common/src/const.ts | 2 +-
.../java/com/quietmobile/Communication/CommunicationModule.java | 2 +-
.../android/app/src/main/java/com/quietmobile/Utils/Utils.kt | 2 +-
packages/mobile/ios/DataDirectory.swift | 2 +-
packages/mobile/src/utils/shareAllData.ts | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f762ef469a..c3db97dc2f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-## [unreleased]
+## [8.0.0]
### Features
diff --git a/packages/common/src/const.ts b/packages/common/src/const.ts
index cb4687a1b7..e47eb2e0b5 100644
--- a/packages/common/src/const.ts
+++ b/packages/common/src/const.ts
@@ -1,5 +1,5 @@
export const DESKTOP_DEV_DATA_DIR = 'Quietdev'
-export const DESKTOP_DATA_DIR = 'Quiet7'
+export const DESKTOP_DATA_DIR = 'Quiet8'
export enum Site {
DOMAIN = 'tryquiet.org',
diff --git a/packages/mobile/android/app/src/main/java/com/quietmobile/Communication/CommunicationModule.java b/packages/mobile/android/app/src/main/java/com/quietmobile/Communication/CommunicationModule.java
index f5630222a2..57a4034ed6 100644
--- a/packages/mobile/android/app/src/main/java/com/quietmobile/Communication/CommunicationModule.java
+++ b/packages/mobile/android/app/src/main/java/com/quietmobile/Communication/CommunicationModule.java
@@ -429,7 +429,7 @@ private static void deleteBackendData() {
Context context = reactContext.getApplicationContext();
try {
- FileUtils.deleteDirectory(new File(context.getFilesDir(), "backend/files7"));
+ FileUtils.deleteDirectory(new File(context.getFilesDir(), "backend/files8"));
} catch (IOException e) {
Log.e("CommunicationModule", e.toString());
}
diff --git a/packages/mobile/android/app/src/main/java/com/quietmobile/Utils/Utils.kt b/packages/mobile/android/app/src/main/java/com/quietmobile/Utils/Utils.kt
index de008bf102..99244f76f6 100644
--- a/packages/mobile/android/app/src/main/java/com/quietmobile/Utils/Utils.kt
+++ b/packages/mobile/android/app/src/main/java/com/quietmobile/Utils/Utils.kt
@@ -10,7 +10,7 @@ import kotlin.coroutines.suspendCoroutine
object Utils {
fun createDirectory(context: Context): String {
- val dataDirectory = File(context.filesDir, "backend/files7")
+ val dataDirectory = File(context.filesDir, "backend/files8")
dataDirectory.mkdirs()
return dataDirectory.absolutePath
diff --git a/packages/mobile/ios/DataDirectory.swift b/packages/mobile/ios/DataDirectory.swift
index d8a1277890..1015cf420d 100644
--- a/packages/mobile/ios/DataDirectory.swift
+++ b/packages/mobile/ios/DataDirectory.swift
@@ -6,7 +6,7 @@ class DataDirectory: NSObject {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0]
let docURL = URL(string: documentsDirectory)!
- let dataPath = docURL.appendingPathComponent("backend/files7")
+ let dataPath = docURL.appendingPathComponent("backend/files8")
if !FileManager.default.fileExists(atPath: dataPath.path) {
do {
try FileManager.default.createDirectory(atPath: dataPath.path, withIntermediateDirectories: true, attributes: nil)
diff --git a/packages/mobile/src/utils/shareAllData.ts b/packages/mobile/src/utils/shareAllData.ts
index 6c9aac561e..c3414e2def 100644
--- a/packages/mobile/src/utils/shareAllData.ts
+++ b/packages/mobile/src/utils/shareAllData.ts
@@ -8,7 +8,7 @@ import { createLogger } from './logger'
const logger = createLogger('shareAllData')
-const DATA_DIR = RNFS.DocumentDirectoryPath + '/backend/files7'
+const DATA_DIR = RNFS.DocumentDirectoryPath + '/backend/files8'
const LOGS_DIR = RNFS.DocumentDirectoryPath + '/logs'
const SHARE_DIR = RNFS.CachesDirectoryPath + '/quiet-data-share'
const SUPPORT_EMAIL = 'logs@tryquiet.org'
From cfcd610b899b11350a76e039c446139be220e14f Mon Sep 17 00:00:00 2001
From: Isla <5048549+islathehut@users.noreply.github.com>
Date: Mon, 6 Jul 2026 13:44:50 -0400
Subject: [PATCH 04/18] Publish
- @quiet/desktop@8.0.0-alpha.1
- @quiet/mobile@8.0.0-alpha.1
---
packages/desktop/CHANGELOG.md | 13 +++++++++++++
packages/desktop/package-lock.json | 4 ++--
packages/desktop/package.json | 2 +-
packages/mobile/CHANGELOG.md | 13 +++++++++++++
packages/mobile/android/app/build.gradle | 4 ++--
packages/mobile/ios/Quiet/Info.plist | 2 +-
.../QuietNotificationServiceExtension/Info.plist | 2 +-
packages/mobile/ios/QuietTests/Info.plist | 2 +-
packages/mobile/package-lock.json | 4 ++--
packages/mobile/package.json | 2 +-
10 files changed, 37 insertions(+), 11 deletions(-)
diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md
index f762ef469a..dc60465011 100644
--- a/packages/desktop/CHANGELOG.md
+++ b/packages/desktop/CHANGELOG.md
@@ -1,3 +1,16 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [8.0.0-alpha.1](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@8.0.0-alpha.0...@quiet/desktop@8.0.0-alpha.1) (2026-07-07)
+
+**Note:** Version bump only for package @quiet/desktop
+
+
+
+
+
# Changelog
## [unreleased]
diff --git a/packages/desktop/package-lock.json b/packages/desktop/package-lock.json
index 99f8aebc98..3ec80c2029 100644
--- a/packages/desktop/package-lock.json
+++ b/packages/desktop/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@quiet/desktop",
- "version": "8.0.0-alpha.0",
+ "version": "8.0.0-alpha.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@quiet/desktop",
- "version": "8.0.0-alpha.0",
+ "version": "8.0.0-alpha.1",
"license": "GPL-3.0-or-later",
"dependencies": {
"@dotenvx/dotenvx": "1.39.0",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 226b055a6a..e625d23e86 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -79,7 +79,7 @@
},
"homepage": "https://github.com/TryQuiet",
"@comment version": "To build new version for specific platform, just replace platform in version tag to one of following linux, mac, windows",
- "version": "8.0.0-alpha.0",
+ "version": "8.0.0-alpha.1",
"description": "Decentralized team chat",
"main": "dist/main/main.js",
"scripts": {
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index f762ef469a..a1aca5dd80 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -1,3 +1,16 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [8.0.0-alpha.1](https://github.com/TryQuiet/quiet/compare/@quiet/mobile@8.0.0-alpha.0...@quiet/mobile@8.0.0-alpha.1) (2026-07-07)
+
+**Note:** Version bump only for package @quiet/mobile
+
+
+
+
+
# Changelog
## [unreleased]
diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle
index c7fb8366e0..5296194c4b 100644
--- a/packages/mobile/android/app/build.gradle
+++ b/packages/mobile/android/app/build.gradle
@@ -174,8 +174,8 @@ android {
applicationId = "com.quietmobile"
minSdkVersion(rootProject.ext.minSdkVersion)
targetSdkVersion(rootProject.ext.targetSdkVersion)
- versionCode 631
- versionName "8.0.0-alpha.0"
+ versionCode 632
+ versionName "8.0.0-alpha.1"
resValue("string", "build_config_package", "com.quietmobile")
testBuildType = System.getProperty("testBuildType", "debug")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
diff --git a/packages/mobile/ios/Quiet/Info.plist b/packages/mobile/ios/Quiet/Info.plist
index 2fc7485a40..b98875cb86 100644
--- a/packages/mobile/ios/Quiet/Info.plist
+++ b/packages/mobile/ios/Quiet/Info.plist
@@ -34,7 +34,7 @@
CFBundleVersion
- 584
+ 585
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist b/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
index 2638ab0e5f..ad79d4bb71 100644
--- a/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
+++ b/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
8.0.0
CFBundleVersion
- 60
+ 61
FirebaseAppDelegateProxyEnabled
NSAppTransportSecurity
diff --git a/packages/mobile/ios/QuietTests/Info.plist b/packages/mobile/ios/QuietTests/Info.plist
index 95f188acab..820f54e972 100644
--- a/packages/mobile/ios/QuietTests/Info.plist
+++ b/packages/mobile/ios/QuietTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 572
+ 573
diff --git a/packages/mobile/package-lock.json b/packages/mobile/package-lock.json
index 87c76bd3ff..c81b68c5cb 100644
--- a/packages/mobile/package-lock.json
+++ b/packages/mobile/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@quiet/mobile",
- "version": "8.0.0-alpha.0",
+ "version": "8.0.0-alpha.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@quiet/mobile",
- "version": "8.0.0-alpha.0",
+ "version": "8.0.0-alpha.1",
"dependencies": {
"@d11/react-native-fast-image": "8.11.1",
"@hcaptcha/react-native-hcaptcha": "^2.1.0",
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index 5f2a29734d..b2f6d37918 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@quiet/mobile",
- "version": "8.0.0-alpha.0",
+ "version": "8.0.0-alpha.1",
"scripts": {
"build": "tsc -p tsconfig.build.json --noEmit",
"storybook-android": "react-native run-android --mode=storybookDebug --appIdSuffix=storybook.debug",
From 893df93f25478ce9f921ab29d0eecb86c5c9043a Mon Sep 17 00:00:00 2001
From: Isla <5048549+islathehut@users.noreply.github.com>
Date: Tue, 7 Jul 2026 10:07:32 -0400
Subject: [PATCH 05/18] Update packages CHANGELOG.md
---
packages/desktop/CHANGELOG.md | 15 +--------------
packages/mobile/CHANGELOG.md | 15 +--------------
2 files changed, 2 insertions(+), 28 deletions(-)
diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md
index dc60465011..c3db97dc2f 100644
--- a/packages/desktop/CHANGELOG.md
+++ b/packages/desktop/CHANGELOG.md
@@ -1,19 +1,6 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-# [8.0.0-alpha.1](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@8.0.0-alpha.0...@quiet/desktop@8.0.0-alpha.1) (2026-07-07)
-
-**Note:** Version bump only for package @quiet/desktop
-
-
-
-
-
# Changelog
-## [unreleased]
+## [8.0.0]
### Features
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index a1aca5dd80..c3db97dc2f 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -1,19 +1,6 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-# [8.0.0-alpha.1](https://github.com/TryQuiet/quiet/compare/@quiet/mobile@8.0.0-alpha.0...@quiet/mobile@8.0.0-alpha.1) (2026-07-07)
-
-**Note:** Version bump only for package @quiet/mobile
-
-
-
-
-
# Changelog
-## [unreleased]
+## [8.0.0]
### Features
From 0f2191361cdacf5563a2e67d4ea1275b20d34518 Mon Sep 17 00:00:00 2001
From: Isla <5048549+islathehut@users.noreply.github.com>
Date: Tue, 7 Jul 2026 10:38:31 -0400
Subject: [PATCH 06/18] Update s3 bucket for 8.0 (#3345)
* Update s3 bucket for 8.0
* Update CHANGELOG
---
.github/workflows/desktop-build.yml | 10 +++++-----
CHANGELOG.md | 2 ++
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index 14f1e1328f..cc837dec64 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -146,7 +146,7 @@ jobs:
env:
TEST_MODE: ${{ github.event.action == 'prereleased' }}
- S3_BUCKET: ${{ github.event.action == 'released' && 'quiet.7.x' || 'test.quiet' }}
+ S3_BUCKET: ${{ github.event.action == 'released' && 'quiet.8.x' || 'test.quiet' }}
CHECKSUM_PATH: ${{ github.event.action == 'released' && 'packages/desktop/dist/latest-linux.yml' || 'packages/desktop/dist/alpha-linux.yml' }}
steps:
@@ -262,7 +262,7 @@ jobs:
env:
TEST_MODE: ${{ github.event.action == 'prereleased' }}
- S3_BUCKET: ${{ github.event.action == 'released' && 'quiet.7.x' || 'test.quiet' }}
+ S3_BUCKET: ${{ github.event.action == 'released' && 'quiet.8.x' || 'test.quiet' }}
steps:
- uses: actions/checkout@v5
@@ -413,7 +413,7 @@ jobs:
env:
TEST_MODE: ${{ github.event.action == 'prereleased' }}
- S3_BUCKET: ${{ github.event.action == 'released' && 'quiet.7.x' || 'test.quiet' }}
+ S3_BUCKET: ${{ github.event.action == 'released' && 'quiet.8.x' || 'test.quiet' }}
steps:
- uses: actions/checkout@v5
@@ -564,7 +564,7 @@ jobs:
startsWith(github.ref, 'refs/tags/@quiet/desktop')
env:
- S3_BUCKET: ${{ github.event.action == 'released' && 'quiet.7.x' || 'test.quiet' }}
+ S3_BUCKET: ${{ github.event.action == 'released' && 'quiet.8.x' || 'test.quiet' }}
MAC_UPDATE_CHANNEL_FILE: ${{ github.event.action == 'released' && 'latest-mac.yml' || 'alpha-mac.yml' }}
steps:
@@ -653,7 +653,7 @@ jobs:
env:
TEST_MODE: ${{ github.event.action == 'prereleased' }}
- S3_BUCKET: ${{ github.event.action == 'released' && 'quiet.7.x' || 'test.quiet' }}
+ S3_BUCKET: ${{ github.event.action == 'released' && 'quiet.8.x' || 'test.quiet' }}
steps:
- uses: actions/checkout@v5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3db97dc2f..e99b831638 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,8 @@
* Include team ID and createdAt in message encryption and validate on consume [#3304](https://github.com/TryQuiet/quiet/issues/3304)
* Require team ID on invite links, use team ID for all chain operations, and hash team name on sigchains [#3296](https://github.com/TryQuiet/quiet/issues/3296)
* Use randomly generated Base58 usernames on sigchain [#3321](https://github.com/TryQuiet/quiet/issues/3321)
+* Update install directories for 8.x [#3338](https://github.com/TryQuiet/quiet/issues/3338)
+* Update S3 bucket for 8.x release binaries [#3346](https://github.com/TryQuiet/quiet/issues/3346as)
## [7.3.0]
From 890dacbb8196611592edf943851eaf8a428ad78e Mon Sep 17 00:00:00 2001
From: Isla <5048549+islathehut@users.noreply.github.com>
Date: Tue, 7 Jul 2026 10:45:53 -0400
Subject: [PATCH 07/18] Publish
- @quiet/desktop@8.0.0-alpha.2
- @quiet/mobile@8.0.0-alpha.2
---
packages/desktop/CHANGELOG.md | 13 +++++++++++++
packages/desktop/package-lock.json | 4 ++--
packages/desktop/package.json | 2 +-
packages/mobile/CHANGELOG.md | 13 +++++++++++++
packages/mobile/android/app/build.gradle | 4 ++--
packages/mobile/ios/Quiet/Info.plist | 2 +-
.../QuietNotificationServiceExtension/Info.plist | 2 +-
packages/mobile/ios/QuietTests/Info.plist | 2 +-
packages/mobile/package-lock.json | 4 ++--
packages/mobile/package.json | 2 +-
10 files changed, 37 insertions(+), 11 deletions(-)
diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md
index c3db97dc2f..bc43fa1bab 100644
--- a/packages/desktop/CHANGELOG.md
+++ b/packages/desktop/CHANGELOG.md
@@ -1,3 +1,16 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [8.0.0-alpha.2](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@8.0.0-alpha.1...@quiet/desktop@8.0.0-alpha.2) (2026-07-07)
+
+**Note:** Version bump only for package @quiet/desktop
+
+
+
+
+
# Changelog
## [8.0.0]
diff --git a/packages/desktop/package-lock.json b/packages/desktop/package-lock.json
index 3ec80c2029..241a647373 100644
--- a/packages/desktop/package-lock.json
+++ b/packages/desktop/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@quiet/desktop",
- "version": "8.0.0-alpha.1",
+ "version": "8.0.0-alpha.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@quiet/desktop",
- "version": "8.0.0-alpha.1",
+ "version": "8.0.0-alpha.2",
"license": "GPL-3.0-or-later",
"dependencies": {
"@dotenvx/dotenvx": "1.39.0",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index e625d23e86..ea543e9166 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -79,7 +79,7 @@
},
"homepage": "https://github.com/TryQuiet",
"@comment version": "To build new version for specific platform, just replace platform in version tag to one of following linux, mac, windows",
- "version": "8.0.0-alpha.1",
+ "version": "8.0.0-alpha.2",
"description": "Decentralized team chat",
"main": "dist/main/main.js",
"scripts": {
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index c3db97dc2f..5b40f120c1 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -1,3 +1,16 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [8.0.0-alpha.2](https://github.com/TryQuiet/quiet/compare/@quiet/mobile@8.0.0-alpha.1...@quiet/mobile@8.0.0-alpha.2) (2026-07-07)
+
+**Note:** Version bump only for package @quiet/mobile
+
+
+
+
+
# Changelog
## [8.0.0]
diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle
index 5296194c4b..1261430000 100644
--- a/packages/mobile/android/app/build.gradle
+++ b/packages/mobile/android/app/build.gradle
@@ -174,8 +174,8 @@ android {
applicationId = "com.quietmobile"
minSdkVersion(rootProject.ext.minSdkVersion)
targetSdkVersion(rootProject.ext.targetSdkVersion)
- versionCode 632
- versionName "8.0.0-alpha.1"
+ versionCode 633
+ versionName "8.0.0-alpha.2"
resValue("string", "build_config_package", "com.quietmobile")
testBuildType = System.getProperty("testBuildType", "debug")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
diff --git a/packages/mobile/ios/Quiet/Info.plist b/packages/mobile/ios/Quiet/Info.plist
index b98875cb86..739e81f968 100644
--- a/packages/mobile/ios/Quiet/Info.plist
+++ b/packages/mobile/ios/Quiet/Info.plist
@@ -34,7 +34,7 @@
CFBundleVersion
- 585
+ 586
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist b/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
index ad79d4bb71..c6be4e01b7 100644
--- a/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
+++ b/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
8.0.0
CFBundleVersion
- 61
+ 62
FirebaseAppDelegateProxyEnabled
NSAppTransportSecurity
diff --git a/packages/mobile/ios/QuietTests/Info.plist b/packages/mobile/ios/QuietTests/Info.plist
index 820f54e972..f8746d75c7 100644
--- a/packages/mobile/ios/QuietTests/Info.plist
+++ b/packages/mobile/ios/QuietTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 573
+ 574
diff --git a/packages/mobile/package-lock.json b/packages/mobile/package-lock.json
index c81b68c5cb..c2c26430af 100644
--- a/packages/mobile/package-lock.json
+++ b/packages/mobile/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@quiet/mobile",
- "version": "8.0.0-alpha.1",
+ "version": "8.0.0-alpha.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@quiet/mobile",
- "version": "8.0.0-alpha.1",
+ "version": "8.0.0-alpha.2",
"dependencies": {
"@d11/react-native-fast-image": "8.11.1",
"@hcaptcha/react-native-hcaptcha": "^2.1.0",
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index b2f6d37918..7efd261b05 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@quiet/mobile",
- "version": "8.0.0-alpha.1",
+ "version": "8.0.0-alpha.2",
"scripts": {
"build": "tsc -p tsconfig.build.json --noEmit",
"storybook-android": "react-native run-android --mode=storybookDebug --appIdSuffix=storybook.debug",
From 802c1e783b55de1f1bf67cad43ac3f4d6e938e62 Mon Sep 17 00:00:00 2001
From: Isla <5048549+islathehut@users.noreply.github.com>
Date: Tue, 7 Jul 2026 10:46:00 -0400
Subject: [PATCH 08/18] Update packages CHANGELOG.md
---
packages/desktop/CHANGELOG.md | 15 ++-------------
packages/mobile/CHANGELOG.md | 15 ++-------------
2 files changed, 4 insertions(+), 26 deletions(-)
diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md
index bc43fa1bab..e99b831638 100644
--- a/packages/desktop/CHANGELOG.md
+++ b/packages/desktop/CHANGELOG.md
@@ -1,16 +1,3 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-# [8.0.0-alpha.2](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@8.0.0-alpha.1...@quiet/desktop@8.0.0-alpha.2) (2026-07-07)
-
-**Note:** Version bump only for package @quiet/desktop
-
-
-
-
-
# Changelog
## [8.0.0]
@@ -38,6 +25,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
* Include team ID and createdAt in message encryption and validate on consume [#3304](https://github.com/TryQuiet/quiet/issues/3304)
* Require team ID on invite links, use team ID for all chain operations, and hash team name on sigchains [#3296](https://github.com/TryQuiet/quiet/issues/3296)
* Use randomly generated Base58 usernames on sigchain [#3321](https://github.com/TryQuiet/quiet/issues/3321)
+* Update install directories for 8.x [#3338](https://github.com/TryQuiet/quiet/issues/3338)
+* Update S3 bucket for 8.x release binaries [#3346](https://github.com/TryQuiet/quiet/issues/3346as)
## [7.3.0]
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index 5b40f120c1..e99b831638 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -1,16 +1,3 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-# [8.0.0-alpha.2](https://github.com/TryQuiet/quiet/compare/@quiet/mobile@8.0.0-alpha.1...@quiet/mobile@8.0.0-alpha.2) (2026-07-07)
-
-**Note:** Version bump only for package @quiet/mobile
-
-
-
-
-
# Changelog
## [8.0.0]
@@ -38,6 +25,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
* Include team ID and createdAt in message encryption and validate on consume [#3304](https://github.com/TryQuiet/quiet/issues/3304)
* Require team ID on invite links, use team ID for all chain operations, and hash team name on sigchains [#3296](https://github.com/TryQuiet/quiet/issues/3296)
* Use randomly generated Base58 usernames on sigchain [#3321](https://github.com/TryQuiet/quiet/issues/3321)
+* Update install directories for 8.x [#3338](https://github.com/TryQuiet/quiet/issues/3338)
+* Update S3 bucket for 8.x release binaries [#3346](https://github.com/TryQuiet/quiet/issues/3346as)
## [7.3.0]
From a45b171dff81338909f7625a1ce6e930be8be1e9 Mon Sep 17 00:00:00 2001
From: Isla <5048549+islathehut@users.noreply.github.com>
Date: Tue, 7 Jul 2026 11:52:11 -0400
Subject: [PATCH 09/18] Publish
- @quiet/desktop@8.0.0
- @quiet/mobile@8.0.0
---
packages/desktop/CHANGELOG.md | 13 +++++++++++++
packages/desktop/package-lock.json | 4 ++--
packages/desktop/package.json | 2 +-
packages/mobile/CHANGELOG.md | 13 +++++++++++++
packages/mobile/android/app/build.gradle | 4 ++--
packages/mobile/ios/Quiet/Info.plist | 2 +-
.../QuietNotificationServiceExtension/Info.plist | 2 +-
packages/mobile/ios/QuietTests/Info.plist | 2 +-
packages/mobile/package-lock.json | 4 ++--
packages/mobile/package.json | 2 +-
10 files changed, 37 insertions(+), 11 deletions(-)
diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md
index e99b831638..79d47ccc56 100644
--- a/packages/desktop/CHANGELOG.md
+++ b/packages/desktop/CHANGELOG.md
@@ -1,3 +1,16 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [8.0.0](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@8.0.0-alpha.2...@quiet/desktop@8.0.0) (2026-07-07)
+
+**Note:** Version bump only for package @quiet/desktop
+
+
+
+
+
# Changelog
## [8.0.0]
diff --git a/packages/desktop/package-lock.json b/packages/desktop/package-lock.json
index 241a647373..38fca024e2 100644
--- a/packages/desktop/package-lock.json
+++ b/packages/desktop/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@quiet/desktop",
- "version": "8.0.0-alpha.2",
+ "version": "8.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@quiet/desktop",
- "version": "8.0.0-alpha.2",
+ "version": "8.0.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"@dotenvx/dotenvx": "1.39.0",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index ea543e9166..69fe6e6f6b 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -79,7 +79,7 @@
},
"homepage": "https://github.com/TryQuiet",
"@comment version": "To build new version for specific platform, just replace platform in version tag to one of following linux, mac, windows",
- "version": "8.0.0-alpha.2",
+ "version": "8.0.0",
"description": "Decentralized team chat",
"main": "dist/main/main.js",
"scripts": {
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index e99b831638..02b223f0d7 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -1,3 +1,16 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+# [8.0.0](https://github.com/TryQuiet/quiet/compare/@quiet/mobile@8.0.0-alpha.2...@quiet/mobile@8.0.0) (2026-07-07)
+
+**Note:** Version bump only for package @quiet/mobile
+
+
+
+
+
# Changelog
## [8.0.0]
diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle
index 1261430000..74d34c97e7 100644
--- a/packages/mobile/android/app/build.gradle
+++ b/packages/mobile/android/app/build.gradle
@@ -174,8 +174,8 @@ android {
applicationId = "com.quietmobile"
minSdkVersion(rootProject.ext.minSdkVersion)
targetSdkVersion(rootProject.ext.targetSdkVersion)
- versionCode 633
- versionName "8.0.0-alpha.2"
+ versionCode 634
+ versionName "8.0.0"
resValue("string", "build_config_package", "com.quietmobile")
testBuildType = System.getProperty("testBuildType", "debug")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
diff --git a/packages/mobile/ios/Quiet/Info.plist b/packages/mobile/ios/Quiet/Info.plist
index 739e81f968..b838183e1d 100644
--- a/packages/mobile/ios/Quiet/Info.plist
+++ b/packages/mobile/ios/Quiet/Info.plist
@@ -34,7 +34,7 @@
CFBundleVersion
- 586
+ 587
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist b/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
index c6be4e01b7..5da6695474 100644
--- a/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
+++ b/packages/mobile/ios/QuietNotificationServiceExtension/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
8.0.0
CFBundleVersion
- 62
+ 63
FirebaseAppDelegateProxyEnabled
NSAppTransportSecurity
diff --git a/packages/mobile/ios/QuietTests/Info.plist b/packages/mobile/ios/QuietTests/Info.plist
index f8746d75c7..b405f49a12 100644
--- a/packages/mobile/ios/QuietTests/Info.plist
+++ b/packages/mobile/ios/QuietTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 574
+ 575
diff --git a/packages/mobile/package-lock.json b/packages/mobile/package-lock.json
index c2c26430af..02f2a87d0a 100644
--- a/packages/mobile/package-lock.json
+++ b/packages/mobile/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@quiet/mobile",
- "version": "8.0.0-alpha.2",
+ "version": "8.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@quiet/mobile",
- "version": "8.0.0-alpha.2",
+ "version": "8.0.0",
"dependencies": {
"@d11/react-native-fast-image": "8.11.1",
"@hcaptcha/react-native-hcaptcha": "^2.1.0",
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index 7efd261b05..14e100c27c 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@quiet/mobile",
- "version": "8.0.0-alpha.2",
+ "version": "8.0.0",
"scripts": {
"build": "tsc -p tsconfig.build.json --noEmit",
"storybook-android": "react-native run-android --mode=storybookDebug --appIdSuffix=storybook.debug",
From 6770fabcfeca91f572f9d1538eeedf3e246a7089 Mon Sep 17 00:00:00 2001
From: Isla <5048549+islathehut@users.noreply.github.com>
Date: Tue, 7 Jul 2026 11:52:18 -0400
Subject: [PATCH 10/18] Update packages CHANGELOG.md
---
packages/desktop/CHANGELOG.md | 13 -------------
packages/mobile/CHANGELOG.md | 13 -------------
2 files changed, 26 deletions(-)
diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md
index 79d47ccc56..e99b831638 100644
--- a/packages/desktop/CHANGELOG.md
+++ b/packages/desktop/CHANGELOG.md
@@ -1,16 +1,3 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-# [8.0.0](https://github.com/TryQuiet/quiet/compare/@quiet/desktop@8.0.0-alpha.2...@quiet/desktop@8.0.0) (2026-07-07)
-
-**Note:** Version bump only for package @quiet/desktop
-
-
-
-
-
# Changelog
## [8.0.0]
diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md
index 02b223f0d7..e99b831638 100644
--- a/packages/mobile/CHANGELOG.md
+++ b/packages/mobile/CHANGELOG.md
@@ -1,16 +1,3 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-# [8.0.0](https://github.com/TryQuiet/quiet/compare/@quiet/mobile@8.0.0-alpha.2...@quiet/mobile@8.0.0) (2026-07-07)
-
-**Note:** Version bump only for package @quiet/mobile
-
-
-
-
-
# Changelog
## [8.0.0]
From bc6561cdcff4a8e75164aa43508bfd3774ac8d73 Mon Sep 17 00:00:00 2001
From: taea
Date: Thu, 9 Jul 2026 17:37:55 -0400
Subject: [PATCH 11/18] stabilize Tor lifecycle across app state changes
---
packages/mobile/ios/Quiet/AppDelegate.h | 5 -
packages/mobile/ios/Quiet/AppDelegate.m | 123 ++--
packages/mobile/ios/TorHandler.swift | 768 +++++++++++++++++++-----
3 files changed, 671 insertions(+), 225 deletions(-)
diff --git a/packages/mobile/ios/Quiet/AppDelegate.h b/packages/mobile/ios/Quiet/AppDelegate.h
index 0cdef8198c..a4ab40a49c 100644
--- a/packages/mobile/ios/Quiet/AppDelegate.h
+++ b/packages/mobile/ios/Quiet/AppDelegate.h
@@ -1,6 +1,5 @@
#import
#import
-#import
#import
#import "RNNodeJsMobile.h"
@@ -8,8 +7,6 @@
// Forward declarations for Swift classes
// (Actual imports happen in AppDelegate.m to avoid circular dependencies)
@class TorHandler;
-@class TORConfiguration;
-@class TORController;
@interface AppDelegate : RCTAppDelegate
@@ -22,7 +19,5 @@
@property (nonatomic, strong) RNNodeJsMobile *nodeJsMobile;
@property (nonatomic, strong) TorHandler *tor;
-@property (nonatomic, strong) TORConfiguration *torConfiguration;
-@property (nonatomic, strong) TORController *torController;
@end
diff --git a/packages/mobile/ios/Quiet/AppDelegate.m b/packages/mobile/ios/Quiet/AppDelegate.m
index 3df7ec57fa..3da44e1fc9 100644
--- a/packages/mobile/ios/Quiet/AppDelegate.m
+++ b/packages/mobile/ios/Quiet/AppDelegate.m
@@ -10,6 +10,9 @@
#import "RNNodeJsMobile.h"
#import "Quiet-Swift.h"
+@interface AppDelegate ()
+@end
+
@implementation AppDelegate
static NSString *const platform = @"mobile";
@@ -58,7 +61,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
// Call only once per nodejs thread
[self createDataDirectory];
- [self spinupBackend:true];
+ [self startTorAndBackend];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
};
@@ -68,9 +71,13 @@ - (void) createDataDirectory {
self.dataPath = [dataDirectory create];
}
-- (void) spinupBackend:(BOOL)init {
+- (void) startTorAndBackend {
+ if (self.tor != nil) {
+ [self.tor enterForeground];
+ return;
+ }
- // (1/4) Find ports to use in tor and backend configuration
+ // Find ports to use in Tor and backend configuration.
Utils *utils = [Utils new];
@@ -91,73 +98,33 @@ - (void) spinupBackend:(BOOL)init {
uint16_t httpTunnelPort = [findFreePort getFirstStartingFromPort:arc4random_uniform(65000 - 1024) + 1024];
- // (2/4) Spawn tor with proper configuration
-
+ // Spawn one Tor instance for the lifetime of this app process. App
+ // background/foreground transitions switch it between DORMANT and ACTIVE.
self.tor = [TorHandler new];
-
- self.torConfiguration = [self.tor getTorConfiguration:socksPort controlPort:controlPort httpTunnelPort:httpTunnelPort];
-
- [self.tor removeOldAuthCookieWithConfiguration:self.torConfiguration];
-
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- [self.tor spawnWithConfiguration:self.torConfiguration];
- });
-
-
- // (3/4) Connect to tor control port natively (so we can use it to shutdown tor when app goes idle)
-
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
- NSData *authCookieData = [self getAuthCookieData];
-
- self.torController = [[TORController alloc] initWithSocketHost:@"127.0.0.1" port:controlPort];
-
- NSError *error = nil;
- // BOOL connected = [self.torController connect:&error];
-
- NSLog(@"Tor control port error %@", error);
-
- [self.torController authenticateWithData:authCookieData completion:^(BOOL success, NSError * _Nullable error) {
- NSString *res = success ? @"YES" : @"NO";
- NSLog(@"Tor control port auth success %@", res);
- NSLog(@"Tor control port auth error %@", error);
- }];
- });
-
- // (4/4) Launch backend or rewire services
-
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
-
- NSString *authCookie = [self getAuthCookie];
-
- if (init) {
- [self launchBackend:controlPort httpTunnelPort:httpTunnelPort authCookie:authCookie];
- } else {
- [self rewireServices:controlPort httpTunnelPort:httpTunnelPort authCookie:authCookie];
- }
- });
-}
-
-- (NSString *) getAuthCookie {
- NSString *authCookie = [self.tor getAuthCookieWithConfiguration:self.torConfiguration];
-
- while (authCookie == nil) {
- authCookie = [self.tor getAuthCookieWithConfiguration:self.torConfiguration];
- };
-
- return authCookie;
+ self.tor.delegate = self;
+ [self.tor startWithSocksPort:socksPort controlPort:controlPort httpTunnelPort:httpTunnelPort];
}
-- (NSData *) getAuthCookieData {
- NSData *authCookie = [self.tor getAuthCookieDataWithConfiguration:self.torConfiguration];
+- (void)torHandlerReady:(TorHandler *)handler
+ controlPort:(uint16_t)controlPort
+ httpTunnelPort:(uint16_t)httpTunnelPort
+ authCookie:(NSString *)authCookie
+{
+ (void)handler;
- while (authCookie == nil) {
- authCookie = [self.tor getAuthCookieDataWithConfiguration:self.torConfiguration];
- };
+ // A readiness callback can race with a background transition. The next
+ // foreground callback will request readiness again, so do nothing here.
+ if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
+ return;
+ }
- return authCookie;
+ if (self.nodeJsMobile == nil) {
+ [self launchBackend:controlPort httpTunnelPort:httpTunnelPort authCookie:authCookie];
+ } else {
+ [self rewireServices:controlPort httpTunnelPort:httpTunnelPort authCookie:authCookie];
+ }
}
-
- (void)launchBackend:(uint16_t)controlPort httpTunnelPort:(uint16_t)httpTunnelPort authCookie:(NSString *)authCookie {
self.nodeJsMobile = [RNNodeJsMobile new];
[self.nodeJsMobile setSocketIOSecret:self.socketIOSecret];
@@ -175,31 +142,10 @@ - (void)rewireServices:(uint16_t)controlPort httpTunnelPort:(uint16_t)httpTunnel
[self.nodeJsMobile sendMessageToNode:@"open":payload];
}
-- (void) stopTor {
- NSLog(@"Sending SIGNAL SHUTDOWN on Tor control port %d", (int)[self.torController isConnected]);
- [self.torController sendCommand:@"SIGNAL SHUTDOWN" arguments:nil data:nil observer:^BOOL(NSArray *codes, NSArray *lines, BOOL *stop) {
- NSUInteger code = codes.firstObject.unsignedIntegerValue;
-
- NSLog(@"Tor control port response code %lu", (unsigned long)code);
-
- if (code != TORControlReplyCodeOK && code != TORControlReplyCodeBadAuthentication)
- return NO;
-
- NSString *message = lines.firstObject ? [[NSString alloc] initWithData:(NSData * _Nonnull)lines.firstObject encoding:NSUTF8StringEncoding] : @"";
-
- NSLog(@"Tor control port response message %@", message);
-
- // BOOL success = (code == TORControlReplyCodeOK && [message isEqualToString:@"OK"]);
-
- *stop = YES;
- return YES;
- }];
-}
-
- (void)applicationDidEnterBackground:(UIApplication *)application
{
QuietSetAppForegroundFlag(NO);
- [self stopTor];
+ [self.tor enterBackground];
NSString * message = [NSString stringWithFormat:@"app:close"];
[self.nodeJsMobile sendMessageToNode:@"close":message];
@@ -226,7 +172,12 @@ - (void)applicationWillEnterForeground:(UIApplication *)application
});
});
- [self spinupBackend:false];
+ [self.tor enterForeground];
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application
+{
+ [self.tor shutdown];
}
/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off.
diff --git a/packages/mobile/ios/TorHandler.swift b/packages/mobile/ios/TorHandler.swift
index 1a62ade0f4..5f03412e32 100644
--- a/packages/mobile/ios/TorHandler.swift
+++ b/packages/mobile/ios/TorHandler.swift
@@ -1,162 +1,662 @@
+import Darwin
+import Foundation
+import OSLog
import Tor
+@objc(TorHandlerDelegate)
+protocol TorHandlerDelegate: AnyObject {
+ @objc(torHandlerReady:controlPort:httpTunnelPort:authCookie:)
+ func torHandlerReady(
+ _ handler: TorHandler,
+ controlPort: UInt16,
+ httpTunnelPort: UInt16,
+ authCookie: String
+ )
+}
+
@objc(TorHandler)
-class TorHandler: NSObject {
-
- @objc(getTorConfiguration:controlPort:httpTunnelPort:)
- func getTorConfiguration(socksPort: in_port_t, controlPort: in_port_t, httpTunnelPort: in_port_t) -> TorConfiguration {
- let conf = TorConfiguration()
+final class TorHandler: NSObject {
+ private enum State: String {
+ case stopped
+ case starting
+ case unknown
+ case active
+ case dormant
+ case stopping
+ }
- let log_loc = "notice stdout"
+ private enum DesiredMode {
+ case stopped
+ case active
+ case dormant
- conf.cookieAuthentication = true
+ var signal: String? {
+ switch self {
+ case .stopped:
+ return nil
+ case .active:
+ return "ACTIVE"
+ case .dormant:
+ return "DORMANT"
+ }
+ }
+ }
- conf.arguments = [
- "--allow-missing-torrc",
- "--ignore-missing-torrc",
- "--ClientOnly", "1",
- "--AvoidDiskWrites", "1",
- "--SocksPort", "127.0.0.1:\(socksPort)",
- "--ControlPort", "127.0.0.1:\(controlPort)",
- "--HTTPTunnelPort", "127.0.0.1:\(httpTunnelPort)",
- "--Log", log_loc,
- ]
+ private struct PendingModeCommand {
+ let id: UInt
+ let generation: UInt
+ let mode: DesiredMode
+ }
- if let dataDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
- .first?.appendingPathComponent("tor", isDirectory: true) {
+ private static let logger = Logger(
+ subsystem: Bundle.main.bundleIdentifier ?? "com.quietmobile",
+ category: "TorHandler"
+ )
+ private static let cookieLength = 32
+ private static let controlTimeout: TimeInterval = 3
+ private static let cookieRetryInterval: TimeInterval = 0.1
+ private static let slowCookieRetryInterval: TimeInterval = 1
+ private static let fastCookieRetryCount = 300
+ private static let stableUptime: TimeInterval = 60
+ private static let sharedLifecycleQueue = DispatchQueue(label: "com.quietmobile.tor-lifecycle")
- // Create tor data directory if it does not yet exist.
- try? FileManager.default.createDirectory(at: dataDir, withIntermediateDirectories: true)
+ // Tor.framework registers callbacks globally and appends each registration.
+ // A static initializer guarantees that we install one pair per process.
+ private static let installLoggingCallbacks: Void = {
+ TORInstallTorLoggingCallback { severity, message in
+ guard severity != .debug, severity != .info else { return }
+ let text = String(cString: message).trimmingCharacters(in: .whitespacesAndNewlines)
+ if severity == .error || severity == .fault {
+ TorHandler.logger.error("Tor: \(text, privacy: .public)")
+ } else {
+ TorHandler.logger.notice("Tor: \(text, privacy: .public)")
+ }
+ }
- conf.dataDirectory = dataDir
- }
+ TORInstallEventLoggingCallback { severity, message in
+ guard severity != .debug, severity != .info else { return }
+ let text = String(cString: message).trimmingCharacters(in: .whitespacesAndNewlines)
+ if severity == .error || severity == .fault {
+ TorHandler.logger.error("libevent: \(text, privacy: .public)")
+ } else {
+ TorHandler.logger.notice("libevent: \(text, privacy: .public)")
+ }
+ }
+ }()
+
+ @objc weak var delegate: TorHandlerDelegate?
+
+ // Tor is process-global. Sharing this queue makes the active-thread check and
+ // construction atomic even if a second TorHandler is created accidentally.
+ private var lifecycleQueue: DispatchQueue { Self.sharedLifecycleQueue }
+ private var state = State.stopped
+ private var desiredMode = DesiredMode.stopped
- return conf
- }
+ private var configuration: TorConfiguration?
+ private var controlPort: UInt16 = 0
+ private var httpTunnelPort: UInt16 = 0
private var torThread: TorThread?
-
- @objc
- func spawn(configuration: TorConfiguration) -> Void {
-
- #if DEBUG
- print("[\(String(describing: type(of: self)))] arguments=\(String(describing: configuration.arguments))")
- #endif
-
- var cookiePath: String? {
- if let cookieUrl = configuration.dataDirectory?.appendingPathComponent("control_auth_cookie") {
- return cookieUrl.absoluteString
- }
-
- return nil
- }
-
- if cookiePath != nil && FileManager.default.fileExists(atPath: cookiePath!) {
- do {
- try FileManager.default.removeItem(atPath: cookiePath!)
- } catch {
- print("Could not delete cookie file, probably read-only filesystem")
- }
- }
-
- // Start Tor
- torThread = TorThread(configuration: configuration)
- torThread?.start()
-
- print("[\(String(describing: type(of: self)))] Starting Tor")
-
- // Wait long enough for Tor itself to have started. It's OK to wait for this
- // because Tor is already trying to connect; this is just the part that polls for
- // progress.
- DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
- TORInstallTorLoggingCallback { severity, msg in
- let s: String
-
- switch severity {
- case .debug:
- return
-
- case .error:
- s = "error"
-
- case .fault:
- s = "fault"
-
- case .info:
- // s = "info"
- return
-
- default:
- s = "default"
- }
-
- print("[Tor \(s)] \(String(cString: msg).trimmingCharacters(in: .whitespacesAndNewlines))")
- }
- TORInstallEventLoggingCallback { severity, msg in
- let s: String
-
- switch severity {
- case .debug:
- return
-
- case .error:
- s = "error"
-
- case .fault:
- s = "fault"
-
- case .info:
- // s = "info"
- return
-
- default:
- s = "default"
+ private var controller: TorController?
+ private var monitorTimer: DispatchSourceTimer?
+
+ private var generation: UInt = 0
+ private var authenticationAttempt: UInt = 0
+ private var commandSequence: UInt = 0
+ private var controllerRetrySequence: UInt = 0
+ private var readinessSequence: UInt = 0
+ private var scheduledControllerRetry: UInt?
+ private var scheduledReadiness: UInt?
+ private var pendingModeCommand: PendingModeCommand?
+ private var restartScheduled = false
+ private var restartAttempts = 0
+ private var authenticationInFlight = false
+ private var readyNotificationPending = false
+
+ private var cookieData: Data?
+ private var authCookie: String?
+
+ override init() {
+ super.init()
+ _ = Self.installLoggingCallbacks
+ }
+
+ deinit {
+ monitorTimer?.cancel()
+ }
+
+ @objc(startWithSocksPort:controlPort:httpTunnelPort:)
+ func start(socksPort: UInt16, controlPort: UInt16, httpTunnelPort: UInt16) {
+ lifecycleQueue.async { [weak self] in
+ guard let self else { return }
+
+ if self.configuration == nil {
+ self.configuration = self.makeConfiguration(
+ socksPort: socksPort,
+ controlPort: controlPort,
+ httpTunnelPort: httpTunnelPort
+ )
+ self.controlPort = controlPort
+ self.httpTunnelPort = httpTunnelPort
+ }
+
+ self.desiredMode = .active
+ self.readyNotificationPending = true
+ self.ensureThreadRunning()
+ }
+ }
+
+ /// Wake the existing Tor instance and notify the app when it is usable.
+ /// Repeated foreground callbacks are coalesced on the lifecycle queue.
+ @objc func enterForeground() {
+ lifecycleQueue.async { [weak self] in
+ guard let self else { return }
+ self.desiredMode = .active
+ self.readyNotificationPending = true
+ self.ensureThreadRunning()
+ self.applyDesiredMode()
+ }
+ }
+
+ /// Put Tor into its low-activity mode without destroying process-global state.
+ @objc func enterBackground() {
+ lifecycleQueue.async { [weak self] in
+ guard let self else { return }
+ self.desiredMode = .dormant
+ self.readyNotificationPending = false
+ self.scheduledReadiness = nil
+ self.applyDesiredMode()
+ }
+ }
+
+ /// Explicit teardown is reserved for process termination. Normal app lifecycle
+ /// transitions use ACTIVE/DORMANT so Tor is never initialized concurrently.
+ @objc func shutdown() {
+ lifecycleQueue.async { [weak self] in
+ guard let self else { return }
+ self.desiredMode = .stopped
+ self.readyNotificationPending = false
+ self.scheduledReadiness = nil
+ self.restartScheduled = false
+ self.state = .stopping
+ self.pendingModeCommand = nil
+
+ guard let controller = self.controller, controller.isConnected else { return }
+ controller.sendCommand("SIGNAL SHUTDOWN", arguments: nil, data: nil) { _, _, stop in
+ stop.pointee = true
+ return true
+ }
+ }
+ }
+
+ private func makeConfiguration(
+ socksPort: UInt16,
+ controlPort: UInt16,
+ httpTunnelPort: UInt16
+ ) -> TorConfiguration {
+ let configuration = TorConfiguration()
+ configuration.cookieAuthentication = true
+ configuration.arguments = [
+ "--allow-missing-torrc",
+ "--ignore-missing-torrc",
+ "--ClientOnly", "1",
+ "--AvoidDiskWrites", "1",
+ "--SocksPort", "127.0.0.1:\(socksPort)",
+ "--ControlPort", "127.0.0.1:\(controlPort)",
+ "--HTTPTunnelPort", "127.0.0.1:\(httpTunnelPort)",
+ "--Log", "notice stdout",
+ ]
+
+ if let dataDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
+ .first?.appendingPathComponent("tor", isDirectory: true) {
+ try? FileManager.default.createDirectory(at: dataDirectory, withIntermediateDirectories: true)
+ configuration.dataDirectory = dataDirectory
+ }
+
+ return configuration
+ }
+
+ private func ensureThreadRunning() {
+ guard desiredMode != .stopped, configuration != nil else { return }
+
+ if let thread = torThread {
+ if thread.isFinished {
+ handleThreadExit(generation: generation)
+ }
+ return
+ }
+
+ // Tor.framework only keeps a weak process-global reference and does not
+ // enforce this in release builds. Treat any active framework thread as a
+ // hard serialization barrier, even if it was not created by this handler.
+ if let activeThread = TorThread.active {
+ if activeThread.isFinished {
+ scheduleThreadCheck(after: 0.25)
+ } else {
+ Self.logger.error("Refusing to initialize Tor while another Tor thread is active")
+ scheduleThreadCheck(after: 1)
+ }
+ return
+ }
+
+ startThread()
+ }
+
+ private func startThread() {
+ guard let configuration, desiredMode != .stopped, torThread == nil else { return }
+
+ removeAuthCookie(configuration: configuration)
+ generation &+= 1
+ let currentGeneration = generation
+
+ controller = nil
+ cookieData = nil
+ authCookie = nil
+ authenticationInFlight = false
+ authenticationAttempt &+= 1
+ scheduledControllerRetry = nil
+ scheduledReadiness = nil
+ pendingModeCommand = nil
+ state = .starting
+ restartScheduled = false
+
+ let thread = TorThread(configuration: configuration)
+ torThread = thread
+ thread.start()
+
+ Self.logger.info("Started Tor generation \(currentGeneration)")
+ startThreadMonitor(generation: currentGeneration)
+ pollForCookie(generation: currentGeneration, attempt: 0)
+
+ lifecycleQueue.asyncAfter(deadline: .now() + Self.stableUptime) { [weak self] in
+ guard let self,
+ currentGeneration == self.generation,
+ self.torThread?.isFinished == false else { return }
+ self.restartAttempts = 0
+ }
+ }
+
+ private func startThreadMonitor(generation: UInt) {
+ monitorTimer?.cancel()
+
+ let timer = DispatchSource.makeTimerSource(queue: lifecycleQueue)
+ timer.schedule(deadline: .now() + 0.25, repeating: 0.25)
+ timer.setEventHandler { [weak self] in
+ guard let self else { return }
+ guard generation == self.generation else {
+ timer.cancel()
+ return
+ }
+ guard self.torThread?.isFinished == true else { return }
+ self.handleThreadExit(generation: generation)
+ }
+ monitorTimer = timer
+ timer.resume()
+ }
+
+ private func handleThreadExit(generation exitedGeneration: UInt) {
+ guard exitedGeneration == generation else { return }
+
+ monitorTimer?.cancel()
+ monitorTimer = nil
+ controller = nil
+ cookieData = nil
+ authCookie = nil
+ authenticationInFlight = false
+ authenticationAttempt &+= 1
+ scheduledControllerRetry = nil
+ scheduledReadiness = nil
+ pendingModeCommand = nil
+ torThread = nil
+ state = .stopped
+
+ guard desiredMode == .active else {
+ Self.logger.info("Tor stopped while the app does not require an active connection")
+ return
+ }
+
+ readyNotificationPending = true
+ restartAttempts += 1
+ let exponent = min(max(restartAttempts - 1, 0), 5)
+ let delay = min(pow(2, Double(exponent)), 30)
+ Self.logger.error("Tor exited unexpectedly; scheduling recovery in \(delay, privacy: .public) seconds")
+ scheduleRestart(after: delay, exitedGeneration: exitedGeneration)
+ }
+
+ private func scheduleRestart(after delay: TimeInterval, exitedGeneration: UInt) {
+ guard !restartScheduled else { return }
+ restartScheduled = true
+
+ lifecycleQueue.asyncAfter(deadline: .now() + delay) { [weak self] in
+ guard let self else { return }
+ guard self.generation == exitedGeneration, self.desiredMode == .active else {
+ self.restartScheduled = false
+ return
+ }
+ self.restartScheduled = false
+ self.ensureThreadRunning()
+ }
+ }
+
+ private func scheduleThreadCheck(after delay: TimeInterval) {
+ guard !restartScheduled else { return }
+ restartScheduled = true
+ lifecycleQueue.asyncAfter(deadline: .now() + delay) { [weak self] in
+ guard let self else { return }
+ self.restartScheduled = false
+ self.ensureThreadRunning()
+ }
+ }
+
+ private func pollForCookie(generation: UInt, attempt: Int) {
+ guard generation == self.generation,
+ desiredMode != .stopped,
+ let thread = torThread else { return }
+ guard !thread.isFinished else {
+ handleThreadExit(generation: generation)
+ return
+ }
+
+ if let configuration,
+ let data = readAuthCookie(configuration: configuration),
+ data.count == Self.cookieLength {
+ cookieData = data
+ connectController(generation: generation)
+ return
+ }
+
+ if attempt == Self.fastCookieRetryCount {
+ Self.logger.error("Tor control cookie is still unavailable; continuing low-frequency readiness checks")
+ }
+
+ let delay = attempt < Self.fastCookieRetryCount
+ ? Self.cookieRetryInterval
+ : Self.slowCookieRetryInterval
+ lifecycleQueue.asyncAfter(deadline: .now() + delay) { [weak self] in
+ self?.pollForCookie(generation: generation, attempt: attempt + 1)
+ }
+ }
+
+ private func connectController(generation: UInt) {
+ guard generation == self.generation,
+ desiredMode != .stopped,
+ let thread = torThread,
+ !thread.isFinished,
+ let cookieData else { return }
+ guard !authenticationInFlight else { return }
+
+ // A direct lifecycle event supersedes a delayed retry. Its sequence token
+ // makes any already-enqueued retry closure a no-op.
+ scheduledControllerRetry = nil
+
+ let control: TorController
+ if let existingController = controller, existingController.isConnected {
+ if authCookie != nil {
+ applyDesiredMode()
+ return
+ }
+ control = existingController
+ } else {
+ controller = nil
+ guard controlPortIsReachable() else {
+ Self.logger.debug("Tor control port is not ready; retrying")
+ scheduleControllerRetry(generation: generation)
+ return
+ }
+
+ let newController = TorController(socketHost: "127.0.0.1", port: controlPort)
+ guard newController.isConnected else {
+ Self.logger.debug("Tor control port is not ready; retrying")
+ scheduleControllerRetry(generation: generation)
+ return
+ }
+ controller = newController
+ control = newController
+ }
+
+ authenticationInFlight = true
+ authenticationAttempt &+= 1
+ let attempt = authenticationAttempt
+
+ control.authenticate(with: cookieData) { [weak self, weak control] success, error in
+ self?.lifecycleQueue.async {
+ guard let self,
+ generation == self.generation,
+ attempt == self.authenticationAttempt,
+ control === self.controller else { return }
+
+ self.authenticationInFlight = false
+ if success {
+ self.authCookie = cookieData.hexEncodedString()
+ self.applyDesiredMode()
+ } else {
+ Self.logger.error("Tor control authentication failed: \(error?.localizedDescription ?? "unknown error", privacy: .public)")
+ self.cookieData = nil
+ self.authCookie = nil
+ self.lifecycleQueue.asyncAfter(deadline: .now() + 0.5) { [weak self] in
+ self?.pollForCookie(generation: generation, attempt: 0)
+ }
}
-
- print("[libevent \(s)] \(String(cString: msg).trimmingCharacters(in: .whitespacesAndNewlines))")
}
- })
+ }
+
+ lifecycleQueue.asyncAfter(deadline: .now() + Self.controlTimeout) { [weak self, weak control] in
+ guard let self,
+ generation == self.generation,
+ attempt == self.authenticationAttempt,
+ self.authenticationInFlight,
+ control === self.controller else { return }
+
+ self.authenticationInFlight = false
+ self.authenticationAttempt &+= 1
+ self.controller = nil
+ Self.logger.debug("Tor control authentication timed out; reconnecting")
+ self.scheduleControllerRetry(generation: generation)
+ }
+ }
+
+ private func scheduleControllerRetry(generation: UInt) {
+ guard generation == self.generation,
+ desiredMode != .stopped,
+ scheduledControllerRetry == nil else { return }
+
+ controllerRetrySequence &+= 1
+ let retry = controllerRetrySequence
+ scheduledControllerRetry = retry
+
+ lifecycleQueue.asyncAfter(deadline: .now() + 0.5) { [weak self] in
+ guard let self,
+ self.scheduledControllerRetry == retry,
+ generation == self.generation else { return }
+ self.scheduledControllerRetry = nil
+ self.connectController(generation: generation)
+ }
}
-
- @objc
- func getAuthCookie(configuration: TorConfiguration) -> String? {
- var auth: Data? {
- if let cookieUrl = configuration.dataDirectory?.appendingPathComponent("control_auth_cookie") {
- return try? Data(contentsOf: cookieUrl)
+
+ private func applyDesiredMode() {
+ guard desiredMode != .stopped else { return }
+
+ guard torThread != nil else {
+ ensureThreadRunning()
+ return
+ }
+ guard torThread?.isFinished != true else {
+ handleThreadExit(generation: generation)
+ return
+ }
+ guard let controller, controller.isConnected, authCookie != nil else {
+ if !authenticationInFlight, cookieData != nil {
+ connectController(generation: generation)
}
+ return
+ }
+ guard pendingModeCommand == nil else { return }
- return nil
+ if desiredMode == .active, state == .active {
+ notifyReadyIfNeeded()
+ return
}
-
- guard let cookie = auth else {
- return nil
+ if desiredMode == .dormant, state == .dormant {
+ return
}
-
- return cookie.hexEncodedString()
+
+ sendModeCommand(desiredMode, controller: controller)
}
-
- @objc
- func getAuthCookieData(configuration: TorConfiguration) -> NSData? {
- var auth: NSData? {
- if let cookieUrl = configuration.dataDirectory?.appendingPathComponent("control_auth_cookie") {
- return NSData(contentsOf: cookieUrl)
+
+ private func sendModeCommand(_ mode: DesiredMode, controller: TorController) {
+ guard let signal = mode.signal else { return }
+
+ commandSequence &+= 1
+ let command = PendingModeCommand(id: commandSequence, generation: generation, mode: mode)
+ pendingModeCommand = command
+
+ controller.sendCommand("SIGNAL \(signal)", arguments: nil, data: nil) { [weak self] codes, _, stop in
+ guard let code = codes.first?.intValue else { return false }
+ stop.pointee = true
+ let success = code == 250
+ self?.lifecycleQueue.async {
+ self?.finishModeCommand(command, success: success)
}
+ return true
+ }
- return nil
+ lifecycleQueue.asyncAfter(deadline: .now() + Self.controlTimeout) { [weak self, weak controller] in
+ guard let self,
+ self.pendingModeCommand?.id == command.id,
+ self.pendingModeCommand?.generation == command.generation,
+ controller === self.controller else { return }
+
+ self.pendingModeCommand = nil
+ self.state = .unknown
+ self.controller = nil
+ Self.logger.debug("Tor mode command timed out; reconnecting the controller")
+ self.connectController(generation: command.generation)
}
-
- guard let cookie = auth else {
- return nil
+ }
+
+ private func finishModeCommand(_ command: PendingModeCommand, success: Bool) {
+ guard pendingModeCommand?.id == command.id,
+ command.generation == generation else { return }
+
+ pendingModeCommand = nil
+ if success {
+ state = command.mode == .active ? .active : .dormant
+ if desiredMode == command.mode {
+ if command.mode == .active {
+ notifyReadyIfNeeded()
+ }
+ return
+ }
+ applyDesiredMode()
+ } else {
+ state = .unknown
+ scheduleControllerRetry(generation: generation)
}
-
- return cookie
}
-
- @objc
- func removeOldAuthCookie(configuration: TorConfiguration) -> Void {
- if let cookieUrl = configuration.dataDirectory?.appendingPathComponent("control_auth_cookie") {
- print("[\(String(describing: type(of: self)))] Removing old auth cookie")
- try? FileManager.default.removeItem(at: cookieUrl)
+
+ private func notifyReadyIfNeeded() {
+ guard desiredMode == .active,
+ readyNotificationPending,
+ scheduledReadiness == nil,
+ state == .active,
+ torThread?.isFinished == false,
+ controller?.isConnected == true,
+ !authenticationInFlight,
+ let authCookie else { return }
+
+ readinessSequence &+= 1
+ let readiness = readinessSequence
+ let currentGeneration = generation
+ scheduledReadiness = readiness
+ let controlPort = controlPort
+ let httpTunnelPort = httpTunnelPort
+
+ DispatchQueue.main.async { [weak self] in
+ guard let self else { return }
+ let shouldDeliver = self.lifecycleQueue.sync {
+ guard self.scheduledReadiness == readiness else { return false }
+ self.scheduledReadiness = nil
+
+ guard self.generation == currentGeneration,
+ self.desiredMode == .active,
+ self.state == .active,
+ self.torThread?.isFinished == false,
+ self.controller?.isConnected == true,
+ !self.authenticationInFlight,
+ self.authCookie == authCookie else {
+ self.applyDesiredMode()
+ return false
+ }
+
+ self.readyNotificationPending = false
+ return true
+ }
+ guard shouldDeliver else { return }
+
+ self.delegate?.torHandlerReady(
+ self,
+ controlPort: controlPort,
+ httpTunnelPort: httpTunnelPort,
+ authCookie: authCookie
+ )
}
}
+
+ private func controlPortIsReachable() -> Bool {
+ let socketDescriptor = Darwin.socket(AF_INET, SOCK_STREAM, 0)
+ guard socketDescriptor >= 0 else { return false }
+ defer { Darwin.close(socketDescriptor) }
+
+ guard fcntl(socketDescriptor, F_SETFL, O_NONBLOCK) != -1 else { return false }
+
+ var address = sockaddr_in()
+ address.sin_len = UInt8(MemoryLayout.size)
+ address.sin_family = sa_family_t(AF_INET)
+ address.sin_port = controlPort.bigEndian
+ address.sin_addr.s_addr = inet_addr("127.0.0.1")
+
+ let connectionResult = withUnsafePointer(to: &address) { pointer in
+ pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { socketAddress in
+ Darwin.connect(
+ socketDescriptor,
+ socketAddress,
+ socklen_t(MemoryLayout.size)
+ )
+ }
+ }
+
+ if connectionResult == 0 {
+ return true
+ }
+ guard errno == EINPROGRESS else { return false }
+
+ var descriptor = pollfd(fd: socketDescriptor, events: Int16(POLLOUT), revents: 0)
+ guard Darwin.poll(&descriptor, 1, 100) > 0 else { return false }
+
+ var socketError: Int32 = 0
+ var socketErrorLength = socklen_t(MemoryLayout.size)
+ guard Darwin.getsockopt(
+ socketDescriptor,
+ SOL_SOCKET,
+ SO_ERROR,
+ &socketError,
+ &socketErrorLength
+ ) == 0 else { return false }
+
+ return socketError == 0
+ }
+
+ private func authCookieURL(configuration: TorConfiguration) -> URL? {
+ configuration.dataDirectory?.appendingPathComponent("control_auth_cookie", isDirectory: false)
+ }
+
+ private func removeAuthCookie(configuration: TorConfiguration) {
+ guard let url = authCookieURL(configuration: configuration) else { return }
+ try? FileManager.default.removeItem(at: url)
+ }
+
+ private func readAuthCookie(configuration: TorConfiguration) -> Data? {
+ guard let url = authCookieURL(configuration: configuration) else { return nil }
+ return try? Data(contentsOf: url)
+ }
}
From 686574b131c8d7d6304d7229fdd14eab3d6aa271 Mon Sep 17 00:00:00 2001
From: taea
Date: Mon, 13 Jul 2026 19:17:06 -0400
Subject: [PATCH 12/18] feat: Enhance message logging and background node
project startup
- Added a new function `getMessageEnvelopeLogMetadata` to extract metadata from message envelopes for improved logging.
- Updated logging in `MessageCodec` to provide more detailed information during payload parsing and envelope deserialization.
- Refactored `startNodeProject` method in `RNNodeJsMobile` to run the Node project in the background, preventing UI blocking.
- Adjusted `launchBackend` in `AppDelegate` to utilize the new background startup method.
- Updated Podfile.lock to reflect changes in Yoga dependency version.
- Modified Xcode project settings to ensure proper framework linking and resource management.
- Enhanced `TorHandler` to include a list of directory authorities for improved bootstrap independence.
---
packages/backend/package.json | 5 +-
packages/backend/src/backendManager.ts | 61 ++--
.../src/mobile-lifecycle-coordinator.spec.ts | 117 ++++++++
.../src/mobile-lifecycle-coordinator.ts | 90 ++++++
.../src/mobile-lifecycle-coordinator.types.ts | 13 +
.../connections-manager.service.spec.ts | 31 ++-
.../connections-manager.service.ts | 10 +-
.../notifications/notificationTokens.store.ts | 20 +-
.../storage/userProfile/userProfile.store.ts | 20 +-
.../src/nest/tor/tor-control.service.spec.ts | 61 ++++
.../src/nest/tor/tor-control.service.ts | 14 +-
.../backend/src/nest/tor/tor.service.spec.ts | 260 ++++++++++++++++++
packages/backend/src/nest/tor/tor.service.ts | 181 +++++++++---
packages/backend/src/rn-bridge.spec.ts | 75 +++++
packages/backend/src/rn-bridge.ts | 46 +++-
.../mobile/ios/NodeJsMobile/RNNodeJsMobile.h | 2 +-
.../mobile/ios/NodeJsMobile/RNNodeJsMobile.m | 9 +-
packages/mobile/ios/Podfile.lock | 2 +-
.../ios/Quiet.xcodeproj/project.pbxproj | 138 +++++-----
packages/mobile/ios/Quiet/AppDelegate.m | 3 +-
packages/mobile/ios/TorHandler.swift | 37 ++-
21 files changed, 1012 insertions(+), 183 deletions(-)
create mode 100644 packages/backend/src/mobile-lifecycle-coordinator.spec.ts
create mode 100644 packages/backend/src/mobile-lifecycle-coordinator.ts
create mode 100644 packages/backend/src/mobile-lifecycle-coordinator.types.ts
create mode 100644 packages/backend/src/nest/tor/tor-control.service.spec.ts
create mode 100644 packages/backend/src/nest/tor/tor.service.spec.ts
create mode 100644 packages/backend/src/rn-bridge.spec.ts
diff --git a/packages/backend/package.json b/packages/backend/package.json
index d95da929ca..9831581e0f 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -10,8 +10,9 @@
"private": true,
"scripts": {
"build": "tsc -p tsconfig.build.json",
- "webpack": "webpack --env mode=development && cp ./lib/bundle.cjs ../backend-bundle/bundle.cjs",
- "webpack:prod": "webpack --env mode=production && cp ./lib/bundle.cjs ../backend-bundle/bundle.cjs",
+ "webpack": "webpack --env mode=development && npm run sync-bundle-consumers",
+ "webpack:prod": "webpack --env mode=production && npm run sync-bundle-consumers",
+ "sync-bundle-consumers": "cp ./lib/bundle.cjs ../backend-bundle/bundle.cjs && mkdir -p ../mobile/nodejs-assets/nodejs-project && cp ./lib/bundle.cjs ../mobile/nodejs-assets/nodejs-project/bundle.cjs",
"postinstall": "npm run applyPatches",
"applyPatches": "npm run applyPatches:it && npm run applyPatches:libp2p",
"applyPatches:libp2p": "patch -f -p0 < ./patch/libp2p/mplex.patch || true",
diff --git a/packages/backend/src/backendManager.ts b/packages/backend/src/backendManager.ts
index a5de3cf7db..fe5b6eb69d 100644
--- a/packages/backend/src/backendManager.ts
+++ b/packages/backend/src/backendManager.ts
@@ -15,10 +15,19 @@ import { HttpsProxyAgent } from 'https-proxy-agent'
import { randomBytes } from 'crypto'
import { sleep } from './nest/common/sleep'
import { type BackendLeaveCommunityMessage } from '@quiet/types'
+import { MobileLifecycleCoordinator } from './mobile-lifecycle-coordinator'
// Shutdown helper constants
const SHUTDOWN_TIMEOUT = 60_000 // 1 minute
+const parseMobilePort = (value: unknown, name: string): number => {
+ const port = Number(value)
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
+ throw new Error(`Invalid mobile ${name}`)
+ }
+ return port
+}
+
const logger = createLogger('backendManager')
logger.info('Launching backend manager')
@@ -232,13 +241,21 @@ export const runBackendMobile = async (rn_bridge: any, secret: string) => {
process.env['BACKEND'] = 'mobile'
process.env['CONNECTION_TIME'] = (new Date().getTime() / 1000).toString()
+ const socketIOPort = parseMobilePort(options.dataPort, 'data port')
+ const httpTunnelPort = options.httpTunnelPort
+ ? parseMobilePort(options.httpTunnelPort, 'HTTP tunnel port')
+ : undefined
+ const torControlPort = options.controlPort
+ ? parseMobilePort(options.controlPort, 'Tor control port')
+ : await getPort()
+
const app: INestApplicationContext = await NestFactory.createApplicationContext(
AppModule.forOptions({
- socketIOPort: options.dataPort,
+ socketIOPort,
socketIOSecret: secret,
- httpTunnelPort: options.httpTunnelPort ? options.httpTunnelPort : null,
+ httpTunnelPort,
torAuthCookie: options.authCookie ? options.authCookie : null,
- torControlPort: options.controlPort ? options.controlPort : await getPort(),
+ torControlPort,
torBinaryPath: options.torBinary ? options.torBinary : null,
options: {
env: {
@@ -249,11 +266,29 @@ export const runBackendMobile = async (rn_bridge: any, secret: string) => {
}),
{ logger: ['warn', 'error', 'log', 'debug', 'verbose'] }
)
- let proxyAgent: HttpsProxyAgent | undefined
+ const connectionsManager = app.get(ConnectionsManagerService)
+ const tor = app.get(Tor)
+ const proxyAgent = app.get>(SOCKS_PROXY_AGENT)
+ const mobileLifecycle = new MobileLifecycleCoordinator({
+ pause: async () => connectionsManager.pause(),
+ activate: async (msg: OpenServices) => {
+ const torControlPort = parseMobilePort(msg.torControlPort, 'Tor control port')
+ const httpTunnelPort = parseMobilePort(msg.httpTunnelPort, 'HTTP tunnel port')
+ tor.rewireNativeTor({
+ controlPort: torControlPort,
+ httpTunnelPort,
+ authCookie: msg.authCookie,
+ })
+ proxyAgent.connectOpts.port = httpTunnelPort
+ proxyAgent.proxy.port = httpTunnelPort.toString()
+ await connectionsManager.resume()
+ },
+ })
let shutdownRequestedFromBridge = false
rn_bridge.channel.on('close', () => {
- const connectionsManager = app.get(ConnectionsManagerService)
- connectionsManager.pause()
+ void mobileLifecycle.pause().catch(error => {
+ logger.error('Failed to pause mobile services', error)
+ })
})
rn_bridge.channel.on('hibernate', async () => {
logger.info('Received hibernate message from RN bridge')
@@ -276,19 +311,9 @@ export const runBackendMobile = async (rn_bridge: any, secret: string) => {
}
})
rn_bridge.channel.on('open', (msg: OpenServices) => {
- const connectionsManager = app.get(ConnectionsManagerService)
- const tor = app.get(Tor)
- proxyAgent = app.get>(SOCKS_PROXY_AGENT)
- const torControlPort = Number(msg.torControlPort)
- const httpTunnelPort = Number(msg.httpTunnelPort)
- tor.rewireNativeTor({
- controlPort: torControlPort,
- httpTunnelPort,
- authCookie: msg.authCookie,
+ void mobileLifecycle.activate(msg).catch(error => {
+ logger.error('Failed to activate mobile services', error)
})
- proxyAgent.connectOpts.port = httpTunnelPort
- proxyAgent.proxy.port = httpTunnelPort.toString()
- connectionsManager.resume()
})
const shutdown = setupGracefulShutdown(app, () => app.get(ConnectionsManagerService))
rn_bridge.channel.on('shutdown', async () => {
diff --git a/packages/backend/src/mobile-lifecycle-coordinator.spec.ts b/packages/backend/src/mobile-lifecycle-coordinator.spec.ts
new file mode 100644
index 0000000000..624dfa98cc
--- /dev/null
+++ b/packages/backend/src/mobile-lifecycle-coordinator.spec.ts
@@ -0,0 +1,117 @@
+import { jest } from '@jest/globals'
+
+import { MobileLifecycleCoordinator } from './mobile-lifecycle-coordinator'
+import type { OpenServices } from './options'
+
+const deferred = () => {
+ let resolve!: (value: T | PromiseLike) => void
+ let reject!: (reason?: unknown) => void
+ const promise = new Promise((promiseResolve, promiseReject) => {
+ resolve = promiseResolve
+ reject = promiseReject
+ })
+ return { promise, reject, resolve }
+}
+
+const services = (authCookie: string): OpenServices => ({
+ authCookie,
+ httpTunnelPort: 12_345,
+ torControlPort: 23_456,
+})
+
+describe('MobileLifecycleCoordinator', () => {
+ it('serializes active behind an in-flight pause', async () => {
+ const pauseDeferred = deferred()
+ const pause = jest.fn(() => pauseDeferred.promise)
+ const activate = jest.fn(async (_services: OpenServices) => undefined)
+ const coordinator = new MobileLifecycleCoordinator({ activate, pause })
+
+ const pauseRequest = coordinator.pause()
+ const activeRequest = coordinator.activate(services('cookie-a'))
+
+ expect(pause).toHaveBeenCalledTimes(1)
+ expect(activate).not.toHaveBeenCalled()
+
+ pauseDeferred.resolve()
+ await Promise.all([pauseRequest, activeRequest])
+
+ expect(activate).toHaveBeenCalledTimes(1)
+ expect(activate).toHaveBeenCalledWith(services('cookie-a'))
+ })
+
+ it('coalesces pause-active-pause to the final paused intent', async () => {
+ const pauseDeferred = deferred()
+ const pause = jest.fn(() => pauseDeferred.promise)
+ const activate = jest.fn(async (_services: OpenServices) => undefined)
+ const coordinator = new MobileLifecycleCoordinator({ activate, pause })
+
+ const firstPause = coordinator.pause()
+ const active = coordinator.activate(services('cookie-a'))
+ const finalPause = coordinator.pause()
+
+ pauseDeferred.resolve()
+ await Promise.all([firstPause, active, finalPause])
+
+ expect(pause).toHaveBeenCalledTimes(1)
+ expect(activate).not.toHaveBeenCalled()
+ })
+
+ it('applies the newest active payload and skips an obsolete pause', async () => {
+ const firstActiveDeferred = deferred()
+ const pause = jest.fn(async () => undefined)
+ const activate = jest.fn((payload: OpenServices) => {
+ if (payload.authCookie === 'cookie-a') {
+ return firstActiveDeferred.promise
+ }
+ return Promise.resolve()
+ })
+ const coordinator = new MobileLifecycleCoordinator({ activate, pause })
+
+ const firstActive = coordinator.activate(services('cookie-a'))
+ const pauseRequest = coordinator.pause()
+ const latestActive = coordinator.activate(services('cookie-b'))
+
+ expect(activate).toHaveBeenCalledTimes(1)
+ firstActiveDeferred.resolve()
+ await Promise.all([firstActive, pauseRequest, latestActive])
+
+ expect(pause).not.toHaveBeenCalled()
+ expect(activate).toHaveBeenCalledTimes(2)
+ expect(activate).toHaveBeenNthCalledWith(1, services('cookie-a'))
+ expect(activate).toHaveBeenNthCalledWith(2, services('cookie-b'))
+ })
+
+ it('applies a queued newer intent before reporting an earlier failure', async () => {
+ const error = new Error('pause failed')
+ const pauseDeferred = deferred()
+ const pause = jest.fn(() => pauseDeferred.promise)
+ const activate = jest.fn(async (_services: OpenServices) => undefined)
+ const coordinator = new MobileLifecycleCoordinator({ activate, pause })
+
+ const pauseRequest = coordinator.pause()
+ const activeRequest = coordinator.activate(services('cookie-b'))
+ const requestsSettled = Promise.allSettled([pauseRequest, activeRequest])
+
+ pauseDeferred.reject(error)
+ const results = await requestsSettled
+
+ expect(activate).toHaveBeenCalledTimes(1)
+ expect(activate).toHaveBeenCalledWith(services('cookie-b'))
+ expect(results).toEqual([
+ { reason: error, status: 'rejected' },
+ { reason: error, status: 'rejected' },
+ ])
+ })
+
+ it('recovers after a rejected transition', async () => {
+ const error = new Error('pause failed')
+ const pause = jest.fn<() => Promise>().mockRejectedValueOnce(error).mockResolvedValueOnce(undefined)
+ const activate = jest.fn(async (_services: OpenServices) => undefined)
+ const coordinator = new MobileLifecycleCoordinator({ activate, pause })
+
+ await expect(coordinator.pause()).rejects.toThrow(error)
+ await coordinator.pause()
+
+ expect(pause).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/packages/backend/src/mobile-lifecycle-coordinator.ts b/packages/backend/src/mobile-lifecycle-coordinator.ts
new file mode 100644
index 0000000000..5c6bfd3bf5
--- /dev/null
+++ b/packages/backend/src/mobile-lifecycle-coordinator.ts
@@ -0,0 +1,90 @@
+import type { OpenServices } from './options'
+import type { MobileLifecycleHandlers, MobileLifecycleIntent } from './mobile-lifecycle-coordinator.types'
+
+/**
+ * Serializes mobile close/open events while coalescing superseded intents.
+ * Active intents retain their payload because a new payload may describe a
+ * replacement native Tor session even when the app is already foregrounded.
+ */
+export class MobileLifecycleCoordinator {
+ private desiredIntent: MobileLifecycleIntent | undefined
+ private transitionInFlight: Promise | undefined
+
+ constructor(private readonly handlers: MobileLifecycleHandlers) {}
+
+ public pause(): Promise {
+ return this.request({ type: 'paused' })
+ }
+
+ public activate(services: OpenServices): Promise {
+ return this.request({ type: 'active', services: { ...services } })
+ }
+
+ private request(intent: MobileLifecycleIntent): Promise {
+ this.desiredIntent = intent
+
+ if (!this.transitionInFlight) {
+ const transition = this.drain()
+ this.transitionInFlight = transition
+ void transition.then(
+ () => this.clearTransition(transition),
+ () => this.clearTransition(transition)
+ )
+ }
+
+ return this.transitionInFlight
+ }
+
+ private clearTransition(transition: Promise): void {
+ if (this.transitionInFlight === transition) {
+ this.transitionInFlight = undefined
+ }
+ }
+
+ private async drain(): Promise {
+ let transitionError: unknown
+ let transitionFailed = false
+
+ while (this.desiredIntent) {
+ const intent = this.desiredIntent
+
+ try {
+ if (intent.type === 'paused') {
+ await this.handlers.pause()
+ } else {
+ await this.handlers.activate(intent.services)
+ }
+ } catch (error) {
+ if (!transitionFailed) {
+ transitionError = error
+ transitionFailed = true
+ }
+
+ // A newer request must still be applied even when the transition it
+ // superseded failed. The shared request promise reports the failure
+ // after the coordinator reaches the latest requested state.
+ if (this.desiredIntent !== intent) {
+ continue
+ }
+ throw transitionError
+ }
+
+ const latestIntent = this.desiredIntent
+ if (latestIntent === intent) {
+ if (transitionFailed) {
+ throw transitionError
+ }
+ return
+ }
+
+ // Repeated close requests require no additional work. Active requests
+ // are always replayed so the newest Tor control payload is applied.
+ if (intent.type === 'paused' && latestIntent.type === 'paused') {
+ if (transitionFailed) {
+ throw transitionError
+ }
+ return
+ }
+ }
+ }
+}
diff --git a/packages/backend/src/mobile-lifecycle-coordinator.types.ts b/packages/backend/src/mobile-lifecycle-coordinator.types.ts
new file mode 100644
index 0000000000..2b92535c50
--- /dev/null
+++ b/packages/backend/src/mobile-lifecycle-coordinator.types.ts
@@ -0,0 +1,13 @@
+import type { OpenServices } from './options'
+
+export type MobileLifecycleIntent =
+ | { type: 'paused' }
+ | {
+ type: 'active'
+ services: OpenServices
+ }
+
+export interface MobileLifecycleHandlers {
+ pause: () => Promise
+ activate: (services: OpenServices) => Promise
+}
diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts
index 62556ece79..da2427923a 100644
--- a/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts
+++ b/packages/backend/src/nest/connections-manager/connections-manager.service.spec.ts
@@ -188,7 +188,7 @@ describe('ConnectionsManagerService', () => {
it('pauses and resumes qss alongside the mobile lifecycle', async () => {
const closeSocketSpy = jest.spyOn(connectionsManagerService, 'closeSocket').mockResolvedValue()
- const openSocketSpy = jest.spyOn(connectionsManagerService, 'openSocket').mockResolvedValue()
+ const listenSpy = jest.spyOn(connectionsManagerService['socketService'], 'listen').mockResolvedValue()
const libp2pPauseSpy = jest.spyOn(connectionsManagerService.libp2pService, 'pause').mockResolvedValue(true)
const libp2pResumeSpy = jest.spyOn(connectionsManagerService.libp2pService, 'resume').mockResolvedValue(true)
const qssPauseSpy = jest.spyOn(qssService, 'pause').mockImplementation(() => {})
@@ -200,11 +200,38 @@ describe('ConnectionsManagerService', () => {
expect(libp2pPauseSpy).toHaveBeenCalledTimes(1)
await connectionsManagerService.resume()
- expect(openSocketSpy).toHaveBeenCalledTimes(1)
+ expect(listenSpy).toHaveBeenCalledTimes(1)
expect(libp2pResumeSpy).toHaveBeenCalledTimes(1)
expect(qssResumeSpy).toHaveBeenCalledTimes(1)
})
+ it('uses bounded socket readiness and awaits libp2p before resuming qss', async () => {
+ let resolveLibp2pResume!: (value: boolean) => void
+ const libp2pResumePromise = new Promise(resolve => {
+ resolveLibp2pResume = resolve
+ })
+ const socketInitSpy = jest.spyOn(connectionsManagerService['socketService'], 'init').mockResolvedValue()
+ const listenSpy = jest.spyOn(connectionsManagerService['socketService'], 'listen').mockResolvedValue()
+ const libp2pResumeSpy = jest
+ .spyOn(connectionsManagerService.libp2pService, 'resume')
+ .mockReturnValue(libp2pResumePromise)
+ const qssResumeSpy = jest.spyOn(qssService, 'resume').mockResolvedValue()
+
+ const resumePromise = connectionsManagerService.resume()
+ await waitForExpect(() => expect(libp2pResumeSpy).toHaveBeenCalledTimes(1))
+
+ expect(socketInitSpy).not.toHaveBeenCalled()
+ expect(listenSpy).toHaveBeenCalledTimes(1)
+ expect(qssResumeSpy).not.toHaveBeenCalled()
+
+ resolveLibp2pResume(true)
+ await resumePromise
+
+ expect(qssResumeSpy).toHaveBeenCalledTimes(1)
+ expect(listenSpy.mock.invocationCallOrder[0]).toBeLessThan(libp2pResumeSpy.mock.invocationCallOrder[0])
+ expect(libp2pResumeSpy.mock.invocationCallOrder[0]).toBeLessThan(qssResumeSpy.mock.invocationCallOrder[0])
+ })
+
it('sets storage team metadata once when QSS and libp2p join events race', async () => {
const teamId = 'team-id'
let resolveStorageInit: () => void
diff --git a/packages/backend/src/nest/connections-manager/connections-manager.service.ts b/packages/backend/src/nest/connections-manager/connections-manager.service.ts
index 0f239bced4..e88f9b21b9 100644
--- a/packages/backend/src/nest/connections-manager/connections-manager.service.ts
+++ b/packages/backend/src/nest/connections-manager/connections-manager.service.ts
@@ -288,8 +288,11 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
public async resume() {
this.logger.info('Resuming!')
- await this.openSocket()
- this.libp2pService?.resume()
+ // A lifecycle transition only needs the data server to accept connections.
+ // Waiting for the frontend START event here would prevent a later pause
+ // from running if the app returns to the background before reconnecting.
+ await this.socketService.listen()
+ await this.libp2pService?.resume()
await this.qssService.resume()
}
@@ -403,7 +406,8 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
}
}
- // This method is only used on iOS through rn-bridge for reacting on lifecycle changes
+ // Reopen the socket and wait for the frontend handshake. Workflows such as
+ // leaveCommunity use this stronger readiness guarantee before completing.
public async openSocket() {
await this.socketService.init()
}
diff --git a/packages/backend/src/nest/storage/notifications/notificationTokens.store.ts b/packages/backend/src/nest/storage/notifications/notificationTokens.store.ts
index e4afed8120..39b927e4e2 100644
--- a/packages/backend/src/nest/storage/notifications/notificationTokens.store.ts
+++ b/packages/backend/src/nest/storage/notifications/notificationTokens.store.ts
@@ -219,17 +219,15 @@ export class NotificationTokensStore extends EncryptedKeyValueIndexedValidatedSt
const valueUserId = encPayload.userId
const decUserId = decEntry.userId
const sigAuthor = encPayload.signature.author.name
- if (
- !(
- key &&
- valueUserId &&
- decUserId &&
- sigAuthor &&
- key === valueUserId &&
- key === decUserId &&
- key === sigAuthor
- )
- ) {
+ if (!(
+ key &&
+ valueUserId &&
+ decUserId &&
+ sigAuthor &&
+ key === valueUserId &&
+ key === decUserId &&
+ key === sigAuthor
+ )) {
logger.error(
`Failed to verify notification token entry: ${entry.hash} - ID mismatch. key=${key}, valueUserId=${valueUserId}, decUserId=${decUserId}, sigAuthor=${sigAuthor}`
)
diff --git a/packages/backend/src/nest/storage/userProfile/userProfile.store.ts b/packages/backend/src/nest/storage/userProfile/userProfile.store.ts
index eee637bff1..998a758021 100644
--- a/packages/backend/src/nest/storage/userProfile/userProfile.store.ts
+++ b/packages/backend/src/nest/storage/userProfile/userProfile.store.ts
@@ -253,17 +253,15 @@ export class UserProfileStore extends EncryptedKeyValueIndexedValidatedStoreBase
const valueUserId = encPayload.userId
const decUserId = decEntry.userId
const sigAuthor = encPayload.signature.author.name
- if (
- !(
- key &&
- valueUserId &&
- decUserId &&
- sigAuthor &&
- key === valueUserId &&
- key === decUserId &&
- key === sigAuthor
- )
- ) {
+ if (!(
+ key &&
+ valueUserId &&
+ decUserId &&
+ sigAuthor &&
+ key === valueUserId &&
+ key === decUserId &&
+ key === sigAuthor
+ )) {
logger.error(
`Failed to verify user profile entry: ${entry.hash} - key, value.userId, decEntry.userId, and signature.author.name must all match. Got key=${key}, valueUserId=${valueUserId}, decUserId=${decUserId}, sigAuthor=${sigAuthor}`
)
diff --git a/packages/backend/src/nest/tor/tor-control.service.spec.ts b/packages/backend/src/nest/tor/tor-control.service.spec.ts
new file mode 100644
index 0000000000..5ad14cbba8
--- /dev/null
+++ b/packages/backend/src/nest/tor/tor-control.service.spec.ts
@@ -0,0 +1,61 @@
+import { jest } from '@jest/globals'
+
+import { ConfigOptions } from '../types'
+import { TorControl } from './tor-control.service'
+import { TorControlAuthType, TorControlParams } from './tor.types'
+
+const createTorControl = (authCookie: string) => {
+ const torControlParams: TorControlParams = {
+ port: 9051,
+ host: 'localhost',
+ auth: {
+ type: TorControlAuthType.COOKIE,
+ value: authCookie,
+ },
+ }
+ const torControl = new TorControl(torControlParams, {} as ConfigOptions)
+ const logger = {
+ debug: jest.fn(),
+ error: jest.fn(),
+ }
+ ;(torControl as any).logger = logger
+ jest.spyOn(torControl as any, 'connect').mockResolvedValue(undefined)
+ return { logger, torControl }
+}
+
+describe('TorControl logging', () => {
+ it('logs command and response metadata without ADD_ONION private keys', async () => {
+ const authCookie = 'auth-cookie-sentinel'
+ const requestPrivateKey = 'ED25519-V3:request-private-key-sentinel'
+ const responsePrivateKey = 'ED25519-V3:response-private-key-sentinel'
+ const { logger, torControl } = createTorControl(authCookie)
+ jest.spyOn(torControl, '_sendCommand').mockResolvedValue({
+ code: 250,
+ messages: ['250-ServiceID=example', `250-PrivateKey=${responsePrivateKey}`, '250 OK'],
+ })
+
+ await torControl.sendCommand(`ADD_ONION ${requestPrivateKey} Flags=Detach Port=80,127.0.0.1:3000`)
+
+ const logs = JSON.stringify(logger.debug.mock.calls)
+ expect(logger.debug).toHaveBeenCalledWith('Sending Tor command', { command: 'ADD_ONION' })
+ expect(logger.debug).toHaveBeenCalledWith('Tor command response', {
+ command: 'ADD_ONION',
+ code: 250,
+ messageCount: 3,
+ })
+ expect(logs).not.toContain(requestPrivateKey)
+ expect(logs).not.toContain(responsePrivateKey)
+ expect(logs).not.toContain(authCookie)
+ })
+
+ it('does not log an authentication cookie', async () => {
+ const authCookie = 'authentication-cookie-sentinel'
+ const { logger, torControl } = createTorControl(authCookie)
+ jest.spyOn(torControl, '_sendCommand').mockResolvedValue({ code: 250, messages: ['250 OK'] })
+
+ await torControl.sendCommand(`AUTHENTICATE ${authCookie}`)
+
+ expect(logger.debug).toHaveBeenCalledWith('Sending Tor command', { command: 'AUTHENTICATE' })
+ expect(JSON.stringify(logger.debug.mock.calls)).not.toContain(authCookie)
+ })
+})
diff --git a/packages/backend/src/nest/tor/tor-control.service.ts b/packages/backend/src/nest/tor/tor-control.service.ts
index a31fcf51ef..dc66c246ee 100644
--- a/packages/backend/src/nest/tor/tor-control.service.ts
+++ b/packages/backend/src/nest/tor/tor-control.service.ts
@@ -92,7 +92,10 @@ export class TorControl {
resolve({ code: 250, messages: dataArray })
} else {
clearTimeout(connectionTimeout)
- this.logger.error(`TOR CONNECTION ERROR:`, dataArray)
+ this.logger.error('Tor control command failed', {
+ responseCode: dataArray[0].slice(0, 3),
+ messageCount: dataArray.length,
+ })
reject(`${dataArray[0]}`)
}
clearTimeout(connectionTimeout)
@@ -103,7 +106,8 @@ export class TorControl {
}
public async sendCommand(command: string): Promise<{ code: number; messages: string[] }> {
- this.logger.debug(`Sending tor command: ${command}`)
+ const commandName = command.trim().split(/\s+/, 1)[0]?.toUpperCase() || 'UNKNOWN'
+ this.logger.debug('Sending Tor command', { command: commandName })
// Only send one command at a time.
if (this.isSending) {
this.logger.debug('Tor connection already established, waiting...')
@@ -120,7 +124,11 @@ export class TorControl {
try {
await this.connect()
const res = await this._sendCommand(command)
- this.logger.debug(`Tor command response: ${res.code} ${res.messages}`)
+ this.logger.debug('Tor command response', {
+ command: commandName,
+ code: res.code,
+ messageCount: res.messages.length,
+ })
return res
} finally {
this.disconnect()
diff --git a/packages/backend/src/nest/tor/tor.service.spec.ts b/packages/backend/src/nest/tor/tor.service.spec.ts
new file mode 100644
index 0000000000..1d887a3a33
--- /dev/null
+++ b/packages/backend/src/nest/tor/tor.service.spec.ts
@@ -0,0 +1,260 @@
+import { jest } from '@jest/globals'
+
+import { ConfigOptions, ServerIoProviderTypes } from '../types'
+import { TorControl } from './tor-control.service'
+import { Tor } from './tor.service'
+import { TorControlAuthType, TorParamsProvider, TorPasswordProvider } from './tor.types'
+
+describe('Tor native session rewiring', () => {
+ const controlPort = 19051
+ const httpTunnelPort = 18118
+ const authCookie = 'cookie-a'
+ const privKey = 'ED25519-V3:test-private-key'
+ const onionAddress = 'test-service-id'
+ const bootstrapDone = '250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"'
+
+ const deferred = () => {
+ let resolve!: (value: T) => void
+ let reject!: (reason?: unknown) => void
+ const promise = new Promise((promiseResolve, promiseReject) => {
+ resolve = promiseResolve
+ reject = promiseReject
+ })
+ return { promise, reject, resolve }
+ }
+
+ const addOnionResponse = () => ({
+ code: 250,
+ messages: [`250-ServiceID=${onionAddress}`, '250 OK'],
+ })
+
+ const createTorService = () => {
+ const configOptions: ConfigOptions = {
+ options: {},
+ socketIOPort: 0,
+ httpTunnelPort,
+ torAuthCookie: authCookie,
+ torControlPort: controlPort,
+ env: {},
+ }
+ const torControl = new TorControl(
+ {
+ port: controlPort,
+ host: 'localhost',
+ auth: {
+ type: TorControlAuthType.COOKIE,
+ value: authCookie,
+ },
+ },
+ configOptions
+ )
+ const torParamsProvider: TorParamsProvider = {
+ torPath: '',
+ options: {
+ env: {
+ LD_LIBRARY_PATH: undefined,
+ HOME: '',
+ },
+ detached: false,
+ },
+ }
+ const torPasswordProvider: TorPasswordProvider = {
+ torPassword: '',
+ torHashedPassword: '',
+ }
+ const serverIoProvider = {
+ io: { emit: jest.fn() },
+ } as unknown as ServerIoProviderTypes
+ const torService = new Tor(configOptions, '', torParamsProvider, torPasswordProvider, serverIoProvider, torControl)
+
+ return { torControl, torService }
+ }
+
+ const registerHiddenService = async (torService: Tor, torControl: TorControl) => {
+ jest.spyOn(torControl, 'sendCommand').mockResolvedValue(addOnionResponse())
+ await torService.spawnHiddenService({ targetPort: 4343, privKey })
+ }
+
+ it('preserves bootstrap and initialized hidden services when the native Tor session is unchanged', async () => {
+ const { torControl, torService } = createTorService()
+ await registerHiddenService(torService, torControl)
+ torService.bootstrapped = true
+ jest.mocked(torControl.sendCommand).mockClear()
+
+ torService.rewireNativeTor({ controlPort, httpTunnelPort, authCookie })
+ await torService.spawnHiddenService({ targetPort: 4343, privKey })
+
+ expect(torService.bootstrapped).toBe(true)
+ expect(torControl.sendCommand).not.toHaveBeenCalled()
+ })
+
+ it('treats a control port parsed from the mobile CLI as the unchanged native Tor session', async () => {
+ const { torControl, torService } = createTorService()
+ torControl.torControlParams.port = String(controlPort) as unknown as number
+ await registerHiddenService(torService, torControl)
+ torService.bootstrapped = true
+ const startBootstrapWatcher = jest.spyOn(torService, 'startBootstrapWatcher')
+ jest.mocked(torControl.sendCommand).mockClear()
+
+ torService.rewireNativeTor({ controlPort, httpTunnelPort, authCookie })
+ await torService.spawnHiddenService({ targetPort: 4343, privKey })
+
+ expect(torService.bootstrapped).toBe(true)
+ expect(startBootstrapWatcher).not.toHaveBeenCalled()
+ expect(torControl.sendCommand).not.toHaveBeenCalled()
+ })
+
+ it('resets bootstrap state and replays hidden services for a new native Tor session', async () => {
+ const { torControl, torService } = createTorService()
+ await registerHiddenService(torService, torControl)
+ torService.bootstrapped = true
+ const startBootstrapWatcher = jest.spyOn(torService, 'startBootstrapWatcher').mockImplementation(() => {})
+ jest.mocked(torControl.sendCommand).mockClear()
+
+ torService.rewireNativeTor({ controlPort, httpTunnelPort, authCookie: 'cookie-b' })
+ await torService.spawnHiddenServices()
+
+ expect(torService.bootstrapped).toBe(false)
+ expect(startBootstrapWatcher).toHaveBeenCalledTimes(1)
+ expect(torControl.sendCommand).toHaveBeenCalledWith(`ADD_ONION ${privKey} Flags=Detach Port=80,127.0.0.1:4343`)
+ })
+
+ it('ignores a stale bootstrap status without spawning services or stopping the replacement watcher', async () => {
+ jest.useFakeTimers()
+ const { torControl, torService } = createTorService()
+ await registerHiddenService(torService, torControl)
+ const staleStatus = deferred<{ code: number; messages: string[] }>()
+ jest
+ .mocked(torControl.sendCommand)
+ .mockReset()
+ .mockImplementation(command => {
+ if (command === 'GETINFO status/bootstrap-phase') return staleStatus.promise
+ return Promise.resolve(addOnionResponse())
+ })
+
+ try {
+ torService.startBootstrapWatcher(100)
+ await jest.advanceTimersByTimeAsync(100)
+ expect(torControl.sendCommand).toHaveBeenCalledWith('GETINFO status/bootstrap-phase')
+
+ torService.rewireNativeTor({ controlPort, httpTunnelPort, authCookie: 'cookie-b' })
+ const replacementWatcher = torService.interval
+ staleStatus.resolve({ code: 250, messages: [bootstrapDone, '250 OK'] })
+ await jest.advanceTimersByTimeAsync(0)
+
+ expect(torService.bootstrapped).toBe(false)
+ expect(torService.interval).toBe(replacementWatcher)
+ expect(torControl.sendCommand).toHaveBeenCalledTimes(1)
+ } finally {
+ torService.resetBootstrapState()
+ jest.useRealTimers()
+ }
+ })
+
+ it('does not let a replaced watcher stop its same-generation successor', async () => {
+ jest.useFakeTimers()
+ const { torControl, torService } = createTorService()
+ await registerHiddenService(torService, torControl)
+ const staleStatus = deferred<{ code: number; messages: string[] }>()
+ jest.mocked(torControl.sendCommand).mockReset().mockReturnValue(staleStatus.promise)
+
+ try {
+ torService.startBootstrapWatcher(100)
+ await jest.advanceTimersByTimeAsync(100)
+
+ torService.startBootstrapWatcher(2500)
+ const replacementWatcher = torService.interval
+ staleStatus.resolve({ code: 250, messages: [bootstrapDone, '250 OK'] })
+ await jest.advanceTimersByTimeAsync(0)
+
+ expect(torService.bootstrapped).toBe(false)
+ expect(torService.interval).toBe(replacementWatcher)
+ } finally {
+ torService.resetBootstrapState()
+ jest.useRealTimers()
+ }
+ })
+
+ it('does not let stale mark work populate state or stop a new-generation watcher', async () => {
+ jest.useFakeTimers()
+ const { torControl, torService } = createTorService()
+ await registerHiddenService(torService, torControl)
+ torService.resetBootstrapState()
+ const staleInitialization = deferred<{ code: number; messages: string[] }>()
+ const sendCommand = jest
+ .mocked(torControl.sendCommand)
+ .mockReset()
+ .mockImplementation(command => {
+ if (command === 'GETINFO status/bootstrap-phase') {
+ return Promise.resolve({ code: 250, messages: [bootstrapDone, '250 OK'] })
+ }
+ return staleInitialization.promise
+ })
+
+ try {
+ torService.startBootstrapWatcher(100)
+ await jest.advanceTimersByTimeAsync(100)
+ expect(sendCommand).toHaveBeenCalledTimes(2)
+
+ torService.rewireNativeTor({ controlPort, httpTunnelPort, authCookie: 'cookie-b' })
+ const replacementWatcher = torService.interval
+ staleInitialization.resolve(addOnionResponse())
+ await jest.advanceTimersByTimeAsync(0)
+
+ expect(torService.bootstrapped).toBe(false)
+ expect(torService.interval).toBe(replacementWatcher)
+
+ sendCommand.mockResolvedValue(addOnionResponse())
+ await torService.spawnHiddenService({ targetPort: 4343, privKey })
+ expect(sendCommand).toHaveBeenCalledTimes(3)
+ } finally {
+ torService.resetBootstrapState()
+ jest.useRealTimers()
+ }
+ })
+
+ it('preserves native Tor state when kill has no managed process to terminate', async () => {
+ const { torControl, torService } = createTorService()
+ await registerHiddenService(torService, torControl)
+ torService.bootstrapped = true
+ jest.mocked(torControl.sendCommand).mockClear()
+
+ await torService.kill()
+ torService.rewireNativeTor({ controlPort, httpTunnelPort, authCookie })
+ await torService.spawnHiddenService({ targetPort: 4343, privKey })
+
+ expect(torService.bootstrapped).toBe(true)
+ expect(torControl.sendCommand).not.toHaveBeenCalled()
+ })
+
+ it('deduplicates concurrent hidden-service initialization by private key', async () => {
+ const { torControl, torService } = createTorService()
+ const initialization = deferred<{ code: number; messages: string[] }>()
+ const sendCommand = jest.spyOn(torControl, 'sendCommand').mockReturnValue(initialization.promise)
+
+ const first = torService.spawnHiddenService({ targetPort: 4343, privKey })
+ const second = torService.spawnHiddenService({ targetPort: 4343, privKey })
+
+ expect(sendCommand).toHaveBeenCalledTimes(1)
+ initialization.resolve(addOnionResponse())
+ await expect(Promise.all([first, second])).resolves.toEqual([`${onionAddress}.onion`, `${onionAddress}.onion`])
+ })
+
+ it('does not let stale hidden-service initialization populate a new Tor generation', async () => {
+ const { torControl, torService } = createTorService()
+ const staleInitialization = deferred<{ code: number; messages: string[] }>()
+ const sendCommand = jest.spyOn(torControl, 'sendCommand').mockReturnValue(staleInitialization.promise)
+ const startBootstrapWatcher = jest.spyOn(torService, 'startBootstrapWatcher').mockImplementation(() => {})
+
+ const staleResult = torService.spawnHiddenService({ targetPort: 4343, privKey })
+ torService.rewireNativeTor({ controlPort, httpTunnelPort, authCookie: 'cookie-b' })
+ staleInitialization.resolve(addOnionResponse())
+ await expect(staleResult).rejects.toThrow('Tor generation changed while initializing hidden service')
+
+ sendCommand.mockResolvedValue(addOnionResponse())
+ await torService.spawnHiddenService({ targetPort: 4343, privKey })
+
+ expect(startBootstrapWatcher).toHaveBeenCalledTimes(1)
+ expect(sendCommand).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/packages/backend/src/nest/tor/tor.service.ts b/packages/backend/src/nest/tor/tor.service.ts
index c80cadca49..bd9b127eac 100644
--- a/packages/backend/src/nest/tor/tor.service.ts
+++ b/packages/backend/src/nest/tor/tor.service.ts
@@ -40,6 +40,8 @@ export class Tor extends EventEmitter implements OnModuleInit {
private readonly logger = createLogger(Tor.name)
private hiddenServices: Map = new Map()
private initializedHiddenServices: Map = new Map()
+ private hiddenServiceInitializationPromises: Map> = new Map()
+ private hiddenServiceGeneration = 0
private markBootstrappedPromise: Promise | undefined
private bootstrapRestartPromise: Promise | undefined
private bootstrapStallState: BootstrapStallState | undefined
@@ -90,22 +92,42 @@ export class Tor extends EventEmitter implements OnModuleInit {
httpTunnelPort: number
authCookie: string
}) {
- this.logger.info('Rewiring native Tor control params', { controlPort, httpTunnelPort })
+ const isSameNativeTorSession =
+ Number(this.torControl.torControlParams.port) === controlPort &&
+ this.torControl.torControlParams.auth.type === TorControlAuthType.COOKIE &&
+ this.torControl.torControlParams.auth.value === authCookie
+
+ this.logger.info('Rewiring native Tor control params', {
+ controlPort,
+ httpTunnelPort,
+ isSameNativeTorSession,
+ })
this.configOptions.torControlPort = controlPort
this.configOptions.httpTunnelPort = httpTunnelPort
this.controlPort = controlPort
this.torControl.torControlParams.port = controlPort
this.torControl.torControlParams.auth.value = authCookie
this.torControl.torControlParams.auth.type = TorControlAuthType.COOKIE
+
+ if (isSameNativeTorSession) {
+ this.logger.info('Native Tor session unchanged; preserving bootstrap and hidden-service state')
+ if (!this.bootstrapped && !this.interval) {
+ this.startBootstrapWatcher()
+ }
+ return
+ }
+
this.resetBootstrapState()
this.startBootstrapWatcher()
}
public resetBootstrapState() {
this.bootstrapGeneration += 1
+ this.hiddenServiceGeneration += 1
this.bootstrapped = false
this.bootstrapStallState = undefined
this.initializedHiddenServices = new Map()
+ this.hiddenServiceInitializationPromises.clear()
this.markBootstrappedPromise = undefined
if (this.initTimeout) {
clearTimeout(this.initTimeout)
@@ -114,16 +136,17 @@ export class Tor extends EventEmitter implements OnModuleInit {
this.stopBootstrapWatcher()
}
- private async markBootstrapped() {
+ private async markBootstrapped(bootstrapGeneration: number) {
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
if (this.bootstrapped) return
if (this.markBootstrappedPromise) {
await this.markBootstrappedPromise
return
}
- const bootstrapGeneration = this.bootstrapGeneration
const markBootstrappedPromise = (async () => {
- await this.spawnHiddenServices()
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
+ await this.spawnHiddenServices(bootstrapGeneration)
if (bootstrapGeneration !== this.bootstrapGeneration) {
this.logger.warn('Bootstrap generation changed while marking Tor bootstrapped, skipping stale event')
return
@@ -137,7 +160,6 @@ export class Tor extends EventEmitter implements OnModuleInit {
}
this.emit('bootstrapped')
this.serverIoProvider.io.emit(SocketEvents.TOR_INITIALIZED)
- this.stopBootstrapWatcher()
})()
this.markBootstrappedPromise = markBootstrappedPromise
@@ -161,6 +183,7 @@ export class Tor extends EventEmitter implements OnModuleInit {
this.stopBootstrapWatcher()
}
+ const bootstrapGeneration = this.bootstrapGeneration
let tickInProgress = false
let tickCount = 0
let lastTickStartedAt = 0
@@ -171,7 +194,8 @@ export class Tor extends EventEmitter implements OnModuleInit {
torPids: this.torDataDirectory ? this.getTorProcessIds() : undefined,
})
- this.interval = setInterval(() => {
+ const watcher = setInterval(() => {
+ if (!this.isCurrentBootstrapWatcher(watcher, bootstrapGeneration)) return
if (tickInProgress) {
this.logger.debug('Bootstrap interval tick skipped (previous still running)', {
tickCount,
@@ -186,20 +210,25 @@ export class Tor extends EventEmitter implements OnModuleInit {
void (async () => {
this.logger.info('Checking bootstrap interval', { tickCount })
- const restartedMissingTorProcess = await this.checkManagedTorProcessHealth()
+ const restartedMissingTorProcess = await this.checkManagedTorProcessHealth(bootstrapGeneration)
+ if (!this.isCurrentBootstrapWatcher(watcher, bootstrapGeneration)) return
if (restartedMissingTorProcess) {
return
}
const bootstrapStatus = await this.getBootstrapStatus()
+ if (!this.isCurrentBootstrapWatcher(watcher, bootstrapGeneration)) return
if (bootstrapStatus.done) {
- await this.markBootstrapped()
- this.stopBootstrapWatcher()
+ await this.markBootstrapped(bootstrapGeneration)
+ if (!this.isCurrentBootstrapWatcher(watcher, bootstrapGeneration)) return
+ this.stopBootstrapWatcher(watcher)
return
}
- await this.checkBootstrapStall(bootstrapStatus)
+ await this.checkBootstrapStall(bootstrapStatus, bootstrapGeneration)
+ if (!this.isCurrentBootstrapWatcher(watcher, bootstrapGeneration)) return
})()
.catch(e => {
+ if (!this.isCurrentBootstrapWatcher(watcher, bootstrapGeneration)) return
this.logger.error(
`Bootstrap interval tick failed (tickCount=${tickCount}, startedAt=${lastTickStartedAt})`,
e
@@ -214,9 +243,15 @@ export class Tor extends EventEmitter implements OnModuleInit {
tickInProgress = false
})
}, intervalMs)
+ this.interval = watcher
}
- private stopBootstrapWatcher() {
+ private isCurrentBootstrapWatcher(watcher: any, bootstrapGeneration: number): boolean {
+ return this.interval === watcher && this.bootstrapGeneration === bootstrapGeneration
+ }
+
+ private stopBootstrapWatcher(watcher = this.interval) {
+ if (this.interval !== watcher) return
if (this.interval) {
clearInterval(this.interval)
this.interval = undefined
@@ -236,10 +271,15 @@ export class Tor extends EventEmitter implements OnModuleInit {
public async isBootstrappingFinished(): Promise {
if (this.bootstrapped) return true
+ const bootstrapGeneration = this.bootstrapGeneration
+ const watcher = this.interval
const bootstrapStatus = await this.getBootstrapStatus()
+ if (bootstrapGeneration !== this.bootstrapGeneration) return false
if (bootstrapStatus.done) {
- await this.markBootstrapped()
- return true
+ await this.markBootstrapped(bootstrapGeneration)
+ if (bootstrapGeneration !== this.bootstrapGeneration || !this.bootstrapped) return false
+ this.stopBootstrapWatcher(watcher)
+ return this.bootstrapped
}
return false
}
@@ -266,7 +306,8 @@ export class Tor extends EventEmitter implements OnModuleInit {
}
}
- private async checkBootstrapStall(status: BootstrapStatus): Promise {
+ private async checkBootstrapStall(status: BootstrapStatus, bootstrapGeneration: number): Promise {
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
if (status.done || status.progress == null) {
this.bootstrapStallState = undefined
return
@@ -302,10 +343,17 @@ export class Tor extends EventEmitter implements OnModuleInit {
return
}
- await this.restartAfterBootstrapStall(status, stalledMs, this.bootstrapStallState.timeoutWarningCount)
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
+ await this.restartAfterBootstrapStall(
+ status,
+ stalledMs,
+ this.bootstrapStallState.timeoutWarningCount,
+ bootstrapGeneration
+ )
}
- private async checkManagedTorProcessHealth(): Promise {
+ private async checkManagedTorProcessHealth(bootstrapGeneration: number): Promise {
+ if (bootstrapGeneration !== this.bootstrapGeneration) return false
if (!this.torParamsProvider.torPath || !this.torDataDirectory || this.bootstrapped) {
return false
}
@@ -322,19 +370,26 @@ export class Tor extends EventEmitter implements OnModuleInit {
return false
}
- await this.restartManagedTor('Managed Tor process disappeared during bootstrap; restarting Tor', {
- controlPort: this.controlPort,
- socksPort: this.socksPort,
- torPids,
- })
+ if (bootstrapGeneration !== this.bootstrapGeneration) return false
+ await this.restartManagedTor(
+ 'Managed Tor process disappeared during bootstrap; restarting Tor',
+ {
+ controlPort: this.controlPort,
+ socksPort: this.socksPort,
+ torPids,
+ },
+ bootstrapGeneration
+ )
return true
}
private async restartAfterBootstrapStall(
status: BootstrapStatus,
stalledMs: number,
- timeoutWarningCount: number
+ timeoutWarningCount: number,
+ bootstrapGeneration: number
): Promise {
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
if (!this.torParamsProvider.torPath) {
this.logger.warn('Tor bootstrap appears stalled, but Tor is externally managed; waiting for native Tor', {
progress: status.progress,
@@ -347,19 +402,28 @@ export class Tor extends EventEmitter implements OnModuleInit {
return
}
- await this.restartManagedTor('Tor bootstrap appears stalled; restarting Tor', {
- progress: status.progress,
- tag: status.tag,
- stalledMs,
- timeoutWarningCount,
- controlPort: this.controlPort,
- socksPort: this.socksPort,
- torPids: this.torDataDirectory ? this.getTorProcessIds() : undefined,
- status: status.rawMessage,
- })
+ await this.restartManagedTor(
+ 'Tor bootstrap appears stalled; restarting Tor',
+ {
+ progress: status.progress,
+ tag: status.tag,
+ stalledMs,
+ timeoutWarningCount,
+ controlPort: this.controlPort,
+ socksPort: this.socksPort,
+ torPids: this.torDataDirectory ? this.getTorProcessIds() : undefined,
+ status: status.rawMessage,
+ },
+ bootstrapGeneration
+ )
}
- private async restartManagedTor(reason: string, context: Record): Promise {
+ private async restartManagedTor(
+ reason: string,
+ context: Record,
+ bootstrapGeneration: number
+ ): Promise {
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
if (this.bootstrapRestartPromise) {
await this.bootstrapRestartPromise
return
@@ -368,6 +432,7 @@ export class Tor extends EventEmitter implements OnModuleInit {
this.logger.warn(reason, context)
this.bootstrapRestartPromise = (async () => {
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
this.initializedHiddenServices = new Map()
this.bootstrapStallState = undefined
this.stopBootstrapWatcher()
@@ -401,9 +466,12 @@ export class Tor extends EventEmitter implements OnModuleInit {
this.logger.info(`${this.torPidPath} exists. Old tor pid: ${oldTorPid}`)
}
+ const bootstrapGeneration = this.bootstrapGeneration
this.initTimeout = setTimeout(async () => {
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
this.logger.debug('Checking init timeout')
const bootstrapDone = await this.isBootstrappingFinished()
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
if (!bootstrapDone) {
this.initializedHiddenServices = new Map()
clearInterval(this.interval)
@@ -446,8 +514,10 @@ export class Tor extends EventEmitter implements OnModuleInit {
}
public resetHiddenServices() {
+ this.hiddenServiceGeneration += 1
this.hiddenServices = new Map()
this.initializedHiddenServices = new Map()
+ this.hiddenServiceInitializationPromises.clear()
}
private torProcessNameCommand(oldTorPid: string): string {
@@ -590,10 +660,13 @@ export class Tor extends EventEmitter implements OnModuleInit {
})
}
- public async spawnHiddenServices() {
+ public async spawnHiddenServices(bootstrapGeneration = this.bootstrapGeneration) {
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
this.logger.info(`Spawning hidden service(s) (count: ${this.hiddenServices.size})`)
for (const el of Array.from(this.hiddenServices.values())) {
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
await this.spawnHiddenService(el)
+ if (bootstrapGeneration !== this.bootstrapGeneration) return
}
}
@@ -612,16 +685,36 @@ export class Tor extends EventEmitter implements OnModuleInit {
this.logger.warn(`Hidden service already initialized for ${initializedHiddenService.onionAddress}`)
return initializedHiddenService.onionAddress
}
- const status = await this.torControl.sendCommand(
- `ADD_ONION ${privKey} Flags=Detach Port=${virtPort},127.0.0.1:${targetPort}`
- )
- const onionAddress = status.messages[0].replace('250-ServiceID=', '')
- this.logger.debug(`Spawned hidden service with onion address ${onionAddress}`)
- const hiddenService: HiddenServiceData = { targetPort, privKey, virtPort, onionAddress }
- this.hiddenServices.set(privKey, hiddenService)
- this.initializedHiddenServices.set(privKey, hiddenService)
- return `${onionAddress}.onion`
+ const initializationInFlight = this.hiddenServiceInitializationPromises.get(privKey)
+ if (initializationInFlight) return await initializationInFlight
+
+ const hiddenServiceGeneration = this.hiddenServiceGeneration
+ const initializationPromise = (async () => {
+ const status = await this.torControl.sendCommand(
+ `ADD_ONION ${privKey} Flags=Detach Port=${virtPort},127.0.0.1:${targetPort}`
+ )
+ if (hiddenServiceGeneration !== this.hiddenServiceGeneration) {
+ throw new Error('Tor generation changed while initializing hidden service')
+ }
+
+ const onionAddress = status.messages[0].replace('250-ServiceID=', '')
+ this.logger.debug(`Spawned hidden service with onion address ${onionAddress}`)
+
+ const hiddenService: HiddenServiceData = { targetPort, privKey, virtPort, onionAddress }
+ this.hiddenServices.set(privKey, hiddenService)
+ this.initializedHiddenServices.set(privKey, hiddenService)
+ return `${onionAddress}.onion`
+ })()
+
+ this.hiddenServiceInitializationPromises.set(privKey, initializationPromise)
+ try {
+ return await initializationPromise
+ } finally {
+ if (this.hiddenServiceInitializationPromises.get(privKey) === initializationPromise) {
+ this.hiddenServiceInitializationPromises.delete(privKey)
+ }
+ }
}
public async destroyHiddenService(serviceId: string): Promise {
@@ -677,7 +770,6 @@ export class Tor extends EventEmitter implements OnModuleInit {
}
public async kill(): Promise {
- this.resetBootstrapState()
return await new Promise((resolve, reject) => {
this.logger.info('Killing tor... with pid', this.process?.pid)
if (this.process === null) {
@@ -685,6 +777,7 @@ export class Tor extends EventEmitter implements OnModuleInit {
resolve()
return
}
+ this.resetBootstrapState()
if (this.initTimeout) clearTimeout(this.initTimeout)
if (this.interval) clearInterval(this.interval)
this.process?.on('close', () => {
diff --git a/packages/backend/src/rn-bridge.spec.ts b/packages/backend/src/rn-bridge.spec.ts
new file mode 100644
index 0000000000..8cbcce48c8
--- /dev/null
+++ b/packages/backend/src/rn-bridge.spec.ts
@@ -0,0 +1,75 @@
+import { jest } from '@jest/globals'
+
+const logger = {
+ debug: jest.fn(),
+ error: jest.fn(),
+ info: jest.fn(),
+ warn: jest.fn(),
+}
+
+jest.unstable_mockModule('./nest/common/logger', () => ({
+ createLogger: () => logger,
+}))
+
+const { EventChannel } = await import('./rn-bridge')
+
+const loggedValues = (): string => JSON.stringify(Object.values(logger).flatMap(mock => mock.mock.calls))
+
+describe('rn-bridge logging', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it('logs event metadata without logging a valid secret payload', async () => {
+ const socketIOSecret = 'socket-secret-sentinel'
+ const authCookie = 'auth-cookie-sentinel'
+ const nonce = 'nonce-sentinel'
+ const channel = new EventChannel('_EVENTS_')
+
+ channel.processData(
+ JSON.stringify({
+ event: 'secret',
+ payload: JSON.stringify([{ socketIOSecret, authCookie, nonce }]),
+ })
+ )
+ await new Promise(resolve => setImmediate(resolve))
+
+ expect(logger.info).toHaveBeenCalledWith('EventChannel received event', {
+ event: 'secret',
+ payloadType: 'array',
+ payloadItemCount: 1,
+ })
+ expect(loggedValues()).not.toContain(socketIOSecret)
+ expect(loggedValues()).not.toContain(authCookie)
+ expect(loggedValues()).not.toContain(nonce)
+ })
+
+ it('does not log malformed legacy payload contents', async () => {
+ const socketIOSecret = 'malformed-socket-secret-sentinel'
+ const authCookie = 'malformed-auth-cookie-sentinel'
+ const malformedEntry = 'malformed-nonce-sentinel'
+ const channel = new EventChannel('_EVENTS_')
+
+ channel.processData(
+ JSON.stringify({
+ event: 'secret',
+ payload: `socketIOSecret:${socketIOSecret}|authCookie:${authCookie}|${malformedEntry}`,
+ })
+ )
+ await new Promise(resolve => setImmediate(resolve))
+
+ expect(logger.warn).toHaveBeenCalledWith('Malformed rn-bridge entry', { index: 2 })
+ expect(loggedValues()).not.toContain(socketIOSecret)
+ expect(loggedValues()).not.toContain(authCookie)
+ expect(loggedValues()).not.toContain(malformedEntry)
+ })
+
+ it('does not log a malformed envelope payload', () => {
+ const secretPayload = 'malformed-envelope-secret-sentinel'
+ const channel = new EventChannel('_EVENTS_')
+
+ expect(() => channel.processData(JSON.stringify({ payload: secretPayload }))).toThrow('Malformed message envelope')
+
+ expect(loggedValues()).not.toContain(secretPayload)
+ })
+})
diff --git a/packages/backend/src/rn-bridge.ts b/packages/backend/src/rn-bridge.ts
index 74048e44ea..44b18d13b7 100644
--- a/packages/backend/src/rn-bridge.ts
+++ b/packages/backend/src/rn-bridge.ts
@@ -25,6 +25,34 @@ interface MessageEnvelope {
payload: string | Record
}
+interface MessageEnvelopeLogMetadata {
+ event: string
+ payloadType: 'array' | 'object' | 'string'
+ payloadItemCount?: number
+ payloadFieldCount?: number
+}
+
+const getMessageEnvelopeLogMetadata = (envelope: MessageEnvelope): MessageEnvelopeLogMetadata => {
+ if (Array.isArray(envelope.payload)) {
+ return {
+ event: envelope.event,
+ payloadType: 'array',
+ payloadItemCount: envelope.payload.length,
+ }
+ }
+ if (typeof envelope.payload === 'object') {
+ return {
+ event: envelope.event,
+ payloadType: 'object',
+ payloadFieldCount: Object.keys(envelope.payload).length,
+ }
+ }
+ return {
+ event: envelope.event,
+ payloadType: 'string',
+ }
+}
+
class MessageCodec {
event: string
payload: string
@@ -37,28 +65,32 @@ class MessageCodec {
return JSON.stringify(envelope)
}
static parsePayload(message: string): Record {
- logger.warn('rn-bridge payload', message)
const parsed: Record = {}
const entries = message.split('|')
+ logger.debug('Parsing legacy rn-bridge payload', { entryCount: entries.length })
if (entries.length < 1) {
- logger.warn('Malformed or non-existent rn-bridge payload ', entries)
+ logger.warn('Malformed or non-existent rn-bridge payload')
return parsed
}
- entries.forEach(s => {
+ entries.forEach((s, index) => {
const split = s.split(':')
if (split.length !== 2) {
- logger.warn('Malformed rn-bridge entry: ', split)
+ logger.warn('Malformed rn-bridge entry', { index })
return
}
parsed[split[0]] = split[1]
})
- logger.info('parsed', JSON.stringify(parsed))
+ logger.debug('Parsed legacy rn-bridge payload', { fieldCount: Object.keys(parsed).length })
return parsed
}
static deserialize(message: string): MessageEnvelope {
const envelope = JSON.parse(message) as MessageEnvelope
if (typeof envelope !== 'object' || !Object.prototype.hasOwnProperty.call(envelope, 'event')) {
- logger.error('Malformed message envelope: ', envelope)
+ logger.error('Malformed message envelope', {
+ valueType: typeof envelope,
+ hasEvent:
+ typeof envelope === 'object' && envelope !== null && Object.prototype.hasOwnProperty.call(envelope, 'event'),
+ })
throw new Error('Malformed message envelope')
}
if (typeof envelope.payload === 'string') {
@@ -100,8 +132,8 @@ class EventChannel extends ChannelSuper {
this.post('message', ...msg)
}
processData(data: string): void {
- logger.info('EventChannel received data:', data)
const envelope = MessageCodec.deserialize(data)
+ logger.info('EventChannel received event', getMessageEnvelopeLogMetadata(envelope))
setImmediate(() => {
this.emitLocal(envelope.event, envelope.payload)
})
diff --git a/packages/mobile/ios/NodeJsMobile/RNNodeJsMobile.h b/packages/mobile/ios/NodeJsMobile/RNNodeJsMobile.h
index 9fc2b73d31..c2d199539a 100644
--- a/packages/mobile/ios/NodeJsMobile/RNNodeJsMobile.h
+++ b/packages/mobile/ios/NodeJsMobile/RNNodeJsMobile.h
@@ -3,7 +3,7 @@
@interface RNNodeJsMobile : NSObject
@property (nonatomic, strong) NSString *socketIOSecret;
+ -(void) startNodeProjectInBackground:(NSString *)command;
-(void) sendMessageToNode:(NSString*)event :(NSString*)message;
-(void) sendMessageBackToReact:(NSString*)channelName :(NSString*)message;
- -(void) callStartNodeProject:(NSString *)input;
@end
diff --git a/packages/mobile/ios/NodeJsMobile/RNNodeJsMobile.m b/packages/mobile/ios/NodeJsMobile/RNNodeJsMobile.m
index 186ff21f00..5fb6ee6bcb 100644
--- a/packages/mobile/ios/NodeJsMobile/RNNodeJsMobile.m
+++ b/packages/mobile/ios/NodeJsMobile/RNNodeJsMobile.m
@@ -87,8 +87,10 @@ -(void)callStartNodeProject:(NSString *)input
[[NodeRunner sharedInstance] startEngineWithArguments:nodeArguments:nodePath];
}
-RCT_EXPORT_METHOD(startNodeProject:(NSString *)command options:(NSDictionary *)options)
+-(void)startNodeProjectInBackground:(NSString *)command
{
+ // node_start runs the Node event loop synchronously for the backend's
+ // lifetime. Keep it off React Native's method queue and UIKit's main thread.
if(![NodeRunner sharedInstance].startedNodeAlready)
{
[NodeRunner sharedInstance].startedNodeAlready=true;
@@ -104,6 +106,11 @@ -(void)callStartNodeProject:(NSString *)input
}
}
+RCT_EXPORT_METHOD(startNodeProject:(NSString *)command options:(NSDictionary *)options)
+{
+ [self startNodeProjectInBackground:command];
+}
+
-(void)sendMessageToNode:(NSString *)event :(NSString *)message
{
NSString * data = [NSString stringWithFormat:@"{ \"event\": \"%@\", \"payload\": \"%@\" }", event, message];
diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock
index 2dcfb3bc9d..76bb5173cb 100644
--- a/packages/mobile/ios/Podfile.lock
+++ b/packages/mobile/ios/Podfile.lock
@@ -2136,7 +2136,7 @@ SPEC CHECKSUMS:
Sodium: 23d11554ecd556196d313cf6130d406dfe7ac6da
SSZipArchive: c69881e8ac5521f0e622291387add5f60f30f3c4
Tor: 39dc71bf048312e202608eb499ca5c74e841b503
- Yoga: 2b02f3f767761bb4ffd25b1bb56fd264be57bd6b
+ Yoga: 1a10502f162e8fc40ff0907c82d01cfe555d00c2
PODFILE CHECKSUM: cfc64e6e7d6e469da4bad48f367062c5e329239f
diff --git a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj
index 91211535f6..79035cd99d 100644
--- a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj
+++ b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj
@@ -55,11 +55,10 @@
18FD2A3E296F009E00A2B8C0 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 18FD2A37296F009E00A2B8C0 /* AppDelegate.m */; };
18FD2A3F296F009E00A2B8C0 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 18FD2A38296F009E00A2B8C0 /* Images.xcassets */; };
18FD2A40296F009E00A2B8C0 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 18FD2A39296F009E00A2B8C0 /* main.m */; };
- 2815B731ADE0FE0C770C78E3 /* Pods_QuietNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1725031A98BBEE5E8F3F6561 /* Pods_QuietNotificationServiceExtension.framework */; };
+ 4D83A3053BB0BA758B7C612D /* Pods_QuietNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D609F9D4DF7238B86189CDC /* Pods_QuietNotificationServiceExtension.framework */; };
+ 6204263D91F3440F598B7BB0 /* Pods_Quiet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4656C8360BE2B6E9C1BCF8CF /* Pods_Quiet.framework */; };
663DC8C12F621139005D2086 /* UserMetadataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663DC8C02F621134005D2086 /* UserMetadataHandler.swift */; };
- 673478D924128C1639690CFF /* Pods_Quiet_QuietTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 805522FA9F468CE465C44459 /* Pods_Quiet_QuietTests.framework */; };
955DC7582BD930B30014725B /* WebsocketSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955DC7572BD930B30014725B /* WebsocketSingleton.swift */; };
- CE99A25A0E4E1440E0C42536 /* Pods_Quiet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A49D8557AFF7D73033A5FD2 /* Pods_Quiet.framework */; };
D3239FB5EFA85E780E1AD201 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 84F12DFE2A5B0E05C2C41286 /* PrivacyInfo.xcprivacy */; };
EB4DC7EC2F5FF6AB00EFD23F /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = EB4DC7EB2F5FF6AB00EFD23F /* GoogleService-Info.plist */; };
EB4DC8002F608A3300EFD23F /* QuietNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EB4DC7F92F608A3300EFD23F /* QuietNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -67,6 +66,7 @@
EB4DC8172F60A81000EFD23F /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = EB4DC7EB2F5FF6AB00EFD23F /* GoogleService-Info.plist */; };
EB5F92FC2F5FBB2100B5C60D /* FirebaseMessagingModule.m in Sources */ = {isa = PBXBuildFile; fileRef = EB5F92FA2F5FBB2100B5C60D /* FirebaseMessagingModule.m */; };
EB5F92FD2F5FBB2100B5C60D /* FirebaseMessagingModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5F92FB2F5FBB2100B5C60D /* FirebaseMessagingModule.swift */; };
+ F4B2150A7001269B8241CC86 /* Pods_Quiet_QuietTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB1D543483316490C8FEBE9 /* Pods_Quiet_QuietTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -132,9 +132,7 @@
03B674042E6103DC00A86655 /* Rubik-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Rubik-Regular.ttf"; path = "../src/assets/fonts/Rubik-Regular.ttf"; sourceTree = SOURCE_ROOT; };
03B674052E6103DC00A86655 /* Rubik-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Rubik-SemiBold.ttf"; path = "../src/assets/fonts/Rubik-SemiBold.ttf"; sourceTree = SOURCE_ROOT; };
03B674062E6103DC00A86655 /* Rubik-SemiBoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Rubik-SemiBoldItalic.ttf"; path = "../src/assets/fonts/Rubik-SemiBoldItalic.ttf"; sourceTree = SOURCE_ROOT; };
- 09783DD98C14F076599A13B4 /* Pods-Quiet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet.release.xcconfig"; path = "Target Support Files/Pods-Quiet/Pods-Quiet.release.xcconfig"; sourceTree = ""; };
13B07F961A680F5B00A75B9A /* Quiet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Quiet.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 1725031A98BBEE5E8F3F6561 /* Pods_QuietNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuietNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
180E120A2AEFB7F900804659 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; };
1827A9E129783D6E00245FD3 /* classic-level.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = "classic-level.framework"; sourceTree = ""; };
183C484F296C7B6700BA2D8B /* v8-platform.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "v8-platform.h"; sourceTree = ""; };
@@ -644,17 +642,19 @@
18FD2A39296F009E00A2B8C0 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Quiet/main.m; sourceTree = ""; };
18FD2A3A296F009E00A2B8C0 /* Quiet.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = Quiet.entitlements; path = Quiet/Quiet.entitlements; sourceTree = ""; };
18FD2A3B296F009E00A2B8C0 /* QuietDebug.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = QuietDebug.entitlements; path = Quiet/QuietDebug.entitlements; sourceTree = ""; };
- 1A49D8557AFF7D73033A5FD2 /* Pods_Quiet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Quiet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 58FBDCDCEEA5EACED0FF5D68 /* Pods-Quiet-QuietTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet-QuietTests.release.xcconfig"; path = "Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests.release.xcconfig"; sourceTree = ""; };
+ 1CB1D543483316490C8FEBE9 /* Pods_Quiet_QuietTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Quiet_QuietTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3D609F9D4DF7238B86189CDC /* Pods_QuietNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuietNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 4656C8360BE2B6E9C1BCF8CF /* Pods_Quiet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Quiet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 4F5C736CD3F644621489397D /* Pods-QuietNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuietNotificationServiceExtension.release.xcconfig"; path = "Target Support Files/Pods-QuietNotificationServiceExtension/Pods-QuietNotificationServiceExtension.release.xcconfig"; sourceTree = ""; };
663DC8C02F621134005D2086 /* UserMetadataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMetadataHandler.swift; sourceTree = ""; };
- 72EA7AA4B77C0780A86EC300 /* Pods-Quiet-QuietTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet-QuietTests.debug.xcconfig"; path = "Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests.debug.xcconfig"; sourceTree = ""; };
- 805522FA9F468CE465C44459 /* Pods_Quiet_QuietTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Quiet_QuietTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
84F12DFE2A5B0E05C2C41286 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Quiet/PrivacyInfo.xcprivacy; sourceTree = ""; };
+ 866ADD8CCEDAE901228A8325 /* Pods-Quiet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet.debug.xcconfig"; path = "Target Support Files/Pods-Quiet/Pods-Quiet.debug.xcconfig"; sourceTree = ""; };
+ 88791CE736EBC48C100DEECB /* Pods-Quiet-QuietTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet-QuietTests.debug.xcconfig"; path = "Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests.debug.xcconfig"; sourceTree = ""; };
955DC7572BD930B30014725B /* WebsocketSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsocketSingleton.swift; sourceTree = ""; };
- C5124A51F9229F2CDC5100C0 /* Pods-QuietNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuietNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-QuietNotificationServiceExtension/Pods-QuietNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; };
- CDD5E3F75BDB63D6BC806F2B /* Pods-QuietNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuietNotificationServiceExtension.release.xcconfig"; path = "Target Support Files/Pods-QuietNotificationServiceExtension/Pods-QuietNotificationServiceExtension.release.xcconfig"; sourceTree = ""; };
+ 983C2DA0FB6D06D1452AFCDF /* Pods-Quiet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet.release.xcconfig"; path = "Target Support Files/Pods-Quiet/Pods-Quiet.release.xcconfig"; sourceTree = ""; };
+ D138F5E8F9C27CF118B62554 /* Pods-Quiet-QuietTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet-QuietTests.release.xcconfig"; path = "Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests.release.xcconfig"; sourceTree = ""; };
DC03A48F321B4BE2A5339F05 /* Quiet.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Quiet.xcconfig; sourceTree = SOURCE_ROOT; };
- DE73D517F18C6E0B941FDFF2 /* Pods-Quiet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Quiet.debug.xcconfig"; path = "Target Support Files/Pods-Quiet/Pods-Quiet.debug.xcconfig"; sourceTree = ""; };
+ DCB679A6329CD994279348D8 /* Pods-QuietNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuietNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-QuietNotificationServiceExtension/Pods-QuietNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; };
E079692B46015F1D27CDBBC1 /* Quiet.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Quiet.debug.xcconfig; sourceTree = SOURCE_ROOT; };
E079692B46015F1D27CDBBC2 /* Quiet.release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Quiet.release.xcconfig; sourceTree = SOURCE_ROOT; };
E17A7BE86B5879902AF7BA3A /* QuietNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = QuietNotificationServiceExtension.debug.xcconfig; sourceTree = SOURCE_ROOT; };
@@ -701,7 +701,7 @@
buildActionMask = 2147483647;
files = (
1827A9E229783D6E00245FD3 /* classic-level.framework in Frameworks */,
- 673478D924128C1639690CFF /* Pods_Quiet_QuietTests.framework in Frameworks */,
+ F4B2150A7001269B8241CC86 /* Pods_Quiet_QuietTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -711,7 +711,7 @@
files = (
00A416342EC2EAA900ACC877 /* NodeMobile.xcframework in Frameworks */,
1827A9E329783D7600245FD3 /* classic-level.framework in Frameworks */,
- CE99A25A0E4E1440E0C42536 /* Pods_Quiet.framework in Frameworks */,
+ 6204263D91F3440F598B7BB0 /* Pods_Quiet.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -719,7 +719,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 2815B731ADE0FE0C770C78E3 /* Pods_QuietNotificationServiceExtension.framework in Frameworks */,
+ 4D83A3053BB0BA758B7C612D /* Pods_QuietNotificationServiceExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -4782,12 +4782,12 @@
1CEEDB4F07B9978C125775C5 /* Pods */ = {
isa = PBXGroup;
children = (
- DE73D517F18C6E0B941FDFF2 /* Pods-Quiet.debug.xcconfig */,
- 09783DD98C14F076599A13B4 /* Pods-Quiet.release.xcconfig */,
- 72EA7AA4B77C0780A86EC300 /* Pods-Quiet-QuietTests.debug.xcconfig */,
- 58FBDCDCEEA5EACED0FF5D68 /* Pods-Quiet-QuietTests.release.xcconfig */,
- C5124A51F9229F2CDC5100C0 /* Pods-QuietNotificationServiceExtension.debug.xcconfig */,
- CDD5E3F75BDB63D6BC806F2B /* Pods-QuietNotificationServiceExtension.release.xcconfig */,
+ 866ADD8CCEDAE901228A8325 /* Pods-Quiet.debug.xcconfig */,
+ 983C2DA0FB6D06D1452AFCDF /* Pods-Quiet.release.xcconfig */,
+ 88791CE736EBC48C100DEECB /* Pods-Quiet-QuietTests.debug.xcconfig */,
+ D138F5E8F9C27CF118B62554 /* Pods-Quiet-QuietTests.release.xcconfig */,
+ DCB679A6329CD994279348D8 /* Pods-QuietNotificationServiceExtension.debug.xcconfig */,
+ 4F5C736CD3F644621489397D /* Pods-QuietNotificationServiceExtension.release.xcconfig */,
);
path = Pods;
sourceTree = "";
@@ -4797,9 +4797,9 @@
children = (
00A416332EC2EAA900ACC877 /* NodeMobile.xcframework */,
1827A9E129783D6E00245FD3 /* classic-level.framework */,
- 1A49D8557AFF7D73033A5FD2 /* Pods_Quiet.framework */,
- 805522FA9F468CE465C44459 /* Pods_Quiet_QuietTests.framework */,
- 1725031A98BBEE5E8F3F6561 /* Pods_QuietNotificationServiceExtension.framework */,
+ 4656C8360BE2B6E9C1BCF8CF /* Pods_Quiet.framework */,
+ 1CB1D543483316490C8FEBE9 /* Pods_Quiet_QuietTests.framework */,
+ 3D609F9D4DF7238B86189CDC /* Pods_QuietNotificationServiceExtension.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -4873,12 +4873,12 @@
isa = PBXNativeTarget;
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "QuietTests" */;
buildPhases = (
- B84DABF150D7B1B5C2DF9239 /* [CP] Check Pods Manifest.lock */,
+ 765F2ED3DBDDEF2FF5C056CB /* [CP] Check Pods Manifest.lock */,
00E356EA1AD99517003FC87E /* Sources */,
00E356EB1AD99517003FC87E /* Frameworks */,
00E356EC1AD99517003FC87E /* Resources */,
- C0311D831E5FCA078E3114A7 /* [CP] Embed Pods Frameworks */,
- D779ACFAE5667A522EFCF468 /* [CP] Copy Pods Resources */,
+ F2CC9725101511987408ECEC /* [CP] Embed Pods Frameworks */,
+ 2FF92392E3AF19C0048F19EA /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -4894,7 +4894,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Quiet" */;
buildPhases = (
- 669A6891CB9F7ADDDD9A98B4 /* [CP] Check Pods Manifest.lock */,
+ 6B0C767FCED8729054722020 /* [CP] Check Pods Manifest.lock */,
FD10A7F022414F080027D42C /* Start Packager */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
@@ -4907,8 +4907,8 @@
1827A9E0297837FE00245FD3 /* [CUSTOM NODEJS MOBILE] Remove prebuilds */,
1868C095292F8FE2001D6D5E /* Embed Frameworks */,
EBD3273E2F5FA01F00E2CD0C /* Embed Foundation Extensions */,
- 468C008BFE8FDD32EC649A5B /* [CP] Embed Pods Frameworks */,
- B04C9103A39CF5F564636327 /* [CP] Copy Pods Resources */,
+ 45E8E1CED3ACB0A8578792B4 /* [CP] Embed Pods Frameworks */,
+ 256DD21612F4D56D318DA3D7 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -4927,7 +4927,7 @@
isa = PBXNativeTarget;
buildConfigurationList = EB4DC8022F608A3300EFD23F /* Build configuration list for PBXNativeTarget "QuietNotificationServiceExtension" */;
buildPhases = (
- 3D09985B32EEEAFD930419CB /* [CP] Check Pods Manifest.lock */,
+ 8A70AA2C80FA5736FD665DB6 /* [CP] Check Pods Manifest.lock */,
EB4DC7F52F608A3300EFD23F /* Sources */,
EB4DC7F62F608A3300EFD23F /* Frameworks */,
EB4DC7F72F608A3300EFD23F /* Resources */,
@@ -5155,29 +5155,41 @@
shellPath = /bin/sh;
shellScript = "find \"$CODESIGNING_FOLDER_PATH/nodejs-project/node_modules/\" -name \"python3\" | xargs rm\n";
};
- 3D09985B32EEEAFD930419CB /* [CP] Check Pods Manifest.lock */ = {
+ 256DD21612F4D56D318DA3D7 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources-${CONFIGURATION}-input-files.xcfilelist",
);
- inputPaths = (
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
- "${PODS_ROOT}/Manifest.lock",
- );
- name = "[CP] Check Pods Manifest.lock";
+ name = "[CP] Copy Pods Resources";
outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources-${CONFIGURATION}-output-files.xcfilelist",
);
- outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-QuietNotificationServiceExtension-checkManifestLockResult.txt",
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 2FF92392E3AF19C0048F19EA /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources.sh\"\n";
showEnvVarsInLog = 0;
};
- 468C008BFE8FDD32EC649A5B /* [CP] Embed Pods Frameworks */ = {
+ 45E8E1CED3ACB0A8578792B4 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -5194,7 +5206,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
- 669A6891CB9F7ADDDD9A98B4 /* [CP] Check Pods Manifest.lock */ = {
+ 6B0C767FCED8729054722020 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -5216,24 +5228,29 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- B04C9103A39CF5F564636327 /* [CP] Copy Pods Resources */ = {
+ 765F2ED3DBDDEF2FF5C056CB /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources-${CONFIGURATION}-input-files.xcfilelist",
);
- name = "[CP] Copy Pods Resources";
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Quiet-QuietTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet/Pods-Quiet-resources.sh\"\n";
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- B84DABF150D7B1B5C2DF9239 /* [CP] Check Pods Manifest.lock */ = {
+ 8A70AA2C80FA5736FD665DB6 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -5248,14 +5265,14 @@
outputFileListPaths = (
);
outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-Quiet-QuietTests-checkManifestLockResult.txt",
+ "$(DERIVED_FILE_DIR)/Pods-QuietNotificationServiceExtension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
- C0311D831E5FCA078E3114A7 /* [CP] Embed Pods Frameworks */ = {
+ F2CC9725101511987408ECEC /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -5272,23 +5289,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
- D779ACFAE5667A522EFCF468 /* [CP] Copy Pods Resources */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources-${CONFIGURATION}-input-files.xcfilelist",
- );
- name = "[CP] Copy Pods Resources";
- outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources-${CONFIGURATION}-output-files.xcfilelist",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Quiet-QuietTests/Pods-Quiet-QuietTests-resources.sh\"\n";
- showEnvVarsInLog = 0;
- };
FD10A7F022414F080027D42C /* Start Packager */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -5368,7 +5368,7 @@
/* Begin XCBuildConfiguration section */
00E356F61AD99517003FC87E /* Debug */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 72EA7AA4B77C0780A86EC300 /* Pods-Quiet-QuietTests.debug.xcconfig */;
+ baseConfigurationReference = 88791CE736EBC48C100DEECB /* Pods-Quiet-QuietTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_MODULES = YES;
@@ -5403,7 +5403,7 @@
};
00E356F71AD99517003FC87E /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 58FBDCDCEEA5EACED0FF5D68 /* Pods-Quiet-QuietTests.release.xcconfig */;
+ baseConfigurationReference = D138F5E8F9C27CF118B62554 /* Pods-Quiet-QuietTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_MODULES = YES;
diff --git a/packages/mobile/ios/Quiet/AppDelegate.m b/packages/mobile/ios/Quiet/AppDelegate.m
index 3da44e1fc9..5a464f45fa 100644
--- a/packages/mobile/ios/Quiet/AppDelegate.m
+++ b/packages/mobile/ios/Quiet/AppDelegate.m
@@ -128,7 +128,8 @@ - (void)torHandlerReady:(TorHandler *)handler
- (void)launchBackend:(uint16_t)controlPort httpTunnelPort:(uint16_t)httpTunnelPort authCookie:(NSString *)authCookie {
self.nodeJsMobile = [RNNodeJsMobile new];
[self.nodeJsMobile setSocketIOSecret:self.socketIOSecret];
- [self.nodeJsMobile callStartNodeProject:[NSString stringWithFormat:@"bundle.cjs --dataPort %hu --dataPath %@ --controlPort %hu --httpTunnelPort %hu --authCookie %@ --platform %@", self.dataPort, self.dataPath, controlPort, httpTunnelPort, authCookie, platform]];
+ NSString *command = [NSString stringWithFormat:@"bundle.cjs --dataPort %hu --dataPath %@ --controlPort %hu --httpTunnelPort %hu --authCookie %@ --platform %@", self.dataPort, self.dataPath, controlPort, httpTunnelPort, authCookie, platform];
+ [self.nodeJsMobile startNodeProjectInBackground:command];
}
- (void)rewireServices:(uint16_t)controlPort httpTunnelPort:(uint16_t)httpTunnelPort authCookie:(NSString *)authCookie {
diff --git a/packages/mobile/ios/TorHandler.swift b/packages/mobile/ios/TorHandler.swift
index 5f03412e32..910375fe54 100644
--- a/packages/mobile/ios/TorHandler.swift
+++ b/packages/mobile/ios/TorHandler.swift
@@ -60,9 +60,26 @@ final class TorHandler: NSObject {
private static let stableUptime: TimeInterval = 60
private static let sharedLifecycleQueue = DispatchQueue(label: "com.quietmobile.tor-lifecycle")
+ // Tor 0.4.5.9's compiled-in authority identities are stale. Keep this list
+ // synchronized with the Tor Project's audited auth_dirs.inc. Bundling these
+ // trust anchors keeps bootstrap independent of a runtime network fetch.
+ // Source: tor commit 4b996aa6469ac78fe746bcc8d6d8f100643e3c01.
+ private static let directoryAuthorities = """
+ moria1 orport=9201 v3ident=F533C81CEF0BC0267857C99B2F471ADF249FA232 128.31.0.39:9231 1A25C6358DB91342AA51720A5038B72742732498
+ tor26 orport=443 v3ident=2F3DF9CA0E5D36F2685A2DA67184EB8DCB8CBA8C ipv6=[2a02:16a8:662:2203::1]:443 217.196.147.77:80 FAA4BCA4A6AC0FB4CA2F8AD5A11D9E122BA894F6
+ dizum orport=443 v3ident=E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58 45.66.35.11:80 7EA6EAD6FD83083C538F44038BBFA077587DD755
+ gabelmoo orport=443 v3ident=ED03BB616EB2F60BEC80151114BB25CEF515B226 ipv6=[2001:638:a000:4140::ffff:189]:443 131.188.40.189:80 F2044413DAC2E02E3D6BCF4735A19BCA1DE97281
+ dannenberg orport=443 v3ident=0232AF901C31A04EE9848595AF9BB7620D4C5B2E ipv6=[2001:678:558:1000::244]:443 193.23.244.244:80 7BE683E65D48141321C5ED92F075C55364AC7123
+ maatuska orport=80 v3ident=49015F787433103580E3B66A1707A00E60F2D15B ipv6=[2001:67c:289c::9]:80 171.25.193.9:443 BD6A829255CB08E66FBE7D3748363586E46B3810
+ longclaw orport=443 v3ident=23D15D965BC35114467363C165C4F724B64B4F66 199.58.81.140:80 74A910646BCEEFBCD2E874FC1DC997430F968145
+ bastet orport=443 v3ident=27102BC123E7AF1D4741AE047E160C91ADC76B21 ipv6=[2620:13:4000:6000::1000:118]:443 204.13.164.118:80 24E2F139121D4394C54B5BCC368B3B411857C413
+ faravahar orport=443 v3ident=70849B868D606BAECFB6128C5E3D782029AA394F 216.218.219.41:80 E3E42D35F801C9D5AB23584E0025D56FE2B33396
+ """.split(separator: "\n").map(String.init)
+
// Tor.framework registers callbacks globally and appends each registration.
- // A static initializer guarantees that we install one pair per process.
- private static let installLoggingCallbacks: Void = {
+ // Tor 0.4.5.9 also asserts if its callback is registered before init_logging,
+ // so install one pair per Tor generation only after control authentication.
+ private static func installLoggingCallbacks() {
TORInstallTorLoggingCallback { severity, message in
guard severity != .debug, severity != .info else { return }
let text = String(cString: message).trimmingCharacters(in: .whitespacesAndNewlines)
@@ -82,7 +99,7 @@ final class TorHandler: NSObject {
TorHandler.logger.notice("libevent: \(text, privacy: .public)")
}
}
- }()
+ }
@objc weak var delegate: TorHandlerDelegate?
@@ -112,15 +129,11 @@ final class TorHandler: NSObject {
private var restartAttempts = 0
private var authenticationInFlight = false
private var readyNotificationPending = false
+ private var loggingCallbacksGeneration: UInt?
private var cookieData: Data?
private var authCookie: String?
- override init() {
- super.init()
- _ = Self.installLoggingCallbacks
- }
-
deinit {
monitorTimer?.cancel()
}
@@ -205,7 +218,7 @@ final class TorHandler: NSObject {
"--ControlPort", "127.0.0.1:\(controlPort)",
"--HTTPTunnelPort", "127.0.0.1:\(httpTunnelPort)",
"--Log", "notice stdout",
- ]
+ ] + Self.directoryAuthorities.flatMap { ["--AlternateDirAuthority", $0] }
if let dataDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
.first?.appendingPathComponent("tor", isDirectory: true) {
@@ -427,6 +440,12 @@ final class TorHandler: NSObject {
self.authenticationInFlight = false
if success {
+ guard self.desiredMode != .stopped,
+ self.torThread?.isFinished == false else { return }
+ if self.loggingCallbacksGeneration != generation {
+ Self.installLoggingCallbacks()
+ self.loggingCallbacksGeneration = generation
+ }
self.authCookie = cookieData.hexEncodedString()
self.applyDesiredMode()
} else {
From c1c4530108d1593771a9a64b79e63d050cff7fc5 Mon Sep 17 00:00:00 2001
From: adrastaea
Date: Tue, 14 Jul 2026 18:03:49 -0400
Subject: [PATCH 13/18] feat: update sync-bundle-consumers script to use node
for file operations
---
packages/backend/package.json | 2 +-
packages/backend/scripts/sync-bundle-consumers.cjs | 13 +++++++++++++
2 files changed, 14 insertions(+), 1 deletion(-)
create mode 100644 packages/backend/scripts/sync-bundle-consumers.cjs
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 9831581e0f..55cf4ec45c 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -12,7 +12,7 @@
"build": "tsc -p tsconfig.build.json",
"webpack": "webpack --env mode=development && npm run sync-bundle-consumers",
"webpack:prod": "webpack --env mode=production && npm run sync-bundle-consumers",
- "sync-bundle-consumers": "cp ./lib/bundle.cjs ../backend-bundle/bundle.cjs && mkdir -p ../mobile/nodejs-assets/nodejs-project && cp ./lib/bundle.cjs ../mobile/nodejs-assets/nodejs-project/bundle.cjs",
+ "sync-bundle-consumers": "node ./scripts/sync-bundle-consumers.cjs",
"postinstall": "npm run applyPatches",
"applyPatches": "npm run applyPatches:it && npm run applyPatches:libp2p",
"applyPatches:libp2p": "patch -f -p0 < ./patch/libp2p/mplex.patch || true",
diff --git a/packages/backend/scripts/sync-bundle-consumers.cjs b/packages/backend/scripts/sync-bundle-consumers.cjs
new file mode 100644
index 0000000000..fdd2b7a1d3
--- /dev/null
+++ b/packages/backend/scripts/sync-bundle-consumers.cjs
@@ -0,0 +1,13 @@
+const { copyFileSync, mkdirSync } = require('node:fs')
+const { dirname, resolve } = require('node:path')
+
+const bundle = resolve(__dirname, '../lib/bundle.cjs')
+const consumers = [
+ resolve(__dirname, '../../backend-bundle/bundle.cjs'),
+ resolve(__dirname, '../../mobile/nodejs-assets/nodejs-project/bundle.cjs'),
+]
+
+for (const consumer of consumers) {
+ mkdirSync(dirname(consumer), { recursive: true })
+ copyFileSync(bundle, consumer)
+}
From 3740f97422ea4a780b00aeb3f4ac663102c854b9 Mon Sep 17 00:00:00 2001
From: taea
Date: Thu, 16 Jul 2026 11:18:41 -0400
Subject: [PATCH 14/18] feat: refactor mobile lifecycle intent types to use
enum for consistency
---
.../backend/src/mobile-lifecycle-coordinator.ts | 14 +++++++++-----
.../src/mobile-lifecycle-coordinator.types.ts | 9 +++++++--
2 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/packages/backend/src/mobile-lifecycle-coordinator.ts b/packages/backend/src/mobile-lifecycle-coordinator.ts
index 5c6bfd3bf5..2c3402741f 100644
--- a/packages/backend/src/mobile-lifecycle-coordinator.ts
+++ b/packages/backend/src/mobile-lifecycle-coordinator.ts
@@ -1,5 +1,9 @@
import type { OpenServices } from './options'
-import type { MobileLifecycleHandlers, MobileLifecycleIntent } from './mobile-lifecycle-coordinator.types'
+import {
+ MobileLifecycleIntentType,
+ type MobileLifecycleHandlers,
+ type MobileLifecycleIntent,
+} from './mobile-lifecycle-coordinator.types'
/**
* Serializes mobile close/open events while coalescing superseded intents.
@@ -13,11 +17,11 @@ export class MobileLifecycleCoordinator {
constructor(private readonly handlers: MobileLifecycleHandlers) {}
public pause(): Promise {
- return this.request({ type: 'paused' })
+ return this.request({ type: MobileLifecycleIntentType.PAUSED })
}
public activate(services: OpenServices): Promise {
- return this.request({ type: 'active', services: { ...services } })
+ return this.request({ type: MobileLifecycleIntentType.ACTIVE, services: { ...services } })
}
private request(intent: MobileLifecycleIntent): Promise {
@@ -49,7 +53,7 @@ export class MobileLifecycleCoordinator {
const intent = this.desiredIntent
try {
- if (intent.type === 'paused') {
+ if (intent.type === MobileLifecycleIntentType.PAUSED) {
await this.handlers.pause()
} else {
await this.handlers.activate(intent.services)
@@ -79,7 +83,7 @@ export class MobileLifecycleCoordinator {
// Repeated close requests require no additional work. Active requests
// are always replayed so the newest Tor control payload is applied.
- if (intent.type === 'paused' && latestIntent.type === 'paused') {
+ if (intent.type === MobileLifecycleIntentType.PAUSED && latestIntent.type === MobileLifecycleIntentType.PAUSED) {
if (transitionFailed) {
throw transitionError
}
diff --git a/packages/backend/src/mobile-lifecycle-coordinator.types.ts b/packages/backend/src/mobile-lifecycle-coordinator.types.ts
index 2b92535c50..25369b5b6a 100644
--- a/packages/backend/src/mobile-lifecycle-coordinator.types.ts
+++ b/packages/backend/src/mobile-lifecycle-coordinator.types.ts
@@ -1,9 +1,14 @@
import type { OpenServices } from './options'
+export enum MobileLifecycleIntentType {
+ PAUSED = 'paused',
+ ACTIVE = 'active',
+}
+
export type MobileLifecycleIntent =
- | { type: 'paused' }
+ | { type: MobileLifecycleIntentType.PAUSED }
| {
- type: 'active'
+ type: MobileLifecycleIntentType.ACTIVE
services: OpenServices
}
From 0e449da945fc07dc8b7ffa3a6d4a7f990871f327 Mon Sep 17 00:00:00 2001
From: taea
Date: Thu, 16 Jul 2026 21:09:23 -0400
Subject: [PATCH 15/18] remove redundant checks
---
packages/backend/src/nest/tor/tor.service.spec.ts | 13 +++++++++++++
packages/backend/src/nest/tor/tor.service.ts | 9 ++-------
2 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/packages/backend/src/nest/tor/tor.service.spec.ts b/packages/backend/src/nest/tor/tor.service.spec.ts
index 1d887a3a33..d0b6b6d795 100644
--- a/packages/backend/src/nest/tor/tor.service.spec.ts
+++ b/packages/backend/src/nest/tor/tor.service.spec.ts
@@ -213,6 +213,19 @@ describe('Tor native session rewiring', () => {
}
})
+ it('rechecks the bootstrap generation after spawning hidden services', async () => {
+ const { torControl, torService } = createTorService()
+ jest.spyOn(torControl, 'sendCommand').mockResolvedValue({ code: 250, messages: [bootstrapDone, '250 OK'] })
+ jest.spyOn(torService, 'startBootstrapWatcher').mockImplementation(() => {})
+ jest.spyOn(torService, 'spawnHiddenServices').mockImplementation(async () => {
+ torService.rewireNativeTor({ controlPort, httpTunnelPort, authCookie: 'cookie-b' })
+ })
+
+ await expect(torService.isBootstrappingFinished()).resolves.toBe(false)
+
+ expect(torService.bootstrapped).toBe(false)
+ })
+
it('preserves native Tor state when kill has no managed process to terminate', async () => {
const { torControl, torService } = createTorService()
await registerHiddenService(torService, torControl)
diff --git a/packages/backend/src/nest/tor/tor.service.ts b/packages/backend/src/nest/tor/tor.service.ts
index bd9b127eac..3da6ae7b31 100644
--- a/packages/backend/src/nest/tor/tor.service.ts
+++ b/packages/backend/src/nest/tor/tor.service.ts
@@ -145,8 +145,9 @@ export class Tor extends EventEmitter implements OnModuleInit {
}
const markBootstrappedPromise = (async () => {
- if (bootstrapGeneration !== this.bootstrapGeneration) return
await this.spawnHiddenServices(bootstrapGeneration)
+ // The generation can change while hidden-service initialization is in flight,
+ // so revalidate before publishing bootstrapped state for the session.
if (bootstrapGeneration !== this.bootstrapGeneration) {
this.logger.warn('Bootstrap generation changed while marking Tor bootstrapped, skipping stale event')
return
@@ -220,12 +221,10 @@ export class Tor extends EventEmitter implements OnModuleInit {
if (!this.isCurrentBootstrapWatcher(watcher, bootstrapGeneration)) return
if (bootstrapStatus.done) {
await this.markBootstrapped(bootstrapGeneration)
- if (!this.isCurrentBootstrapWatcher(watcher, bootstrapGeneration)) return
this.stopBootstrapWatcher(watcher)
return
}
await this.checkBootstrapStall(bootstrapStatus, bootstrapGeneration)
- if (!this.isCurrentBootstrapWatcher(watcher, bootstrapGeneration)) return
})()
.catch(e => {
if (!this.isCurrentBootstrapWatcher(watcher, bootstrapGeneration)) return
@@ -343,7 +342,6 @@ export class Tor extends EventEmitter implements OnModuleInit {
return
}
- if (bootstrapGeneration !== this.bootstrapGeneration) return
await this.restartAfterBootstrapStall(
status,
stalledMs,
@@ -370,7 +368,6 @@ export class Tor extends EventEmitter implements OnModuleInit {
return false
}
- if (bootstrapGeneration !== this.bootstrapGeneration) return false
await this.restartManagedTor(
'Managed Tor process disappeared during bootstrap; restarting Tor',
{
@@ -432,7 +429,6 @@ export class Tor extends EventEmitter implements OnModuleInit {
this.logger.warn(reason, context)
this.bootstrapRestartPromise = (async () => {
- if (bootstrapGeneration !== this.bootstrapGeneration) return
this.initializedHiddenServices = new Map()
this.bootstrapStallState = undefined
this.stopBootstrapWatcher()
@@ -664,7 +660,6 @@ export class Tor extends EventEmitter implements OnModuleInit {
if (bootstrapGeneration !== this.bootstrapGeneration) return
this.logger.info(`Spawning hidden service(s) (count: ${this.hiddenServices.size})`)
for (const el of Array.from(this.hiddenServices.values())) {
- if (bootstrapGeneration !== this.bootstrapGeneration) return
await this.spawnHiddenService(el)
if (bootstrapGeneration !== this.bootstrapGeneration) return
}
From 7c0fc2a7ab3fa6b9e17c2080b35b14508b69d409 Mon Sep 17 00:00:00 2001
From: taea
Date: Thu, 16 Jul 2026 22:22:45 -0400
Subject: [PATCH 16/18] refactor: streamline bootstrap state management in init
process
---
packages/backend/src/nest/tor/tor.service.ts | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/packages/backend/src/nest/tor/tor.service.ts b/packages/backend/src/nest/tor/tor.service.ts
index 3da6ae7b31..4ab3a02e98 100644
--- a/packages/backend/src/nest/tor/tor.service.ts
+++ b/packages/backend/src/nest/tor/tor.service.ts
@@ -429,9 +429,6 @@ export class Tor extends EventEmitter implements OnModuleInit {
this.logger.warn(reason, context)
this.bootstrapRestartPromise = (async () => {
- this.initializedHiddenServices = new Map()
- this.bootstrapStallState = undefined
- this.stopBootstrapWatcher()
await this.init()
})()
@@ -443,9 +440,9 @@ export class Tor extends EventEmitter implements OnModuleInit {
}
public async init(timeout = 120_000): Promise {
+ this.resetBootstrapState()
if (!this.socksPort) this.socksPort = await getPort()
this.logger.info('Initializing tor...')
- this.resetBootstrapState()
return await new Promise((resolve, reject) => {
if (!fs.existsSync(this.quietDir)) {
@@ -469,8 +466,6 @@ export class Tor extends EventEmitter implements OnModuleInit {
const bootstrapDone = await this.isBootstrappingFinished()
if (bootstrapGeneration !== this.bootstrapGeneration) return
if (!bootstrapDone) {
- this.initializedHiddenServices = new Map()
- clearInterval(this.interval)
await this.init()
}
}, timeout)
From 07f0348e69ac6f115a66c42d86a9ef7f1d6c908b Mon Sep 17 00:00:00 2001
From: taea
Date: Fri, 17 Jul 2026 00:07:26 -0400
Subject: [PATCH 17/18] refactor: remove redundant prepare-backend-assets
script from package.json
---
packages/mobile/package.json | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
index 14e100c27c..372f81861e 100644
--- a/packages/mobile/package.json
+++ b/packages/mobile/package.json
@@ -14,12 +14,11 @@
"lint-ci": "npm run lint:no-fix",
"lint-staged": "lint-staged --no-stash",
"gen": "plop",
- "prepare-backend-assets": "mkdir -p ./nodejs-assets/nodejs-project && cp ../backend/lib/bundle.cjs ./nodejs-assets/nodejs-project/bundle.cjs",
"patch-react-native-file-logger": "patch -f -p0 --forward --binary < ./patch/rnfl/build.gradle.patch || true && patch -f -p0 --forward --binary < ./patch/rnfl/AndroidManifest.xml.patch || true",
"patch-state-manager": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || patch -f -d ../state-manager -p0 < ./factory-girl.patch || true",
"patch-react-native": "patch -f -p0 --forward --binary < ./react-native.patch || true",
"patch-webview-crypto": "patch -f -p0 --forward --binary < ./react-native-webview-crypto.patch || true",
- "prepare": "npm run prepare-backend-assets && npm run patch-react-native-file-logger && npm run patch-state-manager && npm run patch-react-native && npm run patch-webview-crypto && npm run build",
+ "prepare": "npm run patch-react-native-file-logger && npm run patch-state-manager && npm run patch-react-native && npm run patch-webview-crypto && npm run build",
"version": "react-native-version --skip-tag"
},
"dependencies": {
From ebe3d84014ece3b9b005d859bbd59865b25c7dea Mon Sep 17 00:00:00 2001
From: taea
Date: Fri, 17 Jul 2026 12:58:14 -0400
Subject: [PATCH 18/18] update changelog
---
CHANGELOG.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e99b831638..425ae293f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## [unreleased]
+
+### Fixes
+
+* iOS tor process lifecycle improvements solving crashes and improving performance [#3349](https://github.com/TryQuiet/quiet/issues/3349)
+
## [8.0.0]
### Features