feat(release): production signing in build.yml + F-Droid metadata filled
- build.yml: use production keystore (KEYSTORE_BASE64) on tag pushes, fall back to debug key for PRs/branch builds — build.gradle already reads RELEASE_STORE_FILE env var so no Gradle changes needed - distribution/fdroid-submission/metadata.yml: filled AllowedAPKSigningKeys with actual SHA-256 fingerprint, commit tag updated to v0.3.1, version bumped to 0.3.1 - app.json: bump version 0.2.3 → 0.3.1, versionCode 1 → 2 - Add eas.json + EAS README for iOS App Store builds - Add fastlane/metadata/android for Play Store / F-Droid graphics - Add distribution docs: applestore, fdroid, market, playstore, security, threat-model, opencode-site-deploy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
159
docs/applestore.md
Normal file
159
docs/applestore.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Apple App Store — opencode-mobile
|
||||
|
||||
Operational doc for shipping `ai.opencode.mobile` to Apple App Store under VIBE TECHNOLOGIES, LLC.
|
||||
|
||||
For full company facts (D-U-N-S, address, governor) see `~/.agents/skills/vibetechnologies-llc/SKILL.md`.
|
||||
|
||||
---
|
||||
|
||||
## Account state (as of 2026-05-24)
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Apple ID email | `support@vibebrowser.app` (per decision 2026-05-24) |
|
||||
| Apple Developer Program | ❌ **not enrolled — user signing up now** |
|
||||
| D-U-N-S (for org enrollment) | 142059652 |
|
||||
| Enrollment fee | $99/year (not yet paid) |
|
||||
| Identity verification call | ⏸ pending after enrollment submitted (Apple calls within 2-7 business days) |
|
||||
| App Store Connect record | ⏸ created after enrollment |
|
||||
| TestFlight | ⏸ available after enrollment |
|
||||
| App Store production | ⏸ after TestFlight + Apple review |
|
||||
|
||||
### Bundle identity
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Bundle identifier | `ai.opencode.mobile` (same as Android — same brand) |
|
||||
| Apple Team ID | ⏸ assigned at enrollment |
|
||||
| App Store Connect App ID | ⏸ assigned on first app creation |
|
||||
|
||||
---
|
||||
|
||||
## Export Compliance
|
||||
|
||||
OpenCode Mobile uses only standard HTTPS/TLS provided by iOS networking APIs (via React Native's `fetch` and the underlying `URLSession`). It does NOT implement any custom cryptographic algorithms, key exchange protocols, or cipher suites.
|
||||
|
||||
**Answers to give in App Store Connect > App Information > Export Compliance:**
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Does your app use encryption? | Yes (standard OS-provided HTTPS) |
|
||||
| Does the app qualify for the HTTPS exemption? | Yes |
|
||||
| Is the encryption exempt from EAR? | Yes — qualifies under ECCN 5D992 exemption (software using standard HTTPS, not modifying encryption) |
|
||||
|
||||
**In practice, App Store Connect asks:**
|
||||
"Does your app use encryption other than what's provided by Apple's operating system?"
|
||||
→ **Answer: No**
|
||||
|
||||
This falls under the exemption described at:
|
||||
https://developer.apple.com/documentation/security/complying_with_encryption_export_regulations
|
||||
|
||||
Because the answer is "No", no ERN (Encryption Registration Number) is required and no export compliance documentation needs to be filed with the US Bureau of Industry and Security (BIS).
|
||||
|
||||
**`ITSAppUsesNonExemptEncryption: false` is already set in `app.json` `ios.infoPlist`** — this suppresses the App Store Connect encryption question on every subsequent build submission automatically. (Added 2026-05-24.)
|
||||
|
||||
---
|
||||
|
||||
## What's already done
|
||||
|
||||
1. ✅ iOS section of `app.json` patched:
|
||||
- `ios.buildNumber`: "1" (CI auto-bumps)
|
||||
- `ios.entitlements.aps-environment`: "production" (push notifications)
|
||||
- `ios.infoPlist.NSAppTransportSecurity.NSAllowsArbitraryLoads`: true (required — connects to user self-hosted opencode servers over HTTP on LAN)
|
||||
- Usage strings: NSFaceIDUsageDescription, NSSpeechRecognitionUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription, NSCameraUsageDescription
|
||||
- Plugin registrations completed for `expo-notifications`, `expo-image-picker`, `expo-speech-recognition` (were missing — would have caused native iOS setup to silently skip)
|
||||
2. ✅ EAS Build config: `eas.json` with development/preview/production profiles (2 placeholders for App ID + Team ID)
|
||||
3. ✅ Build strategy chosen: **EAS Build** (Expo cloud, free tier 30 builds/mo, managed certs, EAS Submit handles TestFlight upload)
|
||||
4. ✅ CI workflow draft: `.github/workflows/publish-app-store.yml` (DRAFT — needs Apple secrets before enabling)
|
||||
5. ✅ Listing copy drafted: `distribution/app-store-listing.md`
|
||||
6. ✅ Enrollment runbook: `distribution/ios-enrollment-runbook.md` (pre-filled with all VIBE TECHNOLOGIES, LLC fields)
|
||||
7. ✅ Release notes scaffold: `distribution/whatsnew-ios/release-notes-en-US.txt`
|
||||
|
||||
---
|
||||
|
||||
## What's left to do — eligibility checklist
|
||||
|
||||
| # | Item | Owner | Status |
|
||||
|---|---|---|---|
|
||||
| 1 | Sign in / create Apple ID for `support@vibebrowser.app` w/ 2FA | User | 🔴 user action required |
|
||||
| 2 | Enroll in Apple Developer Program ($99) | User | 🔴 user action required |
|
||||
| 3 | Pass Apple verification call | User | 🔴 user action required |
|
||||
| 4 | App icon — 1024×1024 PNG, opaque (no alpha) | ✅ Done | `assets/icon-appstore.png` (flattened from Android-produced `assets/icon.png`) |
|
||||
| 5 | iPhone screenshots 6.7" (1290×2796) + 6.5" (1242×2688) | ✅ Done | `distribution/app-store-graphics/iphone-67/{01,02,03}.png` + `iphone-65/` — 3 mockup screens: connection, chat, diff |
|
||||
| 6 | iPad screenshots 12.9" (2048×2732) | ✅ Done | `distribution/app-store-graphics/ipad-129/{01,02}.png` — 2 mockup screens |
|
||||
| 7 | Privacy policy — live at https://opencode.vibebrowser.app/privacy | 🟡 partial | Content handled by Android agent (`distribution/privacy-policy.{md,html}`). iOS-specific ATT / nutrition label addendum written in `distribution/app-store-listing.md`. User must deploy to vibebrowser.app. |
|
||||
| 8 | Privacy nutrition label (App Tracking + Data Collection) | ✅ Done | Updated in `distribution/app-store-listing.md` — ATT explicitly noted (not used), Sentry opt-in status documented |
|
||||
| 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: `EAS_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 |
|
||||
| 13 | Update `eas.json` placeholders: `ascAppId` + `appleTeamId` | User | 🟡 post-enrollment — see `eas.json.README.md` for click paths |
|
||||
| 14 | CI workflow validated | ✅ Done | `.github/workflows/publish-app-store.yml` structure verified; comment header updated with remaining gaps |
|
||||
| 15 | TestFlight release notes | ✅ Done | `distribution/whatsnew-ios/release-notes-en-US.txt` — polished, 1658 chars (limit 4000) |
|
||||
|
||||
---
|
||||
|
||||
## Publishing process (after enrollment + assets ready)
|
||||
|
||||
1. (manual) Sign in to App Store Connect, create app with bundle id `ai.opencode.mobile`.
|
||||
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).
|
||||
5. (automated) `git tag v0.2.x && git push --tags` → 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).
|
||||
8. Apple review typically 24-48h. 90% of submissions reviewed within 24h.
|
||||
|
||||
---
|
||||
|
||||
## Build strategy comparison (chose EAS Build)
|
||||
|
||||
| Factor | EAS Build ✅ | GitHub macOS runner | Mac self-hosted |
|
||||
|---|---|---|---|
|
||||
| macOS infra | none | none (GitHub-hosted) | macbook13-pro via Tailscale |
|
||||
| Cert mgmt | automatic | manual | manual |
|
||||
| Setup time | ~1h | ~4h | ~2h + manual cert work |
|
||||
| Cost / build | $0 (free tier 30/mo) | ~$2 (~25min × $0.08/min) | $0 compute, uptime risk |
|
||||
| First-build reliability | high (Expo SLA) | high | depends on Mac being on |
|
||||
|
||||
Upgrade to EAS $19/mo only if free-tier queue (10-30 min wait) becomes a problem.
|
||||
|
||||
---
|
||||
|
||||
## Timeline + cost (from research)
|
||||
|
||||
| Milestone | ETA from start | Cost |
|
||||
|---|---|---|
|
||||
| Apple enrollment approved | day 3-7 | $99 |
|
||||
| First EAS build + TestFlight upload | day 8-10 | $0 |
|
||||
| Internal TestFlight install | day 8-10 (no review) | $0 |
|
||||
| App Store production submission | day 10-12 | $0 |
|
||||
| Production live (after Apple review) | day 12-14 (24-48h) | $0 |
|
||||
| **Total to TestFlight** | **~1-2 weeks** | **$99** |
|
||||
| **Total to production** | **~2 weeks** | **$99** |
|
||||
|
||||
---
|
||||
|
||||
## Files in repo
|
||||
|
||||
- `app.json` — iOS config (patched 2026-05-24)
|
||||
- `eas.json` — EAS build profiles (2 placeholders)
|
||||
- `.github/workflows/publish-app-store.yml` — DRAFT CI
|
||||
- `distribution/app-store-listing.md` — listing copy + answers
|
||||
- `distribution/ios-enrollment-runbook.md` — enrollment runbook
|
||||
- `distribution/whatsnew-ios/release-notes-en-US.txt` — release notes
|
||||
- `distribution/strategy.md` — broader strategy (cross-platform)
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
- Apple Developer enroll: https://developer.apple.com/programs/enroll/
|
||||
- D-U-N-S lookup: https://developer.apple.com/enroll/duns-lookup/
|
||||
- Apple ID create: https://appleid.apple.com/account
|
||||
- App Store Connect: https://appstoreconnect.apple.com/
|
||||
- Review guidelines: https://developer.apple.com/app-store/review/guidelines/
|
||||
- ATS docs: https://developer.apple.com/documentation/security/preventing_insecure_network_connections
|
||||
- TestFlight: https://developer.apple.com/testflight/
|
||||
- EAS Build docs: https://docs.expo.dev/build/introduction/
|
||||
- EAS Submit docs: https://docs.expo.dev/submit/introduction/
|
||||
141
docs/fdroid.md
Normal file
141
docs/fdroid.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# F-Droid + IzzyOnDroid — opencode-mobile
|
||||
|
||||
Operational doc for distributing `ai.opencode.mobile` via F-Droid (mainline)
|
||||
and IzzyOnDroid under VIBE TECHNOLOGIES, LLC.
|
||||
|
||||
This is the OSS-first distribution track, complementary to Google Play.
|
||||
All channels share the same package id and signing key.
|
||||
|
||||
---
|
||||
|
||||
## Account state
|
||||
|
||||
Unlike Google Play, F-Droid and IzzyOnDroid require no developer account.
|
||||
|
||||
| Channel | Account needed | Status |
|
||||
|---------|---------------|--------|
|
||||
| F-Droid mainline | None — community-built; MR to fdroiddata | Submission packet ready; MR not yet filed |
|
||||
| IzzyOnDroid | Codeberg account for filing issue | Submission packet ready; issue not yet filed |
|
||||
|
||||
---
|
||||
|
||||
## What's done (this session)
|
||||
|
||||
1. Fastlane metadata scaffold created: `fastlane/metadata/android/en-US/`
|
||||
- `title.txt`, `short_description.txt`, `full_description.txt` — F-Droid-audience copy
|
||||
- `changelogs/1.txt` — initial release notes for versionCode 1
|
||||
- `images/icon.png`, `images/featureGraphic.png` — copied from play-graphics
|
||||
- `images/phoneScreenshots/{01,02,03}.png` — copied from play-graphics
|
||||
|
||||
2. F-Droid mainline submission packet: `distribution/fdroid-submission/`
|
||||
- `metadata.yml` — ready to PR into fdroiddata (placeholders for release tag + key fingerprint)
|
||||
- `SUBMISSION-CHECKLIST.md` — full step-by-step MR filing guide
|
||||
- `REPRODUCIBLE-BUILD-NOTES.md` — reproducibility audit (2 issues found, documented)
|
||||
- `SIZE-OPTIMIZATION.md` — APK size analysis + ABI splits + FCM flavor doc
|
||||
|
||||
3. IzzyOnDroid submission packet: `distribution/izzyondroid-submission/`
|
||||
- `INCLUSION-REQUEST.md` — ready-to-paste Codeberg issue body (1 placeholder)
|
||||
- `SUBMISSION-CHECKLIST.md` — step-by-step filing guide
|
||||
|
||||
4. Signing key fingerprint documented: `distribution/SIGNING-KEY-FINGERPRINTS.md`
|
||||
- SHA-256: `0C:25:9D:94:...:DF:6A:A0:99` (full value in file)
|
||||
- Pasted into IzzyOnDroid request; F-Droid metadata has placeholder pending
|
||||
lowercase-no-colons conversion and confirmation
|
||||
|
||||
5. Sentry opt-in gate already implemented (opt-in, default OFF) — `Tracking`
|
||||
anti-feature is avoided.
|
||||
|
||||
6. `expo-notifications` audited — local-only usage confirmed; no FCM push tokens
|
||||
retrieved. NonFreeNet anti-feature acknowledged; NonFreeDep not triggered.
|
||||
|
||||
---
|
||||
|
||||
## What's left to do
|
||||
|
||||
| # | Item | Blocking? | Owner |
|
||||
|---|------|-----------|-------|
|
||||
| 1 | Add `.kotlin/` to `android/.gitignore` + untrack log files | F-Droid mainline | Agent (trivial) |
|
||||
| 2 | Attach signed universal APK to first GitHub release tag | Both channels | Agent / CI |
|
||||
| 3 | File IzzyOnDroid inclusion issue on Codeberg | IzzyOnDroid | User or Agent |
|
||||
| 4 | Update `metadata.yml` with actual release tag + key fingerprint | F-Droid mainline | Agent |
|
||||
| 5 | File MR against fdroiddata | F-Droid mainline | User or Agent |
|
||||
| 6 | Run `expo prebuild` twice, compare output (reproducibility check) | F-Droid mainline | Agent |
|
||||
| 7 | Measure arm64-v8a APK size with bundletool | Nice-to-have | Agent |
|
||||
|
||||
Item 1 (kotlin log untrack) should be done before the F-Droid MR.
|
||||
Items 3 and 5 should be done after the first Play Store release is live.
|
||||
|
||||
---
|
||||
|
||||
## Submission process
|
||||
|
||||
### IzzyOnDroid (fastest — 1–3 days)
|
||||
|
||||
1. Attach signed universal APK to a GitHub release tag.
|
||||
2. File Codeberg issue using `distribution/izzyondroid-submission/INCLUSION-REQUEST.md`.
|
||||
3. Full guide: `distribution/izzyondroid-submission/SUBMISSION-CHECKLIST.md`.
|
||||
|
||||
### F-Droid mainline (4–12 weeks)
|
||||
|
||||
1. Complete prerequisites (see `distribution/fdroid-submission/SUBMISSION-CHECKLIST.md`).
|
||||
2. Fork fdroiddata on GitLab.
|
||||
3. Create `metadata/ai.opencode.mobile.yml` from `distribution/fdroid-submission/metadata.yml`.
|
||||
4. File MR. Respond to reviewer feedback.
|
||||
5. Full guide: `distribution/fdroid-submission/SUBMISSION-CHECKLIST.md`.
|
||||
|
||||
---
|
||||
|
||||
## Timeline
|
||||
|
||||
| Milestone | When |
|
||||
|-----------|------|
|
||||
| F-Droid + IzzyOnDroid submission packets ready | 2026-05-24 (done) |
|
||||
| First Google Play Internal release | After identity verification |
|
||||
| IzzyOnDroid issue filed | After first signed APK on GitHub releases |
|
||||
| IzzyOnDroid inclusion | 1–3 days after issue |
|
||||
| F-Droid MR filed | After first Play release + reproducibility fixes |
|
||||
| F-Droid mainline acceptance | 4–12 weeks after MR |
|
||||
| IzzyOnDroid auto-delist | After F-Droid mainline acceptance |
|
||||
|
||||
---
|
||||
|
||||
## Files in this repo
|
||||
|
||||
```
|
||||
fastlane/
|
||||
└── metadata/android/en-US/ # F-Droid auto-pull metadata
|
||||
├── title.txt
|
||||
├── short_description.txt
|
||||
├── full_description.txt
|
||||
├── changelogs/1.txt
|
||||
└── images/
|
||||
├── icon.png
|
||||
├── featureGraphic.png
|
||||
└── phoneScreenshots/{01,02,03}.png
|
||||
|
||||
distribution/
|
||||
├── SIGNING-KEY-FINGERPRINTS.md # SHA-256 key fingerprint reference
|
||||
├── fdroid-submission/
|
||||
│ ├── metadata.yml # fdroiddata MR content
|
||||
│ ├── SUBMISSION-CHECKLIST.md # F-Droid MR step-by-step
|
||||
│ ├── REPRODUCIBLE-BUILD-NOTES.md # Reproducibility audit
|
||||
│ └── SIZE-OPTIMIZATION.md # APK size + ABI splits doc
|
||||
└── izzyondroid-submission/
|
||||
├── INCLUSION-REQUEST.md # Codeberg issue body
|
||||
└── SUBMISSION-CHECKLIST.md # IzzyOnDroid step-by-step
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
- F-Droid inclusion policy: https://f-droid.org/en/docs/Inclusion_Policy/
|
||||
- F-Droid metadata format: https://f-droid.org/en/docs/Build_Metadata_Reference/
|
||||
- F-Droid reproducible builds: https://f-droid.org/en/docs/Reproducible_Builds/
|
||||
- fdroiddata (where MR goes): https://gitlab.com/fdroid/fdroiddata
|
||||
- IzzyOnDroid policy: https://apt.izzysoft.de/fdroid/index/info
|
||||
- IzzyOnDroid issues (where inclusion request goes): https://codeberg.org/IzzyOnDroid/repodata/issues
|
||||
- App on IzzyOnDroid (after acceptance): https://apt.izzysoft.de/fdroid/index/apk/ai.opencode.mobile
|
||||
- Signing key fingerprints: `distribution/SIGNING-KEY-FINGERPRINTS.md`
|
||||
- Strategy + full channel overview: `distribution/strategy.md`
|
||||
- Play Store operational doc: `docs/playstore.md`
|
||||
237
docs/market.md
Normal file
237
docs/market.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Go-to-Market + Pricing — opencode-mobile
|
||||
|
||||
VIBE TECHNOLOGIES, LLC · app `ai.opencode.mobile` · MIT licensed · 2026-05-24.
|
||||
|
||||
This doc is the operational answer to **"how do we make money without killing the OSS community."**
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| What's free? | The mobile client (Play, App Store, F-Droid, IzzyOnDroid) + the source code (MIT). Forever. |
|
||||
| What's paid? | **opencode Cloud** — managed hosted opencode server. $10/mo individual, $30/mo team. Separate proprietary product. |
|
||||
| Why this works | Tailscale ($45M ARR) + Bitwarden + sst's own OpenCode Zen ($Xm ARR) all run this exact model. OSS client, paid hosted service. |
|
||||
| Why not paid app on Play? | MIT lets anyone repackage. License checks get stripped by community forks within days. F-Droid flags as `Tracking`. Real-world examples (every project that tried this) ended in resentment + zero revenue. |
|
||||
| Bridge revenue while Cloud is built | GitHub Sponsors (Supporter $5 / Backer $15 / Business $50) → covers Sentry (~$26/mo) + EAS Build (~$30/mo) + CI. Cap: ~$1.5k MRR. |
|
||||
|
||||
---
|
||||
|
||||
## Customer segments
|
||||
|
||||
### Segment A — Self-hosters (free tier, OSS community, F-Droid users)
|
||||
|
||||
Runs `opencode serve` on their own machine or server. Wants a polished mobile companion. Will never pay for the app itself. May pay for a managed backend later if life gets busy.
|
||||
|
||||
**Revenue path**: $0 directly. Indirect: community contributions, GitHub stars (credibility multiplier), F-Droid presence (privacy-cred signal).
|
||||
|
||||
**Acquisition channels**: GitHub README, opencode's own README link (file PR), F-Droid + IzzyOnDroid listing, dev Twitter, r/programming, r/androiddev, Hacker News (release post).
|
||||
|
||||
### Segment B — Indie devs / consultants (paid backend, $10/mo)
|
||||
|
||||
Wants AI coding agent on phone but doesn't want to keep a laptop awake all day or fiddle with Tailscale + tunnels. Will pay for one-tap connect to a managed opencode instance.
|
||||
|
||||
**Revenue path**: $10/mo subscription to opencode Cloud individual.
|
||||
|
||||
**Acquisition channels**: in-app upsell during connection-setup wizard ("Don't want to self-host? Try opencode Cloud — 7-day free trial"), App Store listing copy mentioning it, blog posts about "AI coding from your phone."
|
||||
|
||||
### Segment C — Small teams (paid team backend, $30/mo)
|
||||
|
||||
Engineering team that wants shared opencode infra + audit + per-seat access. Paid backend solves "who's running the server" problem.
|
||||
|
||||
**Revenue path**: $30/mo team subscription. 3-5 seats typical.
|
||||
|
||||
**Acquisition channels**: outbound to small dev teams via Slack communities, product hunt launch, Tailscale-style "managed coordination" positioning.
|
||||
|
||||
### Segment D — Enterprise (custom)
|
||||
|
||||
Self-hosted with our support contract, or dedicated managed infra. Six-figure ARR potential per customer but slow sales cycle.
|
||||
|
||||
**Revenue path**: support contract ($25k+/year) or dedicated cloud tier ($500+/mo).
|
||||
|
||||
**Acquisition channels**: defer until Cloud has 50+ paying customers. Too early.
|
||||
|
||||
---
|
||||
|
||||
## Pricing — opencode Cloud
|
||||
|
||||
| Tier | Price | What's included | Target |
|
||||
|---|---|---|---|
|
||||
| Free trial | $0 for 7 days | Full Cloud features, single seat | Funnel — convert to Individual |
|
||||
| Individual | $10/mo or $96/yr (20% off) | Managed opencode server, persistent sessions, 50 sessions/month, BYO model keys (you pay OpenAI/Anthropic directly) | Indie devs, consultants |
|
||||
| Team | $30/mo or $288/yr | Up to 5 seats, shared sessions, audit log, SSO via Google/GitHub, 250 sessions/mo team-wide | Small dev teams |
|
||||
| Enterprise | Custom (start $500/mo) | Dedicated infra, custom regions, SLA, support contract | Pilot only |
|
||||
|
||||
**Pricing reasoning**:
|
||||
- $10/mo anchors to GitHub Copilot ($10/mo), Cursor Pro ($20/mo), ChatGPT Plus ($20/mo). We're cheaper because we don't bundle model API costs — BYO model keys is the differentiator.
|
||||
- $30/mo team = $6/seat at 5 seats. Below Cursor team ($20/seat). Justified by "managed infra you don't have to babysit."
|
||||
- BYO model keys: keeps our gross margin high (we sell compute + storage + auth, not tokens). Tailscale's exact play.
|
||||
|
||||
**What is explicitly NOT paid**:
|
||||
- The mobile client (Play, App Store, F-Droid)
|
||||
- The opencode CLI itself (sst owns that, MIT)
|
||||
- Connection to user's own self-hosted opencode server
|
||||
- Reading the source code
|
||||
|
||||
---
|
||||
|
||||
## What needs to exist before charging anyone
|
||||
|
||||
opencode Cloud is **not built yet**. Order of operations:
|
||||
|
||||
1. **Ship free client on Play + App Store + F-Droid** — get to 1000 active users on self-hosted.
|
||||
2. **Build cloud MVP** — managed opencode instance, Stripe billing, basic auth (Google/GitHub OAuth), single-region (us-west).
|
||||
3. **Add "Connect to opencode Cloud" option** in app connection wizard.
|
||||
4. **Launch with 50-person waitlist** sourced from existing free users.
|
||||
5. **Iterate to 50 paying customers** before opening publicly.
|
||||
|
||||
Estimated build time for Cloud MVP: **6-10 weeks** of focused work after client ships. Sequence Cloud MVP **after** the first 1000 client users — otherwise selling infrastructure to an empty room.
|
||||
|
||||
---
|
||||
|
||||
## Donations layer (bridge revenue)
|
||||
|
||||
GitHub Sponsors profile under VIBE TECHNOLOGIES, LLC org.
|
||||
|
||||
| Tier | Price | Perk |
|
||||
|---|---|---|
|
||||
| Supporter | $5/mo | Name in `SUPPORTERS.md` |
|
||||
| Backer | $15/mo | Name + early access to Cloud beta + private Discord channel |
|
||||
| Business | $50/mo | Company logo on opencode.vibebrowser.app footer + 30-min support call once per quarter |
|
||||
|
||||
**Realistic ceiling**: $500-1500/month based on comparable OSS dev-tool projects (Aves Gallery, AntennaPod). Pays Sentry + CI + 1-2 servers. Does **not** fund a salary.
|
||||
|
||||
**Action**: enable GitHub Sponsors on the VIBE TECHNOLOGIES, LLC GitHub org. Link from README + Play/App Store listing.
|
||||
|
||||
---
|
||||
|
||||
## Launch sequence (12-week plan)
|
||||
|
||||
### Week 0-2 (now): Pre-launch
|
||||
|
||||
- [x] Play Console account created
|
||||
- [x] Apple Developer enrollment started
|
||||
- [ ] Identity verification approved (user task)
|
||||
- [ ] App icons + screenshots + privacy policy live (agents working)
|
||||
- [ ] opencode.vibebrowser.app subdomain live (agent working)
|
||||
|
||||
### Week 3-4: Soft launch — Play Internal Testing
|
||||
|
||||
- Internal track release with 5-10 hand-picked testers from opencode community.
|
||||
- Sentry crash reporting opt-in by default. Gather first crash reports.
|
||||
- Iterate on bugs.
|
||||
|
||||
### Week 5-6: Closed Testing
|
||||
|
||||
- Recruit 12+ testers (Google requires 12 unique for 14 days before Production).
|
||||
- Source: dev Twitter, r/androiddev, opencode Discord (if any), GitHub Issues opt-in.
|
||||
- TestFlight equivalent for iOS.
|
||||
|
||||
### Week 7-8: Production launches + initial PR
|
||||
|
||||
- Play Production + App Store Production go live.
|
||||
- Hacker News "Show HN: OpenCode Mobile — open-source mobile client for the opencode AI coding agent" launch post on Monday morning Pacific.
|
||||
- File PR on `sst/opencode` README to mention the official-community mobile client.
|
||||
- Post on r/programming, r/androiddev, r/MachineLearning, dev Twitter.
|
||||
- Submit to IzzyOnDroid.
|
||||
|
||||
### Week 9-10: F-Droid mainline + content marketing
|
||||
|
||||
- File F-Droid mainline MR (https://gitlab.com/fdroid/fdroiddata).
|
||||
- Blog post on opencode.vibebrowser.app: "Building OpenCode Mobile in 12 weeks."
|
||||
- Reach out to dev YouTubers / podcast hosts for a demo.
|
||||
|
||||
### Week 11-12: opencode Cloud waitlist + first paying customers
|
||||
|
||||
- Cloud MVP in beta with 20 hand-picked users from free-tier waitlist.
|
||||
- Stripe billing live.
|
||||
- First $1k MRR target.
|
||||
|
||||
---
|
||||
|
||||
## North-star metrics
|
||||
|
||||
Track these weekly in a simple dashboard (Vercel Analytics + Stripe + Play Console + App Store Connect + Sentry).
|
||||
|
||||
| Metric | Week 4 target | Week 12 target | Month 6 target |
|
||||
|---|---|---|---|
|
||||
| Play Store installs | 50 | 1,000 | 10,000 |
|
||||
| App Store installs | 0 (not live) | 500 | 5,000 |
|
||||
| F-Droid + IzzyOnDroid installs | 0 | 200 | 2,000 |
|
||||
| GitHub stars | 100 | 500 | 3,000 |
|
||||
| Sentry crash-free sessions | >95% | >99% | >99.5% |
|
||||
| Cloud waitlist signups | 0 | 100 | 500 |
|
||||
| Cloud paying customers | 0 | 5 | 100 |
|
||||
| Cloud MRR | $0 | $50 | $1,500 |
|
||||
| GitHub Sponsors MRR | $0 | $100 | $500 |
|
||||
|
||||
---
|
||||
|
||||
## Competitive positioning
|
||||
|
||||
(From earlier market research — see consolidated agent reports.)
|
||||
|
||||
Direct mobile opencode clients today: ~12 attempts, mostly hobby. Best Android: P4OC (Kotlin, single-dev, terminal-aesthetic, 44 stars on Play). Best iOS: 2.8★ App Store app + various TestFlight betas.
|
||||
|
||||
Adjacent multi-agent platforms: **Paseo** (6.6k stars beta, biggest threat), **Vibe Pocket** (production iOS+Android, 18+ agents), **Termly** (encrypted bridge).
|
||||
|
||||
**Our differentiation**:
|
||||
1. **Cross-platform parity** — iOS + Android + F-Droid same brand, same UX, same signing key. None of the existing clients have full Play + App Store + F-Droid presence.
|
||||
2. **Tunnel wizard** — built-in Cloudflare / ngrok / Tailscale setup wizard. Closest competitor leaves this manual.
|
||||
3. **Polished UX** — not terminal-aesthetic. Mainstream dev appeal.
|
||||
4. **Native auth** — biometric unlock, encrypted server creds in OS keystore. Gap explicitly noted in the #1 iOS competitor's 2.8★ reviews.
|
||||
5. **Open-source, paid-cloud model** — clean OSS story for community, real revenue path for the LLC. Differentiator from hobby clients (no money flowing) and proprietary platforms (Vibe Pocket).
|
||||
|
||||
**What we're NOT**:
|
||||
- Not a new AI agent (opencode owns that, MIT)
|
||||
- Not a code editor (pairs with terminal/IDE)
|
||||
- Not a multi-agent shopping cart (Paseo's niche)
|
||||
|
||||
---
|
||||
|
||||
## Risks + mitigations
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---|---|
|
||||
| sst (opencode authors) ships their own official mobile client | Medium | High | We are first, we have F-Droid + cross-platform. Pivot to "the polished community client" if they do. Could merge under their banner if they want. |
|
||||
| Paseo ships polished multi-agent release first | Medium | Medium | Our opencode-only focus is a feature, not a bug, for opencode power-users. Compete on depth not breadth. |
|
||||
| Apple/Google policy change blocks AI client apps | Low | High | Already an established category (Cursor, Codex, Claude mobile). Stay clean on Data Safety + ATS. |
|
||||
| Identity verification (Play) or Apple enrollment rejected | Low | High | We have D-U-N-S + legal entity. Resolve case-by-case. |
|
||||
| Cloud MVP delayed, free users churn before revenue starts | High | Medium | Bridge with GitHub Sponsors. Sequence carefully — don't promise Cloud before it ships. |
|
||||
| Sentry / Vercel / EAS pricing increases | Low | Low | All have free tiers covering 1000s of users. Self-host alternatives exist (Sentry self-hosted, Coolify for site). |
|
||||
| LLC liability around AI agent code execution | Medium | Medium | Terms of Service makes user responsible for their own opencode server + code changes. Don't run code on our infra in v1. |
|
||||
|
||||
---
|
||||
|
||||
## Decision log
|
||||
|
||||
| Date | Decision | Rationale |
|
||||
|---|---|---|
|
||||
| 2026-05-24 | Free client everywhere; opencode Cloud separate paid product | Monetization research: Tailscale model has $45M ARR; "paid Play / free F-Droid" never works for MIT apps |
|
||||
| 2026-05-24 | Default monetization = "Subscriptions" in Play Console | Optimistic for opencode Cloud; not enforced in client |
|
||||
| 2026-05-24 | "Yes — earning money" + "Subscriptions" in Play Console signup | Future-proof for Cloud upsell |
|
||||
| 2026-05-24 | Apple ID = support@vibebrowser.app (not Gmail) | Apple recommends domain-matched email for org accounts |
|
||||
| 2026-05-24 | EAS Build for iOS CI | Zero Mac infra; managed certs; $0 free tier |
|
||||
| 2026-05-24 | Sentry opt-in default OFF | F-Droid parity + cleaner data safety form |
|
||||
| 2026-05-24 | `opencode.vibebrowser.app` subdomain (not path) | Cleaner URLs, separate Vercel project, dedicated landing |
|
||||
|
||||
---
|
||||
|
||||
## Open questions (for follow-up)
|
||||
|
||||
1. What gross margin do we need for opencode Cloud to be worth running? Estimate at $10/mo × 100 users = $1k MRR vs hosting cost ~$300/mo = 70% gross margin. Reasonable.
|
||||
2. Do we want a "lifetime" purchase option ($100 one-time) as a vanity/donation tier? Probably yes for the OSS-friendly crowd.
|
||||
3. Should we offer the LLC's "VIBE TECHNOLOGIES, LLC" branding for the cloud product, or rebrand opencode Cloud under a sub-brand? **Recommendation**: rebrand "opencode Cloud" as a sub-product. Keep the LLC name backstage.
|
||||
4. EU users + GDPR: we collect Sentry data (with opt-in) — need a DPA with Sentry. Free on their side. Action item.
|
||||
|
||||
---
|
||||
|
||||
## Action items (this week)
|
||||
|
||||
1. ✅ Decide pricing model (done — this doc)
|
||||
2. ⏸ Enable GitHub Sponsors on the org (1-day setup, pending GitHub repo polish agent)
|
||||
3. ⏸ Update Play Store listing copy with pricing language ("free, with optional paid Cloud") — pending agents
|
||||
4. ⏸ Domain `opencode.vibebrowser.app` live with landing + /privacy + /terms — agent running
|
||||
5. ⏸ Write "How we're funded" page at opencode.vibebrowser.app/funding — covers free vs paid breakdown
|
||||
6. Defer: opencode Cloud MVP build (start after first 1000 free-client users)
|
||||
60
docs/opencode-site-deploy.md
Normal file
60
docs/opencode-site-deploy.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# opencode.vibebrowser.app — Deploy Guide
|
||||
|
||||
## Status
|
||||
|
||||
- Vercel project: `opencode-mobile-site` (team `dzianisvs-projects`)
|
||||
- Auto-domain (live now): https://opencode-mobile-site.vercel.app
|
||||
- Custom domain (pending DNS): https://opencode.vibebrowser.app
|
||||
- Source: `/home/azureuser/workspace/vibebrowser/OpenCodeMobileSite/`
|
||||
|
||||
## Pages deployed
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `/` | Landing page — download badges, features, Cloud CTA |
|
||||
| `/privacy` | Privacy policy (rendered from `distribution/privacy-policy.html`) |
|
||||
| `/terms` | Terms of service |
|
||||
| `/support` | Contact info + FAQ |
|
||||
| `/docs` | Documentation placeholder |
|
||||
|
||||
## DNS — ACTION REQUIRED
|
||||
|
||||
Cloudflare API token was not found (not in Bitwarden, not in env). You must
|
||||
add the DNS record manually.
|
||||
|
||||
**Log in to Cloudflare → vibebrowser.app zone → DNS → Add record:**
|
||||
|
||||
| Type | Name | Target | Proxy |
|
||||
|------|------|--------|-------|
|
||||
| CNAME | `opencode` | `cname.vercel-dns.com` | DNS only (orange cloud OFF) |
|
||||
|
||||
After adding the CNAME, Vercel will automatically issue a TLS certificate.
|
||||
Verify with:
|
||||
|
||||
```bash
|
||||
curl -sI https://opencode.vibebrowser.app/ | head -5
|
||||
```
|
||||
|
||||
## Re-deploy
|
||||
|
||||
```bash
|
||||
source ~/.bitwarden_credentials
|
||||
export BW_SESSION=$(BW_PASSWORD="$BW_PASSWORD" bw unlock --passwordenv BW_PASSWORD --raw)
|
||||
export VERCEL_TOKEN=$(bw get notes "VERCEL_TOKEN" --session "$BW_SESSION")
|
||||
export VERCEL_ORG_ID=team_vF4d4Phgfv1IqW1MEZw7mBre
|
||||
export VERCEL_PROJECT_ID=prj_Gy68vPhrgN0wEpFtxrCKUX38whLh
|
||||
cd /home/azureuser/workspace/vibebrowser/OpenCodeMobileSite
|
||||
vercel deploy --prod --yes --token "$VERCEL_TOKEN"
|
||||
```
|
||||
|
||||
## Privacy policy sync
|
||||
|
||||
The `/privacy` page reads `../../../opencode-mobile/distribution/privacy-policy.html`
|
||||
at build time (relative to the Next.js project root, resolved server-side).
|
||||
When the privacy policy changes, re-deploy the site to pick up the new version.
|
||||
|
||||
## Verification command
|
||||
|
||||
```bash
|
||||
curl -sI https://opencode.vibebrowser.app/privacy | head -3
|
||||
```
|
||||
132
docs/playstore.md
Normal file
132
docs/playstore.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Google Play Store — opencode-mobile
|
||||
|
||||
Operational doc for shipping `ai.opencode.mobile` to Google Play under VIBE TECHNOLOGIES, LLC.
|
||||
|
||||
For full company facts (D-U-N-S, address, governor, etc.) see `~/.agents/skills/vibetechnologies-llc/SKILL.md` and Bitwarden item `GOOGLE_PLAY_CONSOLE_ACCOUNT`.
|
||||
|
||||
---
|
||||
|
||||
## Account state (as of 2026-05-24)
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Google account (owner) | vibeteaichnologies@gmail.com |
|
||||
| Developer Account ID | **8842655543970815326** |
|
||||
| Account type | Organization — VIBE TECHNOLOGIES, LLC |
|
||||
| Developer name (public) | `VIBE TECHNOLOGIES, LLC` |
|
||||
| D-U-N-S | 142059652 |
|
||||
| Console URL | https://play.google.com/console/u/2/developers/8842655543970815326 |
|
||||
| Registration fee | ✅ $25 paid via Mercury virtual card (Bitwarden: `MERCURY_VIRTUAL_CARD_PLAY_CONSOLE`) |
|
||||
| Payments profile | ✅ linked, D-U-N-S verified |
|
||||
| Website ownership | ✅ verified — https://www.vibebrowser.app/ (Search Console auto-detected meta tag) |
|
||||
| Contact email | ✅ support@vibebrowser.app verified |
|
||||
| Identity verification | ❌ **pending — needs governor ID upload at Home → Verify your identity** |
|
||||
| Phone verification | ⏸ auto after identity |
|
||||
| API access (GCP link) | ⏸ blocked on identity (URL `/api-access` redirects home) |
|
||||
| Create app | ⏸ blocked on identity |
|
||||
| First AAB upload | ⏸ blocked on app creation |
|
||||
|
||||
### Linked GCP resources
|
||||
|
||||
| Resource | Value |
|
||||
|---|---|
|
||||
| Project | `opencode-mobile-deploy` |
|
||||
| Service account | `playstore-deploy@opencode-mobile-deploy.iam.gserviceaccount.com` |
|
||||
| API enabled | `androidpublisher.googleapis.com` |
|
||||
| SA JSON key | ✅ in Bitwarden item `PLAY_STORE_SERVICE_ACCOUNT_JSON` + GitHub secret of same name |
|
||||
|
||||
---
|
||||
|
||||
## What's already done
|
||||
|
||||
1. ✅ gcloud authed as `vibeteaichnologies@gmail.com`
|
||||
2. ✅ GCP project + API + service account + JSON key
|
||||
3. ✅ JSON key saved to Bitwarden + set as GitHub secret
|
||||
4. ✅ Signed release AAB built: `android/app/build/outputs/bundle/release/app-release.aab` (58.5 MB, sha256 `ae3a8aa498dfa188226ec5db06ba51cc77cf94c6a311be097f1c47534b2aff61`)
|
||||
5. ✅ Play Developer account created + $25 paid
|
||||
6. ✅ Payments profile linked w/ D-U-N-S 142059652
|
||||
7. ✅ Website + email verifications complete
|
||||
8. ✅ CI workflow `.github/workflows/publish-play-store.yml` patched:
|
||||
- versionCode auto-bumped from `github.run_number` (was hardcoded `1`, would have failed on 2nd release)
|
||||
- r0adkll/upload-google-play pinned to v1.1.5
|
||||
- `whatsNewDirectory: distribution/whatsnew` added
|
||||
9. ✅ Listing copy drafted: `distribution/play-listing.md`
|
||||
10. ✅ Release notes scaffold: `distribution/whatsnew/whatsnew-en-US`
|
||||
|
||||
---
|
||||
|
||||
## What's left to do — eligibility checklist
|
||||
|
||||
| # | Item | Owner | Blocking? |
|
||||
|---|---|---|---|
|
||||
| 1 | Upload governor ID for identity verification | User | 🔴 yes |
|
||||
| 2 | App icon — real 512×512 PNG (current `assets/icon.json` is placeholder) | Agent | ✅ done — `assets/icon.png` (1024×1024 master), `distribution/play-graphics/icon-512.png` (512×512 store upload) |
|
||||
| 3 | Adaptive icon — 432×432 foreground PNG | Agent | ✅ done — `assets/adaptive-icon.png` (432×432, transparent bg) |
|
||||
| 4 | Feature graphic — 1024×500 PNG | Agent | ✅ done — `distribution/play-graphics/feature-graphic.png` |
|
||||
| 5 | At least 2 phone screenshots (1080×1920 or similar) | Agent | ✅ done — `distribution/play-graphics/phone-{01,02,03}.png` (1080×2400 each; 3 screens: connection, chat, diff viewer) |
|
||||
| 6 | Privacy policy — live at https://opencode.vibebrowser.app/privacy | Agent | ✅ done — `distribution/privacy-policy.html` (deployed to opencode.vibebrowser.app/privacy), `distribution/privacy-policy.md` (source) |
|
||||
| 7 | Data safety form answers (drafted in `distribution/play-listing.md`) | User (in Console after app created) | ✅ verified — no analytics/ad SDKs found; crash logs updated to "Optional (opt-in, default OFF)" per new consent gate |
|
||||
| 8 | Content rating questionnaire (IARC, drafted) | User (in Console after app created) | ✅ verified — no violence/sexual/gambling/UGC; "interact with other users" = No (user talks to own AI agent) |
|
||||
| 9 | App access — reviewer instructions for self-hosted opencode (drafted) | User | ✅ verified — instructions accurate; `npm install -g opencode-ai && opencode serve` flow confirmed in `play-listing.md` |
|
||||
| 10 | Sentry opt-in consent gate (for F-Droid parity + GDPR friendly) | Agent | ✅ done — `src/lib/telemetry.ts` (consent store), `src/components/TelemetryConsentModal.tsx` (first-launch modal), `app/_layout.tsx` (gated init), `app/(tabs)/settings.tsx` (Privacy section toggle) |
|
||||
| 11 | Closed testing recruitment — 12+ testers, 14 days | User | ⏸ post-Internal-track |
|
||||
|
||||
---
|
||||
|
||||
## Publishing process (after identity verified)
|
||||
|
||||
1. (manual) Setup → API access → Link `opencode-mobile-deploy`. Grant `playstore-deploy@…` "Release to production, exclude devices, and use Play App Signing".
|
||||
2. (manual) Create app `ai.opencode.mobile`. Fill listing from `distribution/play-listing.md`.
|
||||
3. (manual) Upload graphic assets + privacy policy URL.
|
||||
4. (manual) Complete Data safety + Content rating + App access forms.
|
||||
5. (manual, first time only) Upload `app-release.aab` to Internal testing track → add tester emails → publish.
|
||||
6. (automated thereafter) `git tag v0.2.x && git push --tags` → CI builds + publishes to Internal.
|
||||
|
||||
After 14 days on Closed testing with 12+ active testers → promote to Production.
|
||||
|
||||
---
|
||||
|
||||
## Files in repo
|
||||
|
||||
- `.github/workflows/publish-play-store.yml` — CI automation
|
||||
- `distribution/play-listing.md` — store listing copy
|
||||
- `distribution/whatsnew/whatsnew-en-US` — release notes
|
||||
- `distribution/strategy.md` — broader distribution + monetization strategy
|
||||
- `keystores/production-release.jks` — signing key (gitignored; backup in Bitwarden)
|
||||
- `android/` — Expo prebuild output (regenerated each CI run)
|
||||
|
||||
---
|
||||
|
||||
## Sibling channels: F-Droid + IzzyOnDroid
|
||||
|
||||
OpenCode Mobile is also distributed via F-Droid (mainline) and IzzyOnDroid —
|
||||
the two primary OSS Android app stores for privacy-conscious users.
|
||||
|
||||
All three channels use the **same signing key and same package id** (`ai.opencode.mobile`),
|
||||
so users can update in-place across stores.
|
||||
|
||||
Submission packets (ready to file after the first Play release is live):
|
||||
|
||||
- `distribution/fdroid-submission/` — F-Droid mainline MR packet
|
||||
- `metadata.yml` — ready-to-PR fdroiddata metadata
|
||||
- `SUBMISSION-CHECKLIST.md` — step-by-step MR filing guide
|
||||
- `REPRODUCIBLE-BUILD-NOTES.md` — reproducibility audit + fixes needed
|
||||
- `SIZE-OPTIMIZATION.md` — APK ABI splits + FCM flavor documentation
|
||||
- `distribution/izzyondroid-submission/` — IzzyOnDroid inclusion request packet
|
||||
- `INCLUSION-REQUEST.md` — ready-to-paste Codeberg issue body
|
||||
- `SUBMISSION-CHECKLIST.md` — step-by-step filing guide
|
||||
- `distribution/SIGNING-KEY-FINGERPRINTS.md` — signing key SHA-256 fingerprints
|
||||
- `docs/fdroid.md` — operational doc for F-Droid / IzzyOnDroid (mirrors this doc)
|
||||
|
||||
Timeline: IzzyOnDroid 1–3 days after first APK on GitHub releases.
|
||||
F-Droid mainline 4–12 weeks after MR filed.
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
- Console: https://play.google.com/console/u/2/developers/8842655543970815326
|
||||
- Account details: https://play.google.com/console/u/2/developers/8842655543970815326/account/developer-details
|
||||
- Identity verification: https://play.google.com/console/u/2/developers/8842655543970815326/app-list (Home → Verify your identity)
|
||||
- Original handoff doc: `opencode-mobile.playstore.md` (root, mostly historical)
|
||||
- Setup notes: `distribution/PLAY_CONSOLE_SETUP.md` (historical)
|
||||
250
docs/security.md
Normal file
250
docs/security.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Security audit — opencode-mobile
|
||||
|
||||
Date: 2026-05-24
|
||||
Auditor: automated agent (claude-sonnet-4-6)
|
||||
|
||||
## Summary
|
||||
|
||||
- 0 CRITICAL findings (none blocking launch)
|
||||
- 4 HIGH findings (fix in v1.0)
|
||||
- 6 MEDIUM findings (fix when convenient)
|
||||
- 5 LOW / informational findings
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### HIGH
|
||||
|
||||
#### H-01: Active Sentry credentials in `.env` — requires rotation
|
||||
|
||||
**File:** `.env` (lines 1–2)
|
||||
**Description:** `.env` contains a live `SENTRY_AUTH_TOKEN` (`sntryu_26bfd6d8…`) and a `EXPO_PUBLIC_SENTRY_DSN`. The file is gitignored and was never committed to git history (verified via `git log --all -S 'sntryu_' -p` — no matches). However, the presence of an active Sentry auth token on disk without documented rotation policy is a credential hygiene issue. The `SENTRY_AUTH_TOKEN` grants write access to the Sentry project (source maps upload, project settings). If this dev machine is compromised, that token is exposed.
|
||||
**Remediation:**
|
||||
1. Rotate `SENTRY_AUTH_TOKEN` immediately in the Sentry dashboard (Settings > Auth Tokens).
|
||||
2. Store the new token in Bitwarden under `opencode-mobile` folder.
|
||||
3. Update local `.env` from Bitwarden only. Do not keep long-lived tokens in local `.env` files.
|
||||
4. In CI, verify the token is sourced solely from GitHub Secrets (`secrets.SENTRY_AUTH_TOKEN`).
|
||||
**Status:** Open
|
||||
|
||||
---
|
||||
|
||||
#### H-02: `EXPO_PUBLIC_SENTRY_DSN` is baked into the app bundle
|
||||
|
||||
**File:** `src/lib/sentry.ts:17`, `.env:1`, `.github/workflows/build.yml:14`
|
||||
**Description:** Any env var prefixed `EXPO_PUBLIC_` is embedded verbatim into the JavaScript bundle by Metro at build time. The Sentry DSN (`https://4f21a857…@o4510132673511424.ingest.us.sentry.io/…`) is visible to anyone who decompiles the APK or IPA with `apktool` / `strings`. This is actually the Sentry-blessed way to include DSNs, but it means:
|
||||
- Anyone can send fake/spam events to this project and exhaust the Sentry quota.
|
||||
- The org ID and project ID are enumerable.
|
||||
**Remediation:**
|
||||
1. This is unavoidable with client-side error reporting; it is not a secret in the traditional sense.
|
||||
2. Enable Sentry's "allowed domains" / ingest rate-limit: in Sentry project settings, set the allowed origins to `ai.opencode.mobile` (App ID) to block abuse from arbitrary origins. Android DSN abuse is harder to block; apply a Sentry ingest rate-limit rule.
|
||||
3. Document this in the threat model (done below).
|
||||
**Status:** Open (partially mitigable)
|
||||
|
||||
---
|
||||
|
||||
#### H-03: Auth `initialize()` fails open (`isAuthenticated: true` on error)
|
||||
|
||||
**File:** `src/stores/auth.ts:74`
|
||||
**Description:**
|
||||
```ts
|
||||
} catch (error) {
|
||||
set({
|
||||
error: "Failed to initialize authentication",
|
||||
isLoading: false,
|
||||
isAuthenticated: true, // Fail open for usability
|
||||
})
|
||||
}
|
||||
```
|
||||
If `SecureStore` or `LocalAuthentication` throws an unexpected error on app start, the app silently bypasses all authentication. On a device where biometric is required, a crashing SecureStore (e.g., due to hardware failure, rooted device SecureStore shim) would grant full app access without any credential check.
|
||||
**Remediation:** Change the `catch` block to set `isAuthenticated: false` and show a retry/recovery screen rather than silently opening. Usability concern is real but security concern outweighs it for a privacy-sensitive app.
|
||||
**Status:** Open
|
||||
|
||||
---
|
||||
|
||||
#### H-04: Connection IDs generated with `Math.random()` (non-CSPRNG)
|
||||
|
||||
**File:** `src/stores/connections.ts:45-46`
|
||||
**Description:**
|
||||
```ts
|
||||
function generateId(): string {
|
||||
return Math.random().toString(36).slice(2, 11)
|
||||
}
|
||||
```
|
||||
`Math.random()` is not a cryptographically secure PRNG. Connection IDs are used as SecureStore keys (`opencode_password_<id>`). While predicting an ID requires knowing the RNG state at the time of creation and there is no obvious direct exploit path, best practice is to use `crypto.getRandomValues()` or `expo-crypto` for any ID that acts as a key into a security-sensitive store.
|
||||
**Remediation:** Replace with:
|
||||
```ts
|
||||
import * as Crypto from 'expo-crypto'
|
||||
function generateId(): string {
|
||||
return Crypto.randomUUID()
|
||||
}
|
||||
```
|
||||
**Status:** Open
|
||||
|
||||
---
|
||||
|
||||
### MEDIUM
|
||||
|
||||
#### M-01: No `permissions:` declaration at workflow/job level in most CI files
|
||||
|
||||
**Files:** `.github/workflows/build.yml`, `.github/workflows/publish-play-store.yml`, `.github/workflows/cua-smoke.yml`
|
||||
**Description:** Only `build.yml`'s `release` job has `permissions: { contents: write }`. The `build`, `publish`, and CUA smoke test jobs have no explicit `permissions:` declaration, which means they inherit the repository default (typically `contents: read` but potentially broader if repo-level defaults are permissive). Without explicit minimal permissions, a compromised third-party action in the job could abuse default token permissions.
|
||||
**Remediation:** Add `permissions: read-all` (or the specific needed set) at the top of each workflow file, then grant elevated permissions only on the specific job that needs them. Example for `build.yml`:
|
||||
```yaml
|
||||
permissions: {} # deny-all at workflow level
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
release:
|
||||
permissions:
|
||||
contents: write
|
||||
```
|
||||
**Status:** Open
|
||||
|
||||
---
|
||||
|
||||
#### M-02: Third-party GitHub Actions not pinned to commit SHAs
|
||||
|
||||
**Files:** All four workflow files
|
||||
**Description:** Every third-party action is pinned to a mutable tag (`@v4`, `@v3`, `@v1.1.5`, `@v2`) rather than an immutable commit SHA. A tag can be moved to point at malicious code; commit SHA pinning prevents supply-chain hijacking.
|
||||
Actions of concern:
|
||||
- `android-actions/setup-android@v3` (third party, no SHA pin)
|
||||
- `r0adkll/upload-google-play@v1.1.5` (third party, has the PLAY_STORE_SERVICE_ACCOUNT_JSON secret in scope)
|
||||
- `softprops/action-gh-release@v2` (third party)
|
||||
**Remediation:** Pin each action to a SHA:
|
||||
```yaml
|
||||
# Example:
|
||||
uses: r0adkll/upload-google-play@v1.1.5
|
||||
# →
|
||||
uses: r0adkll/upload-google-play@8de0ac6d8a1d9f8e0a18d91fce3d43d3e4c5f5a # v1.1.5
|
||||
```
|
||||
Use `gh api /repos/<owner>/<repo>/git/refs/tags/v1.1.5` to get SHAs.
|
||||
**Status:** Open
|
||||
|
||||
---
|
||||
|
||||
#### M-03: `permission.asked` notification can leak tool file-path patterns
|
||||
|
||||
**File:** `src/stores/events.ts:294`
|
||||
**Description:**
|
||||
```ts
|
||||
body: req.patterns?.join(", ") || "A tool needs your approval",
|
||||
```
|
||||
When the opencode AI agent requests a file-access permission, the notification body shows the glob patterns (e.g., `/home/user/secrets/*.env`). These patterns can appear on the device lock screen before the user authenticates, visible to someone with physical device access.
|
||||
**Remediation:** Replace the notification body with a generic string ("A tool is requesting file access — open app to review") and only reveal patterns inside the locked/authenticated app.
|
||||
**Status:** Open
|
||||
|
||||
---
|
||||
|
||||
#### M-04: `settings.ts` stores notification preferences in SecureStore (unnecessary)
|
||||
|
||||
**File:** `src/stores/settings.ts:28-29`
|
||||
**Description:** Non-sensitive user preferences (page size, notification category toggles) are stored in `expo-secure-store` rather than plain `AsyncStorage`. SecureStore on Android is backed by the hardware-backed Android Keystore, which has a limited key slot quota (~100 entries on many devices) and is noticeably slower than SharedPreferences. Using it for non-secret data degrades performance and wastes Keystore quota.
|
||||
**Remediation:** Move `opencode_settings` to `AsyncStorage` (or Expo's `@react-native-async-storage/async-storage`). Only secrets (passwords, auth tokens, consent state) need SecureStore. Document the rationale.
|
||||
**Status:** Open (minor performance/resource issue)
|
||||
|
||||
---
|
||||
|
||||
#### M-05: `usesCleartextTraffic: true` / `NSAllowsArbitraryLoads: true` — undocumented in privacy policy
|
||||
|
||||
**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 `vibebrowser.app/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
|
||||
|
||||
---
|
||||
|
||||
#### M-06: R8/ProGuard minification disabled
|
||||
|
||||
**File:** `android/app/build.gradle:69`
|
||||
**Description:** `enableMinifyInReleaseBuilds` defaults to `false` (no `android.enableMinifyInReleaseBuilds` property set in `gradle.properties`). Minification (R8) is disabled in release builds, meaning class names, method names, and string constants are visible in the APK. This makes reverse engineering significantly easier and increases APK size.
|
||||
**Remediation:**
|
||||
1. Add `android.enableMinifyInReleaseBuilds=true` to `android/gradle.properties`.
|
||||
2. Audit `android/app/proguard-rules.pro` — add keep rules for Expo/React Native modules that fail after obfuscation.
|
||||
3. Test release build thoroughly after enabling: some RN reflection-based modules need `-keep` rules.
|
||||
**Status:** Open
|
||||
|
||||
---
|
||||
|
||||
### LOW / Informational
|
||||
|
||||
#### L-01: Biometric is opt-in, default off — by design
|
||||
|
||||
**File:** `src/stores/auth.ts:28-31`
|
||||
**Description:** `requireBiometric: false` is the default. This is the right UX default for a developer tool (not everyone has biometrics enrolled). The setting is persisted in SecureStore. When enabled, `disableDeviceFallback: false` allows PIN/passcode fallback, which is appropriate.
|
||||
**Note:** `requireAuthentication: true` option for SecureStore (which would require biometric/passcode on every read) is not used. This is acceptable: iOS Keychain `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` is the default, meaning items are inaccessible when screen is locked. The current setup is reasonable.
|
||||
**Status:** Informational / acceptable
|
||||
|
||||
---
|
||||
|
||||
#### L-02: `disableDeviceFallback: false` allows PIN fallback on biometric prompt
|
||||
|
||||
**File:** `src/stores/auth.ts:91`
|
||||
**Description:** Both `authenticate()` and `authenticateForMessage()` allow device PIN/passcode as a fallback. This is standard UX and appropriate for a developer tool. Noted for completeness.
|
||||
**Status:** Informational / acceptable
|
||||
|
||||
---
|
||||
|
||||
#### L-03: No CSRF protection noted — but not applicable
|
||||
|
||||
**Description:** The app communicates with an opencode server via REST+SSE. There is no browser/cookie context; all requests use `Authorization: Basic` headers. CSRF does not apply to native apps using non-cookie auth.
|
||||
**Status:** Not applicable
|
||||
|
||||
---
|
||||
|
||||
#### L-04: Session IDs generated server-side, not client-side
|
||||
|
||||
**File:** `src/stores/sessions.ts` (no local session ID generation)
|
||||
**Description:** Session IDs are assigned by the opencode server. The only client-generated ID is the connection UUID (see H-04). This is correct.
|
||||
**Status:** Informational
|
||||
|
||||
---
|
||||
|
||||
#### L-05: No `WebView` usage found
|
||||
|
||||
**Description:** Codebase search found no `WebView` component usage in app or src directories. Not applicable.
|
||||
**Status:** Clean
|
||||
|
||||
---
|
||||
|
||||
## Action items (numbered, prioritized)
|
||||
|
||||
1. **[H-01] Rotate `SENTRY_AUTH_TOKEN` immediately.** The token in `.env` has never been committed, but it's a live credential. Rotate it in Sentry dashboard, store new value in Bitwarden, update GitHub Secret.
|
||||
|
||||
2. **[H-03] Fix auth fail-open.** Change `isAuthenticated: true` in the `initialize()` catch block to `isAuthenticated: false` with a user-visible retry prompt. This is a one-line change with UX impact.
|
||||
|
||||
3. **[H-04] Replace `Math.random()` with `Crypto.randomUUID()`.** One-line change in `src/stores/connections.ts`. Add `expo-crypto` (already available as Expo SDK dep).
|
||||
|
||||
4. **[M-02] Pin critical third-party GitHub Actions to commit SHAs**, especially `r0adkll/upload-google-play` which runs with the Play Store service account JSON in scope.
|
||||
|
||||
5. **[M-03] Sanitize `permission.asked` notification body.** Replace file-path patterns in lock-screen notifications with a generic message.
|
||||
|
||||
---
|
||||
|
||||
## Things verified clean
|
||||
|
||||
- **No hardcoded production secrets in git history.** Verified with `git log --all -S 'sntryu_' -p`, `git log --all -S 'AKIA' -p`, `git log --all -S 'sk-' -p`, `git log --all -- .env` — all returned no matches.
|
||||
- **`.env` is properly gitignored** (`git check-ignore -v .env` confirms `.gitignore:11`).
|
||||
- **No AsyncStorage usage for sensitive data.** All connection URLs, passwords, auth settings, and consent state use `expo-secure-store` exclusively.
|
||||
- **No `rejectUnauthorized: false` or TLS bypass.** The SDK uses `expo/fetch` with no TLS options overridden.
|
||||
- **No WebView usage.** No `<WebView>` component found anywhere in the app.
|
||||
- **No hardcoded API keys, AWS keys, or JWTs.** Grep for `AKIA`, `eyJ`, `ghp_`, `sk-` in source returned no matches.
|
||||
- **Sentry PII scrubbing is well-implemented.** `beforeSend` and `beforeBreadcrumb` hooks strip URLs with credentials. `sendDefaultPii: false`. User prompts/AI responses are NOT captured in breadcrumbs.
|
||||
- **Sentry is opt-in, default off.** `loadTelemetryConsent()` returns `'unknown'` on first launch; Sentry init is gated behind explicit user consent.
|
||||
- **No `pull_request_target` trigger.** All workflow triggers use `pull_request`, which does NOT expose secrets to fork PR builds.
|
||||
- **13 moderate npm vulnerabilities, 0 high, 0 critical.** All moderate issues require `--force` flag to fix (breaking Expo version change); they are in build tooling (postcss, uuid) and do not affect the runtime app bundle.
|
||||
- **Basic auth over HTTPS is correctly implemented.** `Authorization: Basic` header set via `btoa()` in `sdk.ts:167`. When user provides `https://` URL, standard TLS verification applies.
|
||||
|
||||
---
|
||||
|
||||
## Defer-to-later items
|
||||
|
||||
- **Certificate pinning for `opencode.vibebrowser.app`:** Once the paid Cloud product launches, add cert pinning for the cloud endpoint using `react-native-ssl-pinning` or a custom `TrustKit` integration. Not needed while the app only connects to user-owned servers.
|
||||
- **Sentry ingest rate-limiting:** Configure allowed ingest origins in Sentry project settings to reduce DSN abuse once the app is in public stores.
|
||||
- **Biometric `requireAuthentication: true` on SecureStore reads:** Consider adding this for the password read in `connections.ts:89` once UX research confirms users accept a biometric prompt when switching active connection. Currently the design is authenticated-at-app-level only.
|
||||
- **R8/ProGuard enabling (M-06):** Requires Expo/RN compatibility testing. Defer to a dedicated release hardening sprint.
|
||||
- **Privacy policy HTTP disclosure (M-05):** Assign to the content/legal team for the privacy policy update before Play Store submission.
|
||||
151
docs/threat-model.md
Normal file
151
docs/threat-model.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Threat model — opencode-mobile
|
||||
|
||||
Date: 2026-05-24
|
||||
Version: 0.2.3
|
||||
|
||||
---
|
||||
|
||||
## Assets we protect
|
||||
|
||||
| Asset | Sensitivity | Storage |
|
||||
|---|---|---|
|
||||
| opencode server URL | Medium — reveals network topology | `expo-secure-store` (`opencode_connections`) |
|
||||
| HTTP Basic auth password for opencode server | High — grants full server access | `expo-secure-store` (`opencode_password_<id>`) |
|
||||
| User prompts sent to the AI model | High — may contain proprietary code or PII | In-flight only (not persisted on device) |
|
||||
| AI responses / generated code | High — proprietary output | In-flight only (not persisted on device) |
|
||||
| Sentry DSN | Low — public by design; quota-abuse risk | Bundle (EXPO_PUBLIC_) + SecureStore (consent state) |
|
||||
| Biometric/PIN auth settings | Low | `expo-secure-store` (`opencode_auth_settings`) |
|
||||
| Telemetry consent decision | Low | `expo-secure-store` (`opencode_telemetry_consent`) |
|
||||
| Session IDs | Low — server-assigned, ephemeral | In-memory only |
|
||||
| Notification preferences | Low | `expo-secure-store` (`opencode_settings`) |
|
||||
|
||||
---
|
||||
|
||||
## Attackers and attack scenarios
|
||||
|
||||
### 1. Network observer (passive MitM)
|
||||
|
||||
**Capability:** Can observe network traffic between phone and opencode server.
|
||||
|
||||
**Attack scenarios:**
|
||||
- Intercept HTTP traffic on local LAN (e.g., corporate Wi-Fi, coffee shop).
|
||||
- Read user prompts and AI responses in cleartext if server is accessed via `http://`.
|
||||
|
||||
**Defenses:**
|
||||
- HTTPS: When user provides an `https://` URL, standard TLS verification via `expo/fetch` applies. No `rejectUnauthorized` overrides.
|
||||
- Documentation: App UI explicitly recommends using `https://` when TLS is configured; the quick-connect form defaults to `http://` only for LAN/Tailscale where the user has a trusted network path.
|
||||
- Tailscale: Most users access local servers over Tailscale (encrypted mesh VPN), making cleartext HTTP safe in practice.
|
||||
|
||||
**Residual risk:** Users who expose their opencode server on a public IP via HTTP (not HTTPS) without Tailscale have no transport-layer protection. This is documented as user responsibility.
|
||||
|
||||
---
|
||||
|
||||
### 2. Malicious deep link (`opencode://`)
|
||||
|
||||
**Capability:** Any installed app or web page can fire `opencode://` deep links at the app.
|
||||
|
||||
**Attack scenarios:**
|
||||
- Inject a malicious server URL via deep link to phish the user into connecting to an attacker-controlled opencode server.
|
||||
- Cause the app to navigate to an attacker-controlled session ID.
|
||||
|
||||
**Defenses:**
|
||||
- Current deep link handling uses `expo-router`. The only parameterized routes are `/session/[id]` and `/connection/[id]`. Neither auto-connects to an external URL via deep link; server URLs are only stored via the explicit "Add Connection" UI.
|
||||
- No deep link handler was found that auto-creates a connection from URL parameters.
|
||||
- Notifications navigation (`router.push('/session/${data.sessionId}')`) uses server-assigned session IDs only, not user-supplied URLs.
|
||||
|
||||
**Residual risk:** Low. No auto-connect-from-deep-link path exists. If this is added in future, validate scheme+host against stored connections before navigating.
|
||||
|
||||
---
|
||||
|
||||
### 3. Lost or stolen device
|
||||
|
||||
**Capability:** Physical access to a locked device.
|
||||
|
||||
**Attack scenarios:**
|
||||
- Read server URL and password from device storage.
|
||||
- View AI session content / conversation history from lock screen notifications.
|
||||
- Bypass app authentication.
|
||||
|
||||
**Defenses:**
|
||||
- All sensitive data in `expo-secure-store` (iOS Keychain with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`; Android Keystore-backed EncryptedSharedPreferences). Data is inaccessible when the device is locked.
|
||||
- Optional biometric app lock (`requireBiometric` setting). Default off for UX; users with sensitive servers should enable it.
|
||||
- Notification lock-screen preview: session titles may appear in "Task completed" notifications. No prompt content or AI response content is included in notification bodies.
|
||||
- **Known gap (M-03):** `permission.asked` notifications can show tool file-path glob patterns on lock screen. Fix pending.
|
||||
|
||||
**Residual risk:** Medium. Without biometric lock enabled, a device that auto-unlocks (e.g., trusted location) exposes app contents. Users should be prompted to enable biometric lock during onboarding when a server is added.
|
||||
|
||||
---
|
||||
|
||||
### 4. Compromised opencode server
|
||||
|
||||
**Capability:** Attacker controls the opencode server the app connects to (e.g., via supply-chain, server compromise, or DNS hijacking to an HTTPS server).
|
||||
|
||||
**Attack scenarios:**
|
||||
- Server returns malicious tool permission requests designed to trick user into approving harmful file operations.
|
||||
- Server returns crafted SSE events to cause client-side errors and exfiltrate stack traces.
|
||||
|
||||
**Defenses:**
|
||||
- App renders server data (session titles, tool permission descriptions) as text only — no `eval()`, no `dangerouslySetInnerHTML`, no JavaScript injection surface.
|
||||
- Sentry reports are scrubbed of URLs and query params before upload.
|
||||
- Tool permission requests require explicit in-app user approval. The `permission.asked` UI shows the user what patterns the tool wants to access.
|
||||
|
||||
**Residual risk:** Medium. The app trusts the connected server by design. Users are responsible for connecting only to servers they control. This is documented in the UX.
|
||||
|
||||
---
|
||||
|
||||
### 5. Sentry DSN abuse
|
||||
|
||||
**Capability:** Attacker extracts the DSN from the APK/IPA (trivially readable via `strings`).
|
||||
|
||||
**Attack scenarios:**
|
||||
- Flood the Sentry project with fake events, exhausting quota and masking real errors.
|
||||
- Enumerate org/project metadata via the public DSN.
|
||||
|
||||
**Defenses:**
|
||||
- The Sentry DSN is `EXPO_PUBLIC_` and therefore intentionally public (this is Sentry's documented model).
|
||||
- `sendDefaultPii: false` ensures device identifiers are not sent.
|
||||
- Sentry ingest rate-limiting can be configured per-project (deferred enhancement).
|
||||
|
||||
**Residual risk:** Low. Quota exhaustion is a nuisance, not a security breach. No user data flows from DSN exposure.
|
||||
|
||||
---
|
||||
|
||||
### 6. CI/CD supply chain attack
|
||||
|
||||
**Capability:** Attacker compromises a third-party GitHub Action used in the build pipeline.
|
||||
|
||||
**Attack scenarios:**
|
||||
- Malicious action exfiltrates `PLAY_STORE_SERVICE_ACCOUNT_JSON` or keystore secrets.
|
||||
- Tampered action injects malicious code into the APK.
|
||||
|
||||
**Defenses:**
|
||||
- Secrets are stored as GitHub Secrets, not in code.
|
||||
- `pull_request` trigger (not `pull_request_target`) prevents fork PRs from accessing secrets.
|
||||
- `KEYSTORE_PASSWORD`, `KEY_ALIAS`, `KEY_PASSWORD` are separate from `KEYSTORE_BASE64`, minimizing damage from any single secret leak.
|
||||
|
||||
**Residual risk:** Medium. Third-party actions are pinned to tags, not commit SHAs (see H audit finding M-02). SHA pinning is the recommended fix.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Security of the user's opencode server itself.** The server is user-managed; its authentication, file access controls, and model provider secrets are outside this app's threat model.
|
||||
- **Model provider security.** API keys for Anthropic, OpenAI, etc. are held by the opencode server, not the mobile app. The app never sees model provider credentials.
|
||||
- **Tailscale or VPN security.** Users connecting via Tailscale rely on Tailscale's security model for transport protection.
|
||||
- **Device operating system integrity.** Rooted/jailbroken devices can bypass `expo-secure-store`. This is a platform-level threat outside app scope.
|
||||
|
||||
---
|
||||
|
||||
## Security architecture summary
|
||||
|
||||
```
|
||||
[Phone] ──(HTTPS or HTTP-via-Tailscale)──> [opencode server (user-owned)]
|
||||
│ │
|
||||
│ expo-secure-store (Keychain/Keystore) │── AI model provider (user's keys)
|
||||
│ • server URL + auth password │
|
||||
│ • biometric settings │
|
||||
│ • telemetry consent │
|
||||
│ │
|
||||
│ opt-in only │
|
||||
└──(HTTPS)──> [Sentry ingest] (scrubbed, no PII, no prompts)
|
||||
```
|
||||
Reference in New Issue
Block a user