diff --git a/.github/workflows/publish-app-store.yml b/.github/workflows/publish-app-store.yml index 97e7bdf..82496cc 100644 --- a/.github/workflows/publish-app-store.yml +++ b/.github/workflows/publish-app-store.yml @@ -7,7 +7,9 @@ # ── HUMAN GATE (one-time, after Apple Developer Program enrollment) ────────────── # Complete ALL of the following before releasing. Do NOT invent any of these IDs. # -# 1. Fill and commit the eas.json placeholders (see eas.json.README.md for click paths): +# 1. Link the app to an Expo project and add its UUID as the GitHub Actions +# repository variable EAS_PROJECT_ID (see eas.json.README.md), then fill and +# commit the eas.json placeholders: # submit.production.ios.ascAppId REPLACE_WITH_APP_STORE_CONNECT_APP_ID → numeric App Store Connect App ID # submit.production.ios.appleTeamId REPLACE_WITH_APPLE_TEAM_ID → 10-char Apple Team ID # @@ -26,7 +28,8 @@ # SENTRY_AUTH_TOKEN, SENTRY_ORG, and SENTRY_PROJECT in Expo before releasing. # # Build number is managed remotely by EAS (eas.json: cli.appVersionSource=remote, -# build.production.ios.autoIncrement=buildNumber). No app.json mutation happens here. +# build.production.ios.autoIncrement=buildNumber). The workflow only injects the +# EAS project linkage into its temporary runner copy of app.json. name: Publish to App Store (TestFlight) @@ -54,6 +57,7 @@ jobs: env: EAS_CLI_VERSION: "21.0.0" EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + EAS_PROJECT_ID: ${{ vars.EAS_PROJECT_ID }} steps: - uses: actions/checkout@v6 @@ -80,6 +84,13 @@ jobs: need "APPLE_APP_STORE_CONNECT_API_KEY_ID" "${ASC_KEY_ID:-}" need "APPLE_APP_STORE_CONNECT_ISSUER_ID" "${ASC_ISSUER_ID:-}" need "APPLE_APP_STORE_CONNECT_API_KEY" "${ASC_KEY_B64:-}" + if [ -z "${EAS_PROJECT_ID:-}" ]; then + echo "::error::Missing required repository variable: EAS_PROJECT_ID" + fail=1 + elif ! printf '%s' "$EAS_PROJECT_ID" | grep -Eq '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'; then + echo "::error::EAS_PROJECT_ID must be an Expo project UUID" + fail=1 + fi asc_app_id=$(node -p "require('./eas.json').submit.production.ios.ascAppId || ''") team_id=$(node -p "require('./eas.json').submit.production.ios.appleTeamId || ''") case "$asc_app_id" in @@ -113,6 +124,18 @@ jobs: - name: Install dependencies (deterministic) run: npm ci --legacy-peer-deps + - name: Configure EAS project linkage + run: | + set -euo pipefail + node -e " + const fs = require('fs'); + const j = require('./app.json'); + j.expo.extra = { ...j.expo.extra, eas: { ...j.expo.extra?.eas, projectId: process.env.EAS_PROJECT_ID } }; + fs.writeFileSync('app.json', JSON.stringify(j, null, 2) + '\n'); + " + test "$(node -p "require('./app.json').expo.extra.eas.projectId")" = "$EAS_PROJECT_ID" + echo "Linked build to Expo project $EAS_PROJECT_ID." + - name: Configure App Store Connect API key env: ASC_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }} diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index f2e412e..d6395e7 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -21,6 +21,7 @@ import { } from "../../src/lib/notifications" import type { Category } from "../../src/lib/notifications" import { hasTelemetryConsent, setTelemetryConsent } from "../../src/lib/telemetry" +import { PRIVACY_POLICY_URL } from "../../src/lib/links" function SettingRow({ icon, @@ -215,7 +216,7 @@ export default function SettingsScreen() { label="Privacy Policy" description="What data we collect and how" isDark={isDark} - onPress={() => Linking.openURL("https://agentlabs.cc/opencode/privacy")} + onPress={() => Linking.openURL(PRIVACY_POLICY_URL)} right={} /> diff --git a/distribution/ios-enrollment-runbook.md b/distribution/ios-enrollment-runbook.md index 08e3bb0..c8f82fc 100644 --- a/distribution/ios-enrollment-runbook.md +++ b/distribution/ios-enrollment-runbook.md @@ -141,13 +141,15 @@ While waiting for Apple's verification call and approval: - Note: Key ID and Issuer ID - Base64-encode the .p8 and store in GitHub secret `APPLE_APP_STORE_CONNECT_API_KEY` -4. Configure the EAS `production` environment for optional crash reporting: +4. Run `eas init` once, then add the generated `extra.eas.projectId` UUID as the GitHub Actions repository variable `EAS_PROJECT_ID`. + +5. Configure the EAS `production` environment for optional crash reporting: - `EXPO_PUBLIC_SENTRY_DSN` - `SENTRY_AUTH_TOKEN` (secret visibility) - `SENTRY_ORG` - `SENTRY_PROJECT` -5. Create an internal TestFlight group and add yourself as tester +6. Create an internal TestFlight group and add yourself as tester --- diff --git a/docs/applestore.md b/docs/applestore.md index fd6a333..61e784d 100644 --- a/docs/applestore.md +++ b/docs/applestore.md @@ -90,7 +90,7 @@ Because the answer is "No", no ERN (Encryption Registration Number) is required | 9 | Export compliance | ✅ Done | `ITSAppUsesNonExemptEncryption: false` added to `app.json`. Answers + rationale in this doc (see Export Compliance section above) and `distribution/app-store-listing.md`. | | 10 | ATS justification in App Review notes | ✅ Done | Full justification text in `distribution/app-store-listing.md` under "App Review Notes — ATS Justification" | | 11 | Reviewer test instructions | ✅ Done | Updated with correct command (`opencode serve --hostname 0.0.0.0`) in `distribution/app-store-listing.md` | -| 12 | GitHub secrets: `EXPO_TOKEN`, `APPLE_APP_STORE_CONNECT_API_KEY_ID`, `APPLE_APP_STORE_CONNECT_ISSUER_ID`, `APPLE_APP_STORE_CONNECT_API_KEY` (base64 .p8) | User | 🟡 post-enrollment — see `.github/workflows/publish-app-store.yml` header | +| 12 | GitHub variable `EAS_PROJECT_ID`; secrets: `EXPO_TOKEN`, `APPLE_APP_STORE_CONNECT_API_KEY_ID`, `APPLE_APP_STORE_CONNECT_ISSUER_ID`, `APPLE_APP_STORE_CONNECT_API_KEY` (base64 .p8) | User | 🟡 post-enrollment — run `eas init`, then see `.github/workflows/publish-app-store.yml` | | 13 | Update `eas.json` placeholders: `ascAppId` + `appleTeamId` | User | 🟡 post-enrollment — see `eas.json.README.md` for click paths | | 14 | CI workflow validated | CI | 🟡 Linux checks pass; PR must prove the macOS Simulator build | | 15 | TestFlight release notes | ✅ Done | `distribution/whatsnew-ios/release-notes-en-US.txt` — polished, 1658 chars (limit 4000) | @@ -102,7 +102,7 @@ Because the answer is "No", no ERN (Encryption Registration Number) is required 1. (manual) Sign in to App Store Connect, create app with bundle id `cc.agentlabs.opencode`. 2. (manual) Generate App Store Connect API key (App Manager role) → download `.p8` → base64 encode → add as GitHub secret. 3. (manual) Update `eas.json` placeholders (Team ID, ASC App ID). -4. (manual) `eas login` + `eas build:configure` for first-time setup (managed signing). +4. (manual) `eas login` + `eas init` + `eas build:configure`; add the generated project UUID as repository variable `EAS_PROJECT_ID`. 5. (automated) Publish a GitHub Release for the version tag → CI calls EAS Build → EAS Submit → IPA lands in TestFlight. 6. (manual, first time) Add internal testers in App Store Connect → distribute via TestFlight. 7. (manual) After internal testing OK → submit for App Store review (production). diff --git a/docs/security.md b/docs/security.md index 2b349f6..0a6cdaa 100644 --- a/docs/security.md +++ b/docs/security.md @@ -152,7 +152,7 @@ When the opencode AI agent requests a file-access permission, the notification b **Files:** `app.json:29`, `app.json:38-39` **Description:** Both platforms allow HTTP connections, which is required for local/LAN servers. This is intentional and correct for the use case. However, neither the Play Store listing, App Store listing, nor a privacy policy document currently explains that HTTP connections may be made to user-provided servers. Google Play's Data Safety section and Apple's App Privacy report will flag arbitrary network access if not documented. **Remediation:** -1. Update the privacy policy at `agentlabs.cc/opencode/privacy` to explain that the app connects to user-configured server addresses that may use HTTP. +1. Update the canonical privacy policy at `https://dzianisv.github.io/opencode-mobile/privacy/` to explain that the app connects to user-configured server addresses that may use HTTP. 2. In Play Store Data Safety: disclose "Other app performance data" collected (crash reports via Sentry — opt-in). **Status:** Open diff --git a/eas.json.README.md b/eas.json.README.md index 5dde065..9aa2943 100644 --- a/eas.json.README.md +++ b/eas.json.README.md @@ -73,10 +73,12 @@ Alternatively, in App Store Connect: ## After filling in the placeholders -1. Commit the updated `eas.json` to the repo. -2. Add the `EXPO_TOKEN` and App Store Connect API GitHub Actions secrets (see `.github/workflows/publish-app-store.yml` for the exact list). -3. Publish a GitHub Release for the version tag (or manually dispatch the App Store workflow). -4. The release event triggers CI to build the IPA via EAS and submit that exact build to TestFlight. +1. Run `eas init` once to create/link the Expo project. +2. Copy the generated `extra.eas.projectId` UUID and add it as the GitHub Actions repository variable `EAS_PROJECT_ID`. The release workflow injects it into `app.json` only on the runner. +3. Commit the updated `eas.json` to the repo. +4. Add the `EXPO_TOKEN` and App Store Connect API GitHub Actions secrets (see `.github/workflows/publish-app-store.yml` for the exact list). +5. Publish a GitHub Release for the version tag (or manually dispatch the App Store workflow). +6. The release event triggers CI to build the IPA via EAS and submit that exact build to TestFlight. --- diff --git a/src/components/TelemetryConsentModal.tsx b/src/components/TelemetryConsentModal.tsx index 28ed48e..bf61b3f 100644 --- a/src/components/TelemetryConsentModal.tsx +++ b/src/components/TelemetryConsentModal.tsx @@ -7,6 +7,7 @@ import { View, Text, TouchableOpacity, StyleSheet, useColorScheme, Modal, Linking } from "react-native" import { Ionicons } from "@expo/vector-icons" +import { PRIVACY_POLICY_URL } from "../lib/links" interface Props { visible: boolean @@ -45,9 +46,7 @@ export function TelemetryConsentModal({ visible, onAllow, onDecline }: Props) { {/* Privacy policy link */} - Linking.openURL("https://agentlabs.cc/opencode/privacy")} - > + Linking.openURL(PRIVACY_POLICY_URL)}> Read our full privacy policy diff --git a/src/lib/links.ts b/src/lib/links.ts new file mode 100644 index 0000000..f22a0f5 --- /dev/null +++ b/src/lib/links.ts @@ -0,0 +1 @@ +export const PRIVACY_POLICY_URL = "https://dzianisv.github.io/opencode-mobile/privacy/" diff --git a/src/lib/sdk.ts b/src/lib/sdk.ts index 0b752fe..cc5b8f9 100644 --- a/src/lib/sdk.ts +++ b/src/lib/sdk.ts @@ -232,7 +232,10 @@ export function createClient(config: ClientConfig) { try { yield JSON.parse(data) } catch (err) { - console.warn("[SSE] Failed to parse event:", data.slice(0, 200), err) + console.warn("[SSE] Failed to parse event", { + length: data.length, + error: err instanceof Error ? err.message : String(err), + }) } } } diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts index 2655c32..6100162 100644 --- a/src/lib/sentry.ts +++ b/src/lib/sentry.ts @@ -50,6 +50,8 @@ export function initSentry() { return scrubEvent(event) }, beforeBreadcrumb(crumb) { + // Console output can contain malformed server payloads, prompts, or code. + if (crumb.category === "console") return null if (crumb.data && typeof crumb.data === "object") { crumb.data = redactObject(crumb.data as Record) } @@ -149,6 +151,7 @@ function scrubEvent(event: T): T { } } if (event.breadcrumbs) { + event.breadcrumbs = event.breadcrumbs.filter((crumb) => crumb.category !== "console") for (const crumb of event.breadcrumbs) { if (typeof crumb.message === "string") crumb.message = redactString(crumb.message) if (crumb.data && typeof crumb.data === "object") {