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:
Dennis V
2026-05-26 01:26:35 +00:00
parent 2dd49117af
commit b8fb390f5c
21 changed files with 1322 additions and 11 deletions

159
docs/applestore.md Normal file
View 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
View 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
View 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)

View 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
View 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
View 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
View 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)
```