Mimic iOS Feeld app on auth + version bump to 9.4.3

Captured a real iOS Feeld token-refresh request — our outbound headers were
unmistakably "not the iOS app." Aligning so requests fingerprint identically.

- APP_VERSION 8.11.0 → 9.4.3 in constants.ts, server/index.js, vite.config.ts
- Bundle id corrected to com.3nder.threender (was com.3nder.ios)
- REQUEST_HEADERS User-Agent now the realistic Alamofire iOS UA, not 'feeld-mobile'
- server/index.js refreshAccessToken now sends the full Firebase iOS header
  set (FirebaseAuth.iOS UA, X-Client-Version, X-Firebase-AppCheck fallback,
  X-Firebase-GMPID, X-Ios-Bundle-Identifier) and uses camelCase body keys.
  Response parsing accepts both camelCase and snake_case for resilience.
- vite proxy /api/firebase now applies the same iOS headers in dev mode
- vite proxy /api/graphql strips browser sec-* fingerprint headers and sets
  the realistic Alamofire UA unconditionally (was a conditional 'feeld-mobile')

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-06-01 18:40:21 -05:00
parent da2bab21e5
commit 39cf3f2a74
3 changed files with 79 additions and 18 deletions
+36 -9
View File
@@ -1255,8 +1255,14 @@ app.put('/api/okcupid/token', (req, res) => {
const FIREBASE_API_KEY = 'AIzaSyD9o9mzulN50-hqOwF6ww9pxUNUxwVOCXA';
const FIREBASE_REFRESH_URL = `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`;
const GRAPHQL_ENDPOINT = 'https://core.api.fldcore.com/graphql';
const APP_VERSION = '8.11.0';
const APP_VERSION = '9.4.3';
const OS_VERSION = '26.2.1';
// iOS app identifiers (mirror of src/config/constants.ts)
const IOS_BUNDLE_ID = 'com.3nder.threender';
const IOS_DEVICE_MODEL = 'iPhone14_4';
const FIREBASE_SDK_VERSION = '11.5.0';
const FIREBASE_GMP_ID = '1:594152761603:ios:f52cf15efff827861b2136';
const FIREBASE_APPCHECK_FALLBACK = 'eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ==';
const AUTH_TOKENS_FILE = path.join(DATA_DIR, 'auth-tokens.json');
const ROTATION_STATE_FILE = path.join(DATA_DIR, 'locationRotation.json');
const SAVED_LOCATIONS_FILE = path.join(DATA_DIR, 'savedLocations.json');
@@ -1333,12 +1339,27 @@ class FeeldAPIClient {
throw new Error('No refresh token available — seed from browser first');
}
// Mirror the headers the real iOS Feeld app sends on token refresh —
// anything missing here flags the request as "not the iOS app."
const response = await fetch(FIREBASE_REFRESH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en',
'Connection': 'keep-alive',
'Content-Type': 'application/json',
'Host': 'securetoken.googleapis.com',
'User-Agent': `FirebaseAuth.iOS/${FIREBASE_SDK_VERSION} ${IOS_BUNDLE_ID}/${APP_VERSION} iPhone/${OS_VERSION} hw/${IOS_DEVICE_MODEL}`,
'X-Client-Version': `iOS/FirebaseSDK/${FIREBASE_SDK_VERSION}/FirebaseCore-iOS`,
'X-Firebase-AppCheck': FIREBASE_APPCHECK_FALLBACK,
'X-Firebase-GMPID': FIREBASE_GMP_ID,
'X-Ios-Bundle-Identifier': IOS_BUNDLE_ID,
},
// iOS app uses camelCase keys — Firebase accepts both, but the iOS shape is what we want.
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
grantType: 'refresh_token',
refreshToken: this.refreshToken,
}),
});
@@ -1348,16 +1369,22 @@ class FeeldAPIClient {
}
const data = await response.json();
this.accessToken = data.access_token;
this.expiresAt = Date.now() + parseInt(data.expires_in) * 1000;
// Firebase returns camelCase keys when the request body is camelCase, snake_case when
// it's snake_case — accept either so a future body shape change doesn't silently break.
const accessToken = data.accessToken ?? data.access_token;
const expiresIn = data.expiresIn ?? data.expires_in;
const newRefreshToken = data.refreshToken ?? data.refresh_token;
this.accessToken = accessToken;
this.expiresAt = Date.now() + parseInt(expiresIn) * 1000;
// Update stored refresh token (Firebase rotates them)
if (data.refresh_token && data.refresh_token !== this.refreshToken) {
this.refreshToken = data.refresh_token;
if (newRefreshToken && newRefreshToken !== this.refreshToken) {
this.refreshToken = newRefreshToken;
this.saveCredentials(this.profileId, this.refreshToken, this.analyticsId);
}
console.log('[FeeldAPI] Token refreshed, expires in', data.expires_in, 'seconds');
console.log('[FeeldAPI] Token refreshed, expires in', expiresIn, 'seconds');
return this.accessToken;
}