diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b918378..e73698f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,9 +173,12 @@ jobs: key: ${{ runner.os }}-konan-${{ hashFiles('gradle/libs.versions.toml') }} restore-keys: ${{ runner.os }}-konan- - - name: Build shared KMP framework + - name: Build shared KMP framework (simulator) run: ./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 + - name: Build shared KMP framework (device) + run: ./gradlew :shared:linkDebugFrameworkIosArm64 + - name: Build iOS app run: | xcodebuild -project iosApp/iosApp.xcodeproj \ diff --git a/README.md b/README.md index 40a852a..e0f4fba 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,30 @@ NON_BROWSER_VIOLATION_SCORE=0 Without this setting, the server's violation system may accumulate ban points against the mobile client if the User-Agent check fails, eventually locking the account out. +### Self-Signed Certificates (iOS) + +The iOS app can connect to a LibreChat server using a self-signed certificate, but iOS enforces strict requirements on all TLS certificates regardless of whether they are self-signed or CA-issued. The certificate must meet **all** of the following or iOS will reject the connection: + +| Requirement | Details | +|---|---| +| **Validity period** | ≤ 398 days (for certificates issued after September 1, 2020) | +| **Subject Alternative Name** | DNS name must be present in the SAN extension — CommonName alone is not accepted | +| **Key algorithm** | RSA ≥ 2048 bits | +| **Signature algorithm** | SHA-256 or better (SHA-1 is not accepted) | +| **Extended Key Usage** | Must include `id-kp-serverAuth` (TLS Web Server Authentication) | + +The 398-day validity limit is the most common cause of failures — a certificate that works in every browser but was generated with a multi-year validity will be rejected by iOS. + +**Setup steps:** + +1. Generate a compliant certificate signed by your own CA (tools like [`mkcert`](https://github.com/FiloSottile/mkcert) handle all requirements automatically) +2. On your iPhone: **Settings → General → VPN & Device Management** — install your CA certificate +3. Go to **Settings → General → About → Certificate Trust Settings** — enable full trust for your CA + +Once the CA is trusted on the device, you only need to re-install it if you create a new CA. The server certificate itself (which you replace when it expires) does not need to be installed separately. + +> **Android** has no equivalent restrictions — self-signed certificates with any validity period work as long as the CA is trusted on the device. + ### Notes - **Registration** — The app respects your server's registration settings. If registration is disabled server-side, only the login form is shown. @@ -88,16 +112,26 @@ For a release build: ### iOS -Build and run on the iOS Simulator: +**Simulator:** ```bash +./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp \ -sdk iphonesimulator \ -destination 'platform=iOS Simulator,name=iPhone 16' \ -derivedDataPath iosApp/build build ``` -See [CONTRIBUTING.md](CONTRIBUTING.md) for full simulator install and launch commands. +**Physical device:** + +```bash +./gradlew :shared:linkDebugFrameworkIosArm64 +open iosApp/iosApp.xcodeproj +``` + +Then in Xcode: select your device, set your Team under Signing & Capabilities, and press ⌘R. + +See [iosApp/README.md](iosApp/README.md) for full build and launch instructions. ### Tech Stack diff --git a/iosApp/README.md b/iosApp/README.md index 5238857..aee4495 100644 --- a/iosApp/README.md +++ b/iosApp/README.md @@ -35,25 +35,51 @@ The `:shared` Gradle module exports `core:common`, `core:model`, `core:network`, ### Build and Run +#### Simulator + ```bash -# Build the app (Xcode build phases handle the shared framework automatically) +# 1. Build the shared KMP framework +./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 + +# 2. Build the Xcode project xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp \ -sdk iphonesimulator \ -destination 'platform=iOS Simulator,name=iPhone 16' \ -derivedDataPath iosApp/build build -# Boot simulator, install, and launch +# 3. Boot simulator, install, and launch xcrun simctl boot "iPhone 16" xcrun simctl install booted iosApp/build/Build/Products/Debug-iphonesimulator/iosApp.app xcrun simctl launch booted com.garfiec.librechat.ios ``` -> **Note:** The first build takes several minutes while the Kotlin/Native toolchain downloads. - -To rebuild only the shared KMP framework (e.g., after changing shared Kotlin code): +#### Physical Device ```bash -./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 +# 1. Build the shared KMP framework for device +./gradlew :shared:linkDebugFrameworkIosArm64 +``` + +Then open the project in Xcode: + +```bash +open iosApp/iosApp.xcodeproj +``` + +In Xcode: +1. Select your iPhone from the device picker (top toolbar) +2. Go to **Signing & Capabilities** → set your **Team** (required for device signing) +3. Press **⌘R** to build and install + +> **Note:** The first build takes several minutes while the Kotlin/Native toolchain downloads. You need an Apple Developer account (free tier works) for device signing. + +### Known Xcode Behaviour + +**Red dot on `Shared.framework` in the navigator** — You may see a red indicator on `Shared.framework` in the Xcode file navigator (left sidebar). This is cosmetic and does not affect builds. It happens because the Xcode project file keeps a static reference path to the framework for display purposes, and that path may not exist if you haven't built that particular target yet. The linker always resolves the framework via `FRAMEWORK_SEARCH_PATHS`, which is set correctly per target — the red dot can be safely ignored. Building the Gradle framework for your active target clears it: + +```bash +./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 # clears it for simulator +./gradlew :shared:linkDebugFrameworkIosArm64 # clears it for device ``` ## Info.plist Permissions diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index b534583..e194340 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -43,7 +43,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Aggregate and copy Compose Multiplatform resources into the app bundle.\n# Runs the Gradle aggregation task to ensure resources from all feature\n# modules are collected, then copies them into the .app bundle where\n# the Compose resource reader expects them at runtime.\n\nif [ \"$CONFIGURATION\" = \"Debug\" ]; then\n ARCH_DIR=\"iosSimulatorArm64\"\n GRADLE_TASK=\":shared:iosSimulatorArm64AggregateResources\"\nelse\n ARCH_DIR=\"iosArm64\"\n GRADLE_TASK=\":shared:iosArm64AggregateResources\"\nfi\n\nPROJECT_ROOT=\"$SRCROOT/..\"\nSRC=\"$PROJECT_ROOT/shared/build/kotlin-multiplatform-resources/aggregated-resources/$ARCH_DIR/composeResources\"\nDST=\"$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/compose-resources/composeResources\"\n\n# Run Gradle aggregation to ensure resources are up to date\necho \"Running $GRADLE_TASK...\"\ncd \"$PROJECT_ROOT\" && ./gradlew \"$GRADLE_TASK\" 2>&1\nGRADLE_EXIT=$?\nif [ $GRADLE_EXIT -ne 0 ]; then\n echo \"error: Gradle task $GRADLE_TASK failed with exit code $GRADLE_EXIT\"\n exit $GRADLE_EXIT\nfi\n\n# Clean stale resources before copying fresh ones\nif [ -d \"$DST\" ]; then\n rm -rf \"$DST\"\nfi\n\nif [ -d \"$SRC\" ]; then\n mkdir -p \"$DST\"\n cp -R \"$SRC/\" \"$DST/\"\n echo \"Copied Compose resources from $SRC to $DST\"\nelse\n echo \"error: Compose resources not found at $SRC after Gradle aggregation\"\n exit 1\nfi\n"; + shellScript = "# Aggregate and copy Compose Multiplatform resources into the app bundle.\n# Runs the Gradle aggregation task to ensure resources from all feature\n# modules are collected, then copies them into the .app bundle where\n# the Compose resource reader expects them at runtime.\n\nif [ \"$PLATFORM_NAME\" = \"iphonesimulator\" ]; then\n ARCH_DIR=\"iosSimulatorArm64\"\n GRADLE_TASK=\":shared:iosSimulatorArm64AggregateResources\"\nelif [ \"$PLATFORM_NAME\" = \"iphoneos\" ]; then\n ARCH_DIR=\"iosArm64\"\n GRADLE_TASK=\":shared:iosArm64AggregateResources\"\nelse\n echo \"error: Unsupported PLATFORM_NAME: $PLATFORM_NAME\"\n exit 1\nfi\n\nPROJECT_ROOT=\"$SRCROOT/..\"\nSRC=\"$PROJECT_ROOT/shared/build/kotlin-multiplatform-resources/aggregated-resources/$ARCH_DIR/composeResources\"\nDST=\"$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/compose-resources/composeResources\"\n\n# Run Gradle aggregation to ensure resources are up to date\necho \"Running $GRADLE_TASK...\"\ncd \"$PROJECT_ROOT\" && ./gradlew \"$GRADLE_TASK\" 2>&1\nGRADLE_EXIT=$?\nif [ $GRADLE_EXIT -ne 0 ]; then\n echo \"error: Gradle task $GRADLE_TASK failed with exit code $GRADLE_EXIT\"\n exit $GRADLE_EXIT\nfi\n\n# Clean stale resources before copying fresh ones\nif [ -d \"$DST\" ]; then\n rm -rf \"$DST\"\nfi\n\nif [ -d \"$SRC\" ]; then\n mkdir -p \"$DST\"\n cp -R \"$SRC/\" \"$DST/\"\n echo \"Copied Compose resources from $SRC to $DST\"\nelse\n echo \"error: Compose resources not found at $SRC after Gradle aggregation\"\n exit 1\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -237,14 +237,22 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", "$(PROJECT_DIR)/../shared/build/bin/iosSimulatorArm64/debugFramework", ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(PROJECT_DIR)/../shared/build/bin/iosArm64/debugFramework", + ); GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = iosApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -268,12 +276,20 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(PROJECT_DIR)/../shared/build/bin/iosSimulatorArm64/releaseFramework", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", "$(PROJECT_DIR)/../shared/build/bin/iosArm64/releaseFramework", ); GENERATE_INFOPLIST_FILE = NO;