Add React Native mobile app (Expo SDK 55) - #64
Conversation
Introduces the mobile client (`apps/mobile/`) as the 4th surface for Envpilot. Uses Expo Router, NativeWind, Convex direct subscriptions, and WorkOS OAuth with PKCE for authentication. Adds server-side mobile auth endpoints and mobileTokens Convex table. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Version Tracker
No version bumps detected in this PR. |
Add alerts tab, project settings, variable sharing/creation screens, and new reusable UI components (Avatar, ChipMono, Icon, MonoCard, Sparkline). Overhaul theme system with expanded color palette, spacing scale, and typography tokens. Redesign all existing screens with richer layouts and improved UX. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rafay99-epic
left a comment
There was a problem hiding this comment.
🔍 Code Review — Scarlet Speedster
Verdict: Comment (2 warnings, 4 suggestions, several looks-good)
Reviewed the security-critical files: auth flow (PKCE), token management, server-side endpoints, and vault access. The non-security skill docs (.agents/, .claude/) were skimmed — they're reference material, not runtime code.
⚠️ Warnings
1. codeVerifier is optional in the callback schema — weakens PKCE
apps/web/src/app/api/mobile/auth/route.ts:17
codeVerifier: z.string().optional(), // ← should be requiredPKCE exists to protect against authorization code interception. Making the verifier optional means a client can skip it, bypassing the entire PKCE protection. The mobile client always sends it (exchangeCodeForTokens in auth.ts), so make it required:
codeVerifier: z.string().min(1),2. revokeToken() silently swallows errors — token lives on server after "sign out"
apps/mobile/src/api/auth.ts:147
await fetch(`${API_URL}/api/mobile/auth?action=revoke`, { ... }).catch(() => {});If the revoke request fails (network error, server down, 500), the error is silently caught and local storage is cleared anyway. The server still considers the token valid. A compromised token remains usable until it expires. At minimum, log the error. Ideally, retry or warn the user that sign-out may not have fully completed.
💡 Suggestions
3. Unknown action falls through to callback
apps/web/src/app/api/mobile/auth/route.ts:41
if (action === "callback") return handleCallback(request);
if (action === "refresh") return handleRefresh(request);
if (action === "revoke") return handleRevoke(request);
return handleCallback(request); // ← unknown action silently runs callbackIf someone sends ?action=delete or any garbage, it runs the callback handler. Return 400 for unknown actions:
return NextResponse.json({ error: "Unknown action" }, { status: 400 });4. refreshAccessToken() doesn't clear invalid tokens on failure
apps/mobile/src/api/auth.ts:124
When refresh fails (!response.ok), the function returns null but leaves the invalid refreshToken in SecureStore. The user gets stuck — the app thinks it has a token but can't use it. Clear tokens on refresh failure:
if (!response.ok) {
await storage.clearAll();
return null;
}5. clearAll() doesn't clear the code verifier
apps/mobile/src/lib/secure-storage.ts:83
The KEYS constant doesn't include "envpilot_code_verifier", so clearAll() (which iterates KEYS) leaves the verifier behind. If a user signs out and signs back in, a stale verifier could interfere. Add it to the cleanup:
await Promise.all([
...keysToDelete,
SecureStore.deleteItemAsync("envpilot_code_verifier"),
]);6. No token expiry handling on the client
The server returns expiresAt in the auth response, but auth.store.ts and auth.ts don't store or use it. The client has no way to proactively refresh before expiry — it only refreshes reactively after a 401. Consider storing expiresAt and refreshing proactively (e.g., when 80% of lifetime has elapsed).
✅ Looks Good
- PKCE implementation is correct — code verifier/challenge generation using
expo-crypto, S256 method, proper base64url encoding. Good. - SecureStore usage is proper — tokens in Keychain/Keystore, not AsyncStorage. This is the right approach.
- Server-side Zod validation — all request bodies are schema-validated before processing. Nice.
- Bearer token format check —
mob_prefix validation inauthenticateMobileRequestis a good first line of defense. - Structured logging with
tokenPrefix()— you're not logging full tokens, just prefixes. Excellent security practice. - Error handling on server — proper try/catch, rate-limit detection, no stack traces leaked to client.
- Convex token lifecycle — create/refresh/revoke/validate mutations are well-structured.
- Fire-and-forget
updateLastUsed— non-critical update doesn't block the response. Good pattern.
Reviewed by Scarlet Speedster 🔴💨 — automated code review system
|
Do not merge this PR. This PR is very much outdated and this branch is very much outdated. The functionality and the mobile UI can be utilized into the further pages, but for now I am keeping this PR as a draft. |
Summary
apps/mobile/— the 4th client surface for Envpilot, built with Expo SDK 55 (React 19, RN 0.83), Expo Router, NativeWind, and direct Convex subscriptionsexpo-auth-sessionand a callback deep link (envpilot://callback)POST /api/mobile/auth?action=callback|refresh|revoke) andmobileTokensConvex table for token lifecycle/api/mobile/(.*)to unauthenticated proxy paths, mobile vault endpoint, and root-leveldev:mobile/build:mobilescriptsWhat's included
WebBrowser.openBrowserAsync+ deep link callback, code-for-token exchange, refresh, revokeapps/web/src/app/api/mobile/auth/route.ts(3 actions),apps/web/src/lib/mobile-auth.ts(token validation),convex/mobileTokens.ts(CRUD + indexes)mobileTokenstable withby_access_token,by_refresh_token,by_userindexesdev:mobile,build:mobile:android,build:mobile:iosscripts, .env.local symlink in setupTest plan
cd apps/mobile && npx expo start --clear— app launches on Android/iOSmobileTokensentry createdbun run typecheckpasses across monorepo🤖 Generated with Claude Code