From 559c7bfdef62c1f06b6f05195384092a7d73b7bd Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 04:52:21 +0100 Subject: [PATCH 1/4] fix: implement smart local session priority to prevent working local credentials from being overwritten by server data --- frontend/electron/main.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 09c1cb8..1622459 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -286,15 +286,31 @@ const syncAccounts = async (isManual = false) => { } else { const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0); const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0); - if (sDate > lDate) { + // 1. SENSITIVE DATA SYNC (Credentials) + const sSessionDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0); + const lSessionDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0); + + const isLocalAccount = !exists._id.startsWith('shared_'); + const isLocalSessionHealthy = exists.steamLoginSecure && !exists.authError; + + // SMART OVERWRITE LOGIC: + // - If it's a remote shared account: Newest wins. + // - If it's a LOCAL account: Only overwrite if our local session is broken/missing. + const shouldOverwriteCredentials = !isLocalAccount ? (sSessionDate > lSessionDate) : (!isLocalSessionHealthy && sSessionDate > lSessionDate); + + if (shouldOverwriteCredentials) { if (s.loginName) exists.loginName = s.loginName; if (s.loginConfig) exists.loginConfig = s.loginConfig; - if (s.steamLoginSecure) { exists.steamLoginSecure = s.steamLoginSecure; exists.autoCheckCooldown = true; exists.authError = false; } + if (s.steamLoginSecure) { + exists.steamLoginSecure = s.steamLoginSecure; + exists.autoCheckCooldown = true; + exists.authError = false; + } exists.sessionUpdatedAt = s.sessionUpdatedAt; hasChanges = true; } - // Metadata Sync (Pull) + // 2. Metadata Sync (Pull) - Always "Newest Wins" const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0); const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0); if (sMetaDate > lMetaDate) { From 2ef8dd06e7121c3f9e13b3e58380d9503647b71d Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 04:54:36 +0100 Subject: [PATCH 2/4] fix: implement granular session health detection with SteamAuthError and smart conditional replacement logic --- frontend/electron/main.ts | 9 +++++-- frontend/electron/services/scraper.ts | 39 +++++++++++++++------------ 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 1622459..867701e 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -7,7 +7,7 @@ import axios from 'axios'; import fs from 'fs'; import { pathToFileURL } from 'url'; import { fetchProfileData, scrapeBanStatus } from './services/steam-web'; -import { scrapeCooldown } from './services/scraper'; +import { scrapeCooldown, SteamAuthError } from './services/scraper'; import { steamClient, LocalSteamAccount } from './services/steam-client'; import { BackendService } from './services/backend'; @@ -247,7 +247,12 @@ const scrapeAccountData = async (account: Account) => { if (backend) await backend.pushCooldown(account.steamId, undefined, now.toISOString()); } } catch (e: any) { - if (e.message.includes('cookie') || e.message.includes('Sign In')) account.authError = true; + if (e instanceof SteamAuthError) { + account.authError = true; + } else { + console.error(`[Scraper] Temporary error for ${account.personaName}: ${e.message}`); + } + } } } if (backend && !account._id.startsWith('shared_')) { diff --git a/frontend/electron/services/scraper.ts b/frontend/electron/services/scraper.ts index e470573..58bd2e2 100644 --- a/frontend/electron/services/scraper.ts +++ b/frontend/electron/services/scraper.ts @@ -6,6 +6,14 @@ export interface CooldownData { expiresAt?: Date; } +// Custom error to identify session death +export class SteamAuthError extends Error { + constructor(message: string) { + super(message); + this.name = "SteamAuthError"; + } +} + export const scrapeCooldown = async (steamId: string, steamLoginSecure: string): Promise => { const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`; @@ -15,16 +23,21 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string): 'Cookie': steamLoginSecure, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }, - timeout: 10000 + timeout: 10000, + validateStatus: (status) => status < 500 // Allow redirects to handle them manually }); + // If Steam redirects us to the login page, the cookie is dead + if (response.data.includes('Sign In') || response.request.path.includes('/login')) { + throw new SteamAuthError('Invalid or expired steamLoginSecure cookie'); + } + const $ = cheerio.load(response.data); - if (response.data.includes('Sign In') || !response.data.includes('Personal Game Data')) { - throw new Error('Invalid or expired steamLoginSecure cookie'); + if (!response.data.includes('Personal Game Data')) { + throw new SteamAuthError('Session invalid: Personal Game Data not accessible'); } - // 1. Locate the specific table containing cooldown info let expirationDate: Date | undefined = undefined; $('table').each((_, table) => { @@ -36,15 +49,10 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string): rows.each((_, row) => { const dateText = $(row).find('td').eq(expirationIndex).text().trim(); if (dateText && dateText !== '') { - // Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC' const cleanDateText = dateText.replace(' GMT', ' UTC'); const parsed = new Date(cleanDateText); - if (!isNaN(parsed.getTime())) { - // We want the newest expiration date found - if (!expirationDate || parsed > (expirationDate as Date)) { - expirationDate = parsed; - } + if (!expirationDate || parsed > (expirationDate as Date)) expirationDate = parsed; } } }); @@ -52,11 +60,7 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string): }); if (expirationDate && (expirationDate as Date).getTime() > Date.now()) { - console.log(`[Scraper] Found active cooldown until: ${(expirationDate as Date).toISOString()}`); - return { - isActive: true, - expiresAt: expirationDate - }; + return { isActive: true, expiresAt: expirationDate }; } const content = $('#personal_game_data_content').text(); @@ -66,7 +70,8 @@ export const scrapeCooldown = async (steamId: string, steamLoginSecure: string): return { isActive: false }; } catch (error: any) { - console.error(`[Scraper] Error for ${steamId}:`, error.message); - throw error; + if (error instanceof SteamAuthError) throw error; + console.error(`[Scraper] Network/Internal Error for ${steamId}:`, error.message); + throw error; // Generic errors don't trigger re-auth } }; From d6d87107f5cc563c40f8faa0a25074e1b8189114 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 04:56:30 +0100 Subject: [PATCH 3/4] fix: resolve syntax error by removing extra closing brace in main.ts --- frontend/electron/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 867701e..d744acc 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -253,7 +253,6 @@ const scrapeAccountData = async (account: Account) => { console.error(`[Scraper] Temporary error for ${account.personaName}: ${e.message}`); } } - } } if (backend && !account._id.startsWith('shared_')) { await backend.shareAccount(account); From 1d4fb031046b170f6ea00399957eb60c3518c8d5 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 04:57:14 +0100 Subject: [PATCH 4/4] chore: bump version to 1.3.3 --- frontend/dist-electron/main.js | 19 +++++- frontend/dist-electron/services/scraper.js | 41 +++++++----- .../dist-electron/services/steam-client.js | 65 +++++++------------ frontend/package-lock.json | 4 +- frontend/package.json | 2 +- 5 files changed, 66 insertions(+), 65 deletions(-) diff --git a/frontend/dist-electron/main.js b/frontend/dist-electron/main.js index 2217aba..a76aa33 100644 --- a/frontend/dist-electron/main.js +++ b/frontend/dist-electron/main.js @@ -219,8 +219,12 @@ const scrapeAccountData = async (account) => { } } catch (e) { - if (e.message.includes('cookie') || e.message.includes('Sign In')) + if (e instanceof scraper_1.SteamAuthError) { account.authError = true; + } + else { + console.error(`[Scraper] Temporary error for ${account.personaName}: ${e.message}`); + } } } if (backend && !account._id.startsWith('shared_')) { @@ -259,7 +263,16 @@ const syncAccounts = async (isManual = false) => { else { const sDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0); const lDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0); - if (sDate > lDate) { + // 1. SENSITIVE DATA SYNC (Credentials) + const sSessionDate = s.sessionUpdatedAt ? new Date(s.sessionUpdatedAt) : new Date(0); + const lSessionDate = exists.sessionUpdatedAt ? new Date(exists.sessionUpdatedAt) : new Date(0); + const isLocalAccount = !exists._id.startsWith('shared_'); + const isLocalSessionHealthy = exists.steamLoginSecure && !exists.authError; + // SMART OVERWRITE LOGIC: + // - If it's a remote shared account: Newest wins. + // - If it's a LOCAL account: Only overwrite if our local session is broken/missing. + const shouldOverwriteCredentials = !isLocalAccount ? (sSessionDate > lSessionDate) : (!isLocalSessionHealthy && sSessionDate > lSessionDate); + if (shouldOverwriteCredentials) { if (s.loginName) exists.loginName = s.loginName; if (s.loginConfig) @@ -272,7 +285,7 @@ const syncAccounts = async (isManual = false) => { exists.sessionUpdatedAt = s.sessionUpdatedAt; hasChanges = true; } - // Metadata Sync (Pull) + // 2. Metadata Sync (Pull) - Always "Newest Wins" const sMetaDate = s.lastMetadataCheck ? new Date(s.lastMetadataCheck) : new Date(0); const lMetaDate = exists.lastBanCheck ? new Date(exists.lastBanCheck) : new Date(0); if (sMetaDate > lMetaDate) { diff --git a/frontend/dist-electron/services/scraper.js b/frontend/dist-electron/services/scraper.js index ec3db93..fe60708 100644 --- a/frontend/dist-electron/services/scraper.js +++ b/frontend/dist-electron/services/scraper.js @@ -36,9 +36,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.scrapeCooldown = void 0; +exports.scrapeCooldown = exports.SteamAuthError = void 0; const axios_1 = __importDefault(require("axios")); const cheerio = __importStar(require("cheerio")); +// Custom error to identify session death +class SteamAuthError extends Error { + constructor(message) { + super(message); + this.name = "SteamAuthError"; + } +} +exports.SteamAuthError = SteamAuthError; const scrapeCooldown = async (steamId, steamLoginSecure) => { const url = `https://steamcommunity.com/profiles/${steamId}/gcpd/730?tab=matchmaking`; try { @@ -47,13 +55,17 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => { 'Cookie': steamLoginSecure, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }, - timeout: 10000 + timeout: 10000, + validateStatus: (status) => status < 500 // Allow redirects to handle them manually }); - const $ = cheerio.load(response.data); - if (response.data.includes('Sign In') || !response.data.includes('Personal Game Data')) { - throw new Error('Invalid or expired steamLoginSecure cookie'); + // If Steam redirects us to the login page, the cookie is dead + if (response.data.includes('Sign In') || response.request.path.includes('/login')) { + throw new SteamAuthError('Invalid or expired steamLoginSecure cookie'); + } + const $ = cheerio.load(response.data); + if (!response.data.includes('Personal Game Data')) { + throw new SteamAuthError('Session invalid: Personal Game Data not accessible'); } - // 1. Locate the specific table containing cooldown info let expirationDate = undefined; $('table').each((_, table) => { const headers = $(table).find('th').map((_, th) => $(th).text().trim()).get(); @@ -63,25 +75,18 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => { rows.each((_, row) => { const dateText = $(row).find('td').eq(expirationIndex).text().trim(); if (dateText && dateText !== '') { - // Steam uses 'GMT' which some JS engines don't parse well, replace with 'UTC' const cleanDateText = dateText.replace(' GMT', ' UTC'); const parsed = new Date(cleanDateText); if (!isNaN(parsed.getTime())) { - // We want the newest expiration date found - if (!expirationDate || parsed > expirationDate) { + if (!expirationDate || parsed > expirationDate) expirationDate = parsed; - } } } }); } }); if (expirationDate && expirationDate.getTime() > Date.now()) { - console.log(`[Scraper] Found active cooldown until: ${expirationDate.toISOString()}`); - return { - isActive: true, - expiresAt: expirationDate - }; + return { isActive: true, expiresAt: expirationDate }; } const content = $('#personal_game_data_content').text(); if (content.includes('Competitive Cooldown') || content.includes('Your account is currently')) { @@ -90,8 +95,10 @@ const scrapeCooldown = async (steamId, steamLoginSecure) => { return { isActive: false }; } catch (error) { - console.error(`[Scraper] Error for ${steamId}:`, error.message); - throw error; + if (error instanceof SteamAuthError) + throw error; + console.error(`[Scraper] Network/Internal Error for ${steamId}:`, error.message); + throw error; // Generic errors don't trigger re-auth } }; exports.scrapeCooldown = scrapeCooldown; diff --git a/frontend/dist-electron/services/steam-client.js b/frontend/dist-electron/services/steam-client.js index a1f55e1..b25aa94 100644 --- a/frontend/dist-electron/services/steam-client.js +++ b/frontend/dist-electron/services/steam-client.js @@ -49,10 +49,6 @@ class SteamClientService { return null; return path_1.default.join(this.steamPath, 'config', 'config.vdf'); } - /** - * Safe Atomic Write: Writes to a temp file and renames it. - * This prevents file corruption if the app crashes during write. - */ safeWriteVdf(filePath, data) { const tempPath = `${filePath}.tmp_${Date.now()}`; const dir = path_1.default.dirname(filePath); @@ -61,7 +57,6 @@ class SteamClientService { fs_1.default.mkdirSync(dir, { recursive: true }); const vdfContent = (0, simple_vdf_1.stringify)(data); fs_1.default.writeFileSync(tempPath, vdfContent, 'utf-8'); - // Atomic rename fs_1.default.renameSync(tempPath, filePath); } catch (e) { @@ -77,7 +72,6 @@ class SteamClientService { if (loginUsersPath && fs_1.default.existsSync(loginUsersPath)) { this.readLocalAccounts(); chokidar_1.default.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => { - console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`); this.readLocalAccounts(); }); } @@ -89,7 +83,7 @@ class SteamClientService { try { const content = fs_1.default.readFileSync(filePath, 'utf-8'); if (!content.trim()) - return; // Empty file + return; const data = (0, simple_vdf_1.parse)(content); if (!data || !data.users) return; @@ -99,8 +93,7 @@ class SteamClientService { if (!user || !user.AccountName) continue; accounts.push({ - steamId: steamId64, - accountName: user.AccountName, + steamId: steamId64, accountName: user.AccountName, personaName: user.PersonaName || user.AccountName, timestamp: parseInt(user.Timestamp) || 0 }); @@ -108,9 +101,7 @@ class SteamClientService { if (this.onAccountsChanged) this.onAccountsChanged(accounts); } - catch (error) { - console.error('[SteamClient] Error parsing loginusers.vdf:', error); - } + catch (error) { } } extractAccountConfig(accountName) { const configPath = this.getConfigVdfPath(); @@ -123,7 +114,6 @@ class SteamClientService { return (accounts && accounts[accountName]) ? accounts[accountName] : null; } catch (e) { - console.error('[SteamClient] Failed to extract config.vdf data'); return null; } } @@ -131,17 +121,7 @@ class SteamClientService { const configPath = this.getConfigVdfPath(); if (!configPath) return; - let data = { - InstallConfigStore: { - Software: { - Valve: { - Steam: { - Accounts: {} - } - } - } - } - }; + let data = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } }; if (fs_1.default.existsSync(configPath)) { try { const content = fs_1.default.readFileSync(configPath, 'utf-8'); @@ -151,18 +131,23 @@ class SteamClientService { } catch (e) { } } - // Ensure safe nesting - if (!data.InstallConfigStore) - data.InstallConfigStore = {}; - if (!data.InstallConfigStore.Software) - data.InstallConfigStore.Software = {}; - if (!data.InstallConfigStore.Software.Valve) - data.InstallConfigStore.Software.Valve = {}; - if (!data.InstallConfigStore.Software.Valve.Steam) - data.InstallConfigStore.Software.Valve.Steam = {}; - if (!data.InstallConfigStore.Software.Valve.Steam.Accounts) - data.InstallConfigStore.Software.Valve.Steam.Accounts = {}; - data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData; + const ensurePath = (obj, keys) => { + let curr = obj; + for (const key of keys) { + if (!curr[key] || typeof curr[key] !== 'object') + curr[key] = {}; + curr = curr[key]; + } + return curr; + }; + const steamAccounts = ensurePath(data, ['InstallConfigStore', 'Software', 'Valve', 'Steam', 'Accounts']); + // FAILPROOF: Force crucial flags that Steam uses to decide session validity + steamAccounts[accountName] = { + ...accountData, + RememberPassword: "1", + AllowAutoLogin: "1", + Timestamp: Math.floor(Date.now() / 1000).toString() + }; try { this.safeWriteVdf(configPath, data); console.log(`[SteamClient] Safely injected session for ${accountName}`); @@ -220,6 +205,7 @@ class SteamClientService { } catch (e) { } } + // Injection of the actual authentication blob if (accountConfig && accountName) { this.injectAccountConfig(accountName, accountConfig); } @@ -232,11 +218,7 @@ class SteamClientService { for (const regPath of regLocations) { if (!fs_1.default.existsSync(path_1.default.dirname(regPath))) continue; - let regData = { Registry: { HKCU: { Software: { Valve: { Steam: { - AutoLoginUser: "", - RememberPassword: "1", - AlreadyLoggedIn: "1" - } } } } } }; + let regData = { Registry: { HKCU: { Software: { Valve: { Steam: { AutoLoginUser: "", RememberPassword: "1", AlreadyLoggedIn: "1" } } } } } }; if (fs_1.default.existsSync(regPath)) { try { const content = fs_1.default.readFileSync(regPath, 'utf-8'); @@ -246,7 +228,6 @@ class SteamClientService { } catch (e) { } } - // Deep merge helper const ensurePath = (obj, keys) => { let curr = obj; for (const key of keys) { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 985ed93..3b9028d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ultimate-ban-tracker-desktop", - "version": "1.3.2", + "version": "1.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ultimate-ban-tracker-desktop", - "version": "1.3.2", + "version": "1.3.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@emotion/react": "^11.14.0", diff --git a/frontend/package.json b/frontend/package.json index dd33e3b..ee46848 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "ultimate-ban-tracker-desktop", "description": "Professional Steam Account Manager & Ban Tracker", - "version": "1.3.2", + "version": "1.3.3", "author": "Nils Pukropp ", "homepage": "https://narl.io", "license": "SEE LICENSE IN LICENSE",