From 39cf3f2a74dcea39453b69f3553e514187e78d4d Mon Sep 17 00:00:00 2001 From: Trey T Date: Mon, 1 Jun 2026 18:40:21 -0500 Subject: [PATCH] Mimic iOS Feeld app on auth + version bump to 9.4.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- web/server/index.js | 45 +++++++++++++++++++++++++++++-------- web/src/config/constants.ts | 14 +++++++++--- web/vite.config.ts | 38 ++++++++++++++++++++++++++----- 3 files changed, 79 insertions(+), 18 deletions(-) diff --git a/web/server/index.js b/web/server/index.js index f5b0077..891fdf8 100755 --- a/web/server/index.js +++ b/web/server/index.js @@ -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; } diff --git a/web/src/config/constants.ts b/web/src/config/constants.ts index e20b13a..854c39c 100755 --- a/web/src/config/constants.ts +++ b/web/src/config/constants.ts @@ -1,6 +1,13 @@ -// ⚠️ When updating, also update vite.config.ts proxy headers (search for APP_VERSION) -export const APP_VERSION = '8.11.0'; +// ⚠️ When updating, also update vite.config.ts proxy headers and server/index.js APP_VERSION +export const APP_VERSION = '9.4.3'; export const OS_VERSION = '26.2.1'; +// iOS app identifiers captured from a real iPhone — used by the proxy + server to mimic the iOS app. +export const IOS_BUNDLE_ID = 'com.3nder.threender'; +export const IOS_DEVICE_MODEL = 'iPhone14_4'; +export const FIREBASE_SDK_VERSION = '11.5.0'; +export const FIREBASE_GMP_ID = '1:594152761603:ios:f52cf15efff827861b2136'; +// Sent by the iOS app when on-device AppCheck fails — Firebase still accepts this fallback. +export const FIREBASE_APPCHECK_FALLBACK = 'eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ=='; export const API_CONFIG = { // Use Vite proxy to bypass CORS @@ -16,7 +23,8 @@ export const REQUEST_HEADERS = { 'x-device-os': 'ios', 'x-app-version': APP_VERSION, 'x-os-version': OS_VERSION, - 'User-Agent': 'feeld-mobile', + // Realistic iOS Alamofire UA — matches what the real Feeld app sends. + 'User-Agent': `Feeld/${APP_VERSION} (${IOS_BUNDLE_ID}; build:1; iOS ${OS_VERSION}) Alamofire/5.9.1`, 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.9', 'Accept-Encoding': 'gzip, deflate, br', diff --git a/web/vite.config.ts b/web/vite.config.ts index f56a334..4c9e8eb 100755 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -37,10 +37,14 @@ export default defineConfig({ // Remove browser headers that reveal this is a web client proxyReq.removeHeader('origin'); proxyReq.removeHeader('referer'); - // Ensure mobile app headers are preserved - if (!proxyReq.getHeader('user-agent')?.includes('feeld')) { - proxyReq.setHeader('User-Agent', 'feeld-mobile'); - } + proxyReq.removeHeader('sec-fetch-dest'); + proxyReq.removeHeader('sec-fetch-mode'); + proxyReq.removeHeader('sec-fetch-site'); + proxyReq.removeHeader('sec-ch-ua'); + proxyReq.removeHeader('sec-ch-ua-mobile'); + proxyReq.removeHeader('sec-ch-ua-platform'); + // Match the real iOS app's Alamofire UA. Keep APP_VERSION in sync with constants.ts. + proxyReq.setHeader('User-Agent', 'Feeld/9.4.3 (com.3nder.threender; build:1; iOS 26.2.1) Alamofire/5.9.1'); }); }, }, @@ -49,6 +53,28 @@ export default defineConfig({ changeOrigin: true, rewrite: (path) => path.replace(/^\/api\/firebase/, ''), secure: false, + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq) => { + // Strip browser fingerprint headers + proxyReq.removeHeader('origin'); + proxyReq.removeHeader('referer'); + proxyReq.removeHeader('sec-fetch-dest'); + proxyReq.removeHeader('sec-fetch-mode'); + proxyReq.removeHeader('sec-fetch-site'); + proxyReq.removeHeader('sec-ch-ua'); + proxyReq.removeHeader('sec-ch-ua-mobile'); + proxyReq.removeHeader('sec-ch-ua-platform'); + // Mirror the headers a real iOS Feeld app sends on token refresh. + proxyReq.setHeader('User-Agent', 'FirebaseAuth.iOS/11.5.0 com.3nder.threender/9.4.3 iPhone/26.2.1 hw/iPhone14_4'); + proxyReq.setHeader('Accept', '*/*'); + proxyReq.setHeader('Accept-Language', 'en'); + proxyReq.setHeader('Accept-Encoding', 'gzip, deflate, br'); + proxyReq.setHeader('X-Client-Version', 'iOS/FirebaseSDK/11.5.0/FirebaseCore-iOS'); + proxyReq.setHeader('X-Firebase-AppCheck', 'eyJlcnJvciI6IlVOS05PV05fRVJST1IifQ=='); + proxyReq.setHeader('X-Firebase-GMPID', '1:594152761603:ios:f52cf15efff827861b2136'); + proxyReq.setHeader('X-Ios-Bundle-Identifier', 'com.3nder.threender'); + }); + }, }, '/api/images': { target: 'https://res.cloudinary.com', @@ -84,10 +110,10 @@ export default defineConfig({ proxyReq.removeHeader('sec-ch-ua-platform'); // Set mobile app headers to match iOS app // APP_VERSION: keep in sync with src/config/constants.ts APP_VERSION - proxyReq.setHeader('User-Agent', 'Feeld/8.11.0 (com.3nder.ios; build:1; iOS 26.2.1) Alamofire/5.9.1'); + proxyReq.setHeader('User-Agent', 'Feeld/9.4.3 (com.3nder.threender; build:1; iOS 26.2.1) Alamofire/5.9.1'); proxyReq.setHeader('Accept', '*/*'); proxyReq.setHeader('Accept-Language', 'en-US,en;q=0.9'); - proxyReq.setHeader('x-app-version', '8.11.0'); + proxyReq.setHeader('x-app-version', '9.4.3'); proxyReq.setHeader('x-device-os', 'ios'); proxyReq.setHeader('x-os-version', '26.2.1'); });