From 8ddac065c6ad9ebcfc93e5707123c2caf536f632 Mon Sep 17 00:00:00 2001 From: Jeppe B <2jepp9350@gmail.com> Date: Mon, 20 Jul 2026 14:37:17 +0200 Subject: [PATCH] Guard iOS debug signing with live CORS (#181) Verify the stable API and exact Capacitor iOS CORS contract before signing device-debug IPAs, with regression coverage and troubleshooting guidance. --- .github/workflows/ios-device-debug.yml | 49 ++++++++++++++++++++++++++ docs/ios-device-debug.md | 18 ++++++++++ tests/unit/mobile-ios-device.spec.js | 4 +++ 3 files changed, 71 insertions(+) diff --git a/.github/workflows/ios-device-debug.yml b/.github/workflows/ios-device-debug.yml index 107e2431..e96334cb 100644 --- a/.github/workflows/ios-device-debug.yml +++ b/.github/workflows/ios-device-debug.yml @@ -171,6 +171,55 @@ jobs: - name: Install dependencies run: npm ci --legacy-peer-deps + - name: Verify stable API and Capacitor iOS CORS + shell: bash + run: | + set -euo pipefail + api_base="${IOS_DEBUG_API_URL%/}" + curl --fail --silent --show-error --location \ + --connect-timeout 10 --max-time 20 \ + --header 'Accept: application/json' \ + --output /dev/null \ + "$api_base/ping" + + cors_headers="$RUNNER_TEMP/ios-debug-cors-headers.txt" + cors_body="$RUNNER_TEMP/ios-debug-cors-body.txt" + cors_status="$(curl --silent --show-error \ + --connect-timeout 10 --max-time 20 \ + --request OPTIONS \ + --header 'Origin: capacitor://localhost' \ + --header 'Access-Control-Request-Method: POST' \ + --header 'Access-Control-Request-Headers: authorization,content-type' \ + --dump-header "$cors_headers" \ + --output "$cors_body" \ + --write-out '%{http_code}' \ + "$api_base/ping")" + if [[ ! "$cors_status" =~ ^2[0-9][0-9]$ ]]; then + echo "Stable API rejected the Capacitor iOS CORS preflight with HTTP $cors_status" >&2 + sed -n '1,20p' "$cors_body" >&2 + exit 1 + fi + if ! grep -Eiq '^access-control-allow-origin:[[:space:]]*capacitor://localhost[[:space:]]*$' "$cors_headers"; then + echo "Stable API did not allow the exact capacitor://localhost origin" >&2 + exit 1 + fi + if ! grep -Eiq '^access-control-allow-credentials:[[:space:]]*true[[:space:]]*$' "$cors_headers"; then + echo "Stable API did not allow credentialed Capacitor requests" >&2 + exit 1 + fi + if ! grep -Eiq '^access-control-allow-methods:.*[[:space:],]POST([[:space:],]|$)' "$cors_headers"; then + echo "Stable API did not allow POST from the Capacitor origin" >&2 + exit 1 + fi + if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Authorization([[:space:],]|$)' "$cors_headers"; then + echo "Stable API did not allow the Authorization header from the Capacitor origin" >&2 + exit 1 + fi + if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Content-Type([[:space:],]|$)' "$cors_headers"; then + echo "Stable API did not allow the Content-Type header from the Capacitor origin" >&2 + exit 1 + fi + - name: Build stable production web payload env: RELEASE_COMMIT_SHA: ${{ env.RESOLVED_SOURCE_SHA }} diff --git a/docs/ios-device-debug.md b/docs/ios-device-debug.md index f12cf7c0..130915db 100644 --- a/docs/ios-device-debug.md +++ b/docs/ios-device-debug.md @@ -348,6 +348,24 @@ an already installed development build from launching. or been revoked. - Collect syslog and crash reports before reinstalling so evidence is preserved. +### App reports `No response was received` + +- Keep the API base set to `https://api-v2.truckwash.io/master/api`. The bare + `https://api-v2.truckwash.io` host is the public gateway, not the application + API base. +- Confirm `https://api-v2.truckwash.io/master/api/ping` responds before + investigating the device or app. +- Capacitor serves bundled iOS content from `capacitor://localhost`. The stable + API must return `Access-Control-Allow-Origin: capacitor://localhost` for that + exact origin, including authenticated preflight requests. +- The signing workflow checks API reachability and this CORS contract before + compiling or signing. If it fails, deploy the backend CORS policy fix before + dispatching another IPA; do not replace the API URL or use an unsupported + HTTP/HTTPS `iosScheme` workaround. +- An already-built IPA starts using a corrected server-side CORS policy without + modification. Build and install a higher version when recording a verified + device-test result for the fix. + ### iOS beta or new major iOS version breaks device tools - Record the device model, exact iOS version, helper error, source SHA, and IPA diff --git a/tests/unit/mobile-ios-device.spec.js b/tests/unit/mobile-ios-device.spec.js index dd675670..753f0b8a 100644 --- a/tests/unit/mobile-ios-device.spec.js +++ b/tests/unit/mobile-ios-device.spec.js @@ -485,6 +485,10 @@ describe("repository device-debug configuration", () => { expect(workflow).toContain("name: mobile-device-debug"); expect(workflow).toContain("RELEASE_COMMIT_SHA: ${{ env.RESOLVED_SOURCE_SHA }}"); expect(workflow).toContain("IOS_DEBUG_CERTIFICATE_BASE64: ${{ secrets.IOS_DEBUG_CERTIFICATE_BASE64 }}"); + expect(workflow).toContain("Origin: capacitor://localhost"); + expect(workflow).toContain("Stable API did not allow the exact capacitor://localhost origin"); + expect(workflow).toContain("Stable API did not allow credentialed Capacitor requests"); + expect(workflow).toContain("Stable API did not allow the Authorization header from the Capacitor origin"); expect(workflow).not.toContain("IOS_DEBUG_KEYCHAIN_PASSWORD"); expect(workflow).not.toMatch(/upload-app|notarytool|transporter/iu); });