Remove BuyMode and fix

This commit is contained in:
2025-11-08 18:25:51 +01:00
parent 40dd9b9fd8
commit 6b687a1018
16 changed files with 24 additions and 997 deletions

View File

@@ -88,7 +88,6 @@ For detailed configuration, advanced features, and troubleshooting, visit our co
| **[Getting Started](docs/getting-started.md)** | Detailed installation and first-run guide | | **[Getting Started](docs/getting-started.md)** | Detailed installation and first-run guide |
| **[Configuration](docs/config.md)** | Complete configuration options reference | | **[Configuration](docs/config.md)** | Complete configuration options reference |
| **[Accounts & 2FA](docs/accounts.md)** | Setting up accounts with TOTP authentication | | **[Accounts & 2FA](docs/accounts.md)** | Setting up accounts with TOTP authentication |
| **[Buy Mode](docs/buy-mode.md)** | 💳 Manual purchase monitoring with point tracking |
| **[Dashboard](src/dashboard/README.md)** | 🆕 Local web dashboard for monitoring and control | | **[Dashboard](src/dashboard/README.md)** | 🆕 Local web dashboard for monitoring and control |
| **[External Scheduling](docs/schedule.md)** | Use OS schedulers for automation | | **[External Scheduling](docs/schedule.md)** | Use OS schedulers for automation |
| **[Docker Deployment](docs/docker.md)** | Running in containers | | **[Docker Deployment](docs/docker.md)** | Running in containers |
@@ -127,29 +126,6 @@ Access at `http://localhost:3000` to:
--- ---
## 💳 Buy Mode
Monitor your points in real-time while manually redeeming rewards:
```bash
# Interactive account selection (recommended)
npm run buy
# Or specify account directly
npm run buy your@email.com
npm run buy 1 # By account number
```
**What it does:**
- Opens browser with 2 tabs: monitoring + your browsing tab
- Tracks point changes every ~10 seconds
- Sends alerts when points are spent
- Completely passive - you control the redemption
**[📖 Full Buy Mode Guide](docs/buy-mode.md)**
---
## 🆕 Account Creator ## 🆕 Account Creator
Automatically create new Microsoft accounts with referral link support: Automatically create new Microsoft accounts with referral link support:
@@ -231,8 +207,6 @@ Container includes:
- ✅ Random execution delays (anti-detection) - ✅ Random execution delays (anti-detection)
- ✅ Health checks - ✅ Health checks
**⚠️ Note:** Buy Mode is not available in Docker (requires interactive terminal)
**📖 [Full Docker Guide](docs/docker.md)** **📖 [Full Docker Guide](docs/docker.md)**
--- ---

View File

@@ -265,12 +265,6 @@ This creates diverse, natural-looking search patterns.
See [Query Diversity Engine](config.md#query-diversity-engine). See [Query Diversity Engine](config.md#query-diversity-engine).
### What is "Buy Mode"?
A manual purchase assistant that monitors your points in real-time while you redeem rewards. Not fully automated—you control the redemption.
See [Buy Mode Guide](buy-mode.md).
### Can I get notifications? ### Can I get notifications?
Yes! The script supports: Yes! The script supports:

View File

@@ -1,217 +0,0 @@
# 💳 Buy Mode
**Manually redeem rewards while monitoring points**
---
## 💡 What Is It?
Launches browser and **passively monitors** your points balance while you manually shop/redeem.
**Use case:** Safely redeem gift cards without automation interference.
---
## ⚡ Quick Start
### Option 1: Interactive Selection (Recommended)
```bash
npm run buy
```
### Option 2: Direct Email Selection
```bash
npm run buy your@email.com
```
### Option 3: Numeric Account Index
```bash
npm run buy 1
```
**What happens:**
1. Opens 2 browser tabs:
- **Monitor tab** — Background point tracking (auto-refresh every ~10s)
- **Your tab** — Use this for manual purchases
2. Monitors points passively without clicking
3. Sends alerts when spending detected
4. Session runs for configured duration (default: 45 minutes)
---
## 🎯 Account Selection Methods
### 1. Interactive Selection (Easiest)
Run without arguments to see a menu of available accounts:
```bash
npm run buy
```
You'll see:
```
Available accounts:
────────────────────────────────────────────────────────────
[1] my***t@outlook.com Direct
[2] se***d@outlook.com 🔒 Proxy
[3] th***d@outlook.com Direct
────────────────────────────────────────────────────────────
Enter account number (1-3) or email:
```
**Features:**
- ✅ Masked emails for privacy
- ✅ Proxy status indication
- ✅ Only shows enabled accounts
- ✅ Validates selection before proceeding
### 2. By Email Address
Specify the exact email address:
```bash
npm run buy myaccount@outlook.com
```
### 3. By Account Number
Use the account index (1-based):
```bash
npm run buy 1 # First enabled account
npm run buy 2 # Second enabled account
```
---
## 🎯 Example Usage
### Redeem Gift Card (Interactive)
```bash
npm run buy
```
Choose your account from the interactive menu.
### Redeem Gift Card (Email)
```bash
npm run buy myaccount@outlook.com
```
1. Script opens Microsoft Rewards in browser
2. Use the **user tab** to browse and redeem
3. **Monitor tab** tracks your balance in background
4. Get notification when points decrease
### Redeem Gift Card (Index)
```bash
npm run buy 1
```
Directly selects the first enabled account from your accounts list.
---
## ⚙️ Configuration
**Set max session time:**
**Edit** `src/config.jsonc`:
```jsonc
{
"buyMode": {
"maxMinutes": 45 // Session duration (minimum: 10, default: 45)
}
}
```
**Session behavior:**
- Monitor tab refreshes every ~10 seconds
- Session automatically ends after `maxMinutes`
- You can close the browser anytime
- Cookies are saved at the end
---
## 🔔 Notifications
Buy mode sends real-time alerts when:
- 💳 **Points spent** — Shows amount and new balance
- 📉 **Balance changes** — Tracks cumulative spending
**Example alert:**
```
💳 Spend Detected (Buy Mode)
Account: user@email.com
Spent: -500 points
Current: 12,500 points
Session spent: 1,200 points
```
**Alert channels:** Uses your configured webhooks (Discord, NTFY, etc.)
---
## 🛠️ Troubleshooting
| Problem | Solution |
|---------|----------|
| **"No enabled accounts found"** | Enable at least one account in `accounts.jsonc` |
| **"Invalid account index"** | Check your account number (must be 1-N) |
| **"Account not found"** | Verify email spelling and that account is enabled |
| **Monitor tab closes** | Script auto-reopens it in background |
| **No spending alerts** | Check webhook/NTFY config in `config.jsonc` |
| **Session too short** | Increase `maxMinutes` in config |
| **Interactive prompt not showing** | Run: `npm run buy` (no arguments) |
---
## ⚠️ Important Notes
-**Browser visible** — Always runs in visible mode (not headless)
-**No automation** — Script only monitors, never clicks or redeems
-**Safe** — Use your browsing tab normally
-**Real-time tracking** — Immediate notifications on point changes
-**Multiple selection methods** — Interactive, email, or index
-**Privacy-friendly** — Emails are masked in interactive mode
- ⚠️ **Only enabled accounts** — Disabled accounts don't appear
---
## 📊 Session Summary
At the end of each buy mode session, you'll receive a summary:
```
Account: myaccount@outlook.com
Duration: 45m 12s
Initial points: 15,000
Current points: 13,500
Total spent: 1,500
```
This summary is sent via your configured notification channels.
---
## 💡 Pro Tips
- **Use interactive mode** for the safest selection
- **Build first** if you modified code: `npm run build`
- **Multiple accounts?** Use numeric index for speed
- **Check your balance** before and after in the monitor tab
---
## 📚 Next Steps
**Setup notifications?**
**[Discord Webhooks](./conclusionwebhook.md)**
**[NTFY Push](./ntfy.md)**
**Manage multiple accounts?**
**[Accounts Guide](./accounts.md)**
**Back to automation?**
**[Getting Started](./getting-started.md)**
---
**[← Back to Hub](./index.md)** | **[Config Guide](./config.md)**

View File

@@ -123,11 +123,10 @@ This page mirrors the defaults that ship in `src/config.jsonc` and explains what
--- ---
## Buy Mode & Updates ## Updates
| Key | Default | Notes | | Key | Default | Notes |
| --- | --- | --- | | --- | --- | --- |
| `buyMode.maxMinutes` | `45` | Session length cap when using `-buy`. |
| `update.git` | `true` | Run git updater after completion. | | `update.git` | `true` | Run git updater after completion. |
| `update.docker` | `false` | Use Docker updater instead. | | `update.docker` | `false` | Use Docker updater instead. |
| `update.scriptPath` | `setup/update/update.mjs` | Update script path. | | `update.scriptPath` | `setup/update/update.mjs` | Update script path. |

View File

@@ -53,7 +53,6 @@ When running inside Docker, you can instead rely on `update.docker: true` so the
- **Risk management**: Leave `riskManagement.enabled` and `banPrediction` on unless you have a reason to reduce telemetry. Raising `riskThreshold` (>75) makes alerts rarer. - **Risk management**: Leave `riskManagement.enabled` and `banPrediction` on unless you have a reason to reduce telemetry. Raising `riskThreshold` (>75) makes alerts rarer.
- **Search pacing**: The delay window (`search.settings.delay.min` / `max`) accepts either numbers (ms) or strings like `"2min"`. Keep the range wide enough for natural behaviour. - **Search pacing**: The delay window (`search.settings.delay.min` / `max`) accepts either numbers (ms) or strings like `"2min"`. Keep the range wide enough for natural behaviour.
- **Dry run**: Set `dryRun: true` to test account rotation without performing tasks. Useful for validating login flow after configuration changes. - **Dry run**: Set `dryRun: true` to test account rotation without performing tasks. Useful for validating login flow after configuration changes.
- **Buy mode**: The config entry simply caps the session length. Use `npm run buy` to launch it.
--- ---

View File

@@ -201,20 +201,6 @@ The build process regenerates `package-lock.json` inside the container to ensure
- **Native performance on all architectures** - **Native performance on all architectures**
- **Raspberry Pi users won't encounter binary mismatch errors** - **Raspberry Pi users won't encounter binary mismatch errors**
### Buy Mode Not Supported
**Buy Mode cannot be used in Docker** because it requires interactive terminal input. Use Buy Mode only in local installations:
```bash
# ✅ Works locally
npm run buy
# ❌ Does not work in Docker
docker exec microsoft-rewards-script npm run buy
```
For manual redemptions, run the bot locally outside Docker.
### Headless Mode Required ### Headless Mode Required
Docker containers **must run in headless mode**. The Dockerfile automatically sets `FORCE_HEADLESS=1`. Do not disable this. Docker containers **must run in headless mode**. The Dockerfile automatically sets `FORCE_HEADLESS=1`. Do not disable this.

View File

@@ -31,7 +31,6 @@
| **[NTFY Alerts](ntfy.md)** | Mobile push notifications | | **[NTFY Alerts](ntfy.md)** | Mobile push notifications |
| **[Proxy Setup](proxy.md)** | IP rotation (optional) | | **[Proxy Setup](proxy.md)** | IP rotation (optional) |
| **[Docker](docker.md)** | Container deployment | | **[Docker](docker.md)** | Container deployment |
| **[Buy Mode](buy-mode.md)** | Manual purchase monitoring |
--- ---

View File

@@ -51,8 +51,7 @@ Open NTFY app → Add subscription → Enter your topic name
- 🚨 **Errors** — Script crashes, login failures - 🚨 **Errors** — Script crashes, login failures
- ⚠️ **Warnings** — Missing points, suspicious activity - ⚠️ **Warnings** — Missing points, suspicious activity
- 🏆 **Milestones** — Account completed successfully - 🏆 **Milestones** — Account completed successfully
- 💳 **Buy mode** — Point spending detected - **Summary** — End-of-run report
- 📊 **Summary** — End-of-run report
--- ---

View File

@@ -24,7 +24,6 @@
"start": "node --enable-source-maps ./dist/index.js", "start": "node --enable-source-maps ./dist/index.js",
"ts-start": "node --loader ts-node/esm ./src/index.ts", "ts-start": "node --loader ts-node/esm ./src/index.ts",
"dev": "ts-node ./src/index.ts -dev", "dev": "ts-node ./src/index.ts -dev",
"buy": "node --enable-source-maps ./dist/index.js -buy",
"creator": "ts-node ./src/account-creation/cli.ts", "creator": "ts-node ./src/account-creation/cli.ts",
"dashboard": "node --enable-source-maps ./dist/index.js -dashboard", "dashboard": "node --enable-source-maps ./dist/index.js -dashboard",
"dashboard-dev": "ts-node ./src/index.ts -dashboard", "dashboard-dev": "ts-node ./src/index.ts -dashboard",

View File

@@ -27,20 +27,10 @@ class Browser {
let browser: import('rebrowser-playwright').Browser let browser: import('rebrowser-playwright').Browser
try { try {
const envForceHeadless = process.env.FORCE_HEADLESS === '1' const envForceHeadless = process.env.FORCE_HEADLESS === '1'
let headless = envForceHeadless ? true : (this.bot.config.browser?.headless ?? false) const headless = envForceHeadless ? true : (this.bot.config.browser?.headless ?? false)
// Buy/Interactive mode: always visible and with enhanced stealth
const isBuyMode = this.bot.isBuyModeEnabled()
if (isBuyMode && !envForceHeadless) {
if (headless !== false) {
const target = this.bot.getBuyModeTarget()
this.bot.log(this.bot.isMobile, 'BROWSER', `Interactive mode: forcing headless=false${target ? ` for ${target}` : ''}`, 'warn')
}
headless = false
}
const engineName = 'chromium' const engineName = 'chromium'
this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless}${isBuyMode ? ', stealth-mode=ENHANCED' : ''})`) this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless})`)
const proxyConfig = this.buildPlaywrightProxy(proxy) const proxyConfig = this.buildPlaywrightProxy(proxy)
const isLinux = process.platform === 'linux' const isLinux = process.platform === 'linux'
@@ -63,26 +53,10 @@ class Browser {
'--disk-cache-size=1' '--disk-cache-size=1'
] : [] ] : []
// ENHANCED STEALTH MODE for Buy/Interactive Mode
// These arguments help bypass CAPTCHA and automation detection
const stealthArgs = isBuyMode ? [
'--disable-blink-features=AutomationControlled', // Critical: Hide automation
'--disable-features=IsolateOrigins,site-per-process', // Reduce detection surface
'--disable-site-isolation-trials',
'--disable-web-security', // Allow cross-origin (may help with CAPTCHA)
'--disable-features=VizDisplayCompositor', // Reduce GPU fingerprinting
'--no-first-run',
'--no-default-browser-check',
'--disable-infobars',
'--window-position=0,0',
'--window-size=1920,1080', // Consistent window size
'--start-maximized'
] : []
browser = await playwright.chromium.launch({ browser = await playwright.chromium.launch({
headless, headless,
...(proxyConfig && { proxy: proxyConfig }), ...(proxyConfig && { proxy: proxyConfig }),
args: [...baseArgs, ...linuxStabilityArgs, ...stealthArgs], args: [...baseArgs, ...linuxStabilityArgs],
timeout: isLinux ? 90000 : 60000 timeout: isLinux ? 90000 : 60000
}) })
} catch (e: unknown) { } catch (e: unknown) {
@@ -106,8 +80,6 @@ class Browser {
const globalTimeout = this.bot.config.browser?.globalTimeout ?? 30000 const globalTimeout = this.bot.config.browser?.globalTimeout ?? 30000
context.setDefaultTimeout(typeof globalTimeout === 'number' ? globalTimeout : this.bot.utils.stringToMs(globalTimeout)) context.setDefaultTimeout(typeof globalTimeout === 'number' ? globalTimeout : this.bot.utils.stringToMs(globalTimeout))
const isBuyMode = this.bot.isBuyModeEnabled()
try { try {
context.on('page', async (page) => { context.on('page', async (page) => {
try { try {
@@ -131,65 +103,6 @@ class Browser {
document.documentElement.appendChild(style) document.documentElement.appendChild(style)
} catch {/* ignore */} } catch {/* ignore */}
}) })
// ENHANCED ANTI-DETECTION for Buy/Interactive Mode
if (isBuyMode) {
await page.addInitScript(`
// Override navigator.webdriver (critical for CAPTCHA bypass)
Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', {
get: () => false
});
// Add chrome runtime (looks more human)
Object.defineProperty(window, 'chrome', {
writable: true,
enumerable: true,
configurable: false,
value: { runtime: {} }
});
// Add plugins (looks more human)
Object.defineProperty(navigator, 'plugins', {
get: () => [
{ name: 'Chrome PDF Plugin' },
{ name: 'Chrome PDF Viewer' },
{ name: 'Native Client' }
]
});
// Languages
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en', 'fr']
});
// Hide automation markers
['__nightmare', '__playwright', '__pw_manual', '__webdriver_script_fn', 'webdriver'].forEach(prop => {
try {
if (prop in window) delete (window as Record<string, unknown>)[prop];
} catch {
// Silently ignore: property deletion may be blocked by browser security
}
});
// Override permissions to avoid detection
const originalPermissionsQuery = window.navigator.permissions.query;
window.navigator.permissions.query = function(params) {
if (params.name === 'notifications') {
return Promise.resolve({
state: Notification.permission,
name: 'notifications',
onchange: null,
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => true
});
}
return originalPermissionsQuery.call(this, params);
};
`)
this.bot.log(this.bot.isMobile, 'BROWSER', '🛡️ Enhanced stealth mode activated (anti-CAPTCHA)', 'log', 'green')
}
} catch (e) { } catch (e) {
this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn') this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn')
} }

View File

@@ -143,11 +143,6 @@
"host": "127.0.0.1" // Bind address (default: 127.0.0.1, localhost only for security) "host": "127.0.0.1" // Bind address (default: 127.0.0.1, localhost only for security)
}, },
// Buy mode
"buyMode": {
"maxMinutes": 45
},
// Updates // Updates
"update": { "update": {
"enabled": true, // Enable automatic updates (default: true) "enabled": true, // Enable automatic updates (default: true)

View File

@@ -1,197 +0,0 @@
/**
* Buy Mode Manual Handler Module
* Extracted from index.ts runBuyMode() method
*
* Provides manual spending mode where user retains control:
* - Opens two browser tabs (monitor + browsing)
* - Passively monitors point changes every ~10s
* - Detects spending and sends notifications
* - Session has configurable duration (default: 45 minutes)
* - User manually selects and purchases items
*/
import type { BrowserContext } from 'playwright'
import type { MicrosoftRewardsBot } from '../index'
import type { Account } from '../interface/Account'
import { BuyModeMonitor, BuyModeSelector } from '../util/BuyMode'
import { saveSessionData } from '../util/Load'
import { log } from '../util/Logger'
interface AccountSummary {
email: string
durationMs: number
desktopCollected: number
mobileCollected: number
totalCollected: number
initialTotal: number
endTotal: number
errors: string[]
banned: { status: boolean; reason: string }
}
export class BuyModeManual {
private bot: MicrosoftRewardsBot
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
/**
* Execute manual buy mode session
* Opens browser, logs in, and passively monitors points while user browses/purchases
* @param buyModeArgument Optional account email to use (otherwise prompts user)
* @returns Promise that resolves when session ends
*/
async execute(buyModeArgument?: string): Promise<void> {
try {
const buyModeConfig = this.bot.config.buyMode as { maxMinutes?: number } | undefined
const maxMinutes = buyModeConfig?.maxMinutes ?? 45
// Access private accounts array via type assertion
const accounts = (this.bot as unknown as { accounts: Account[] }).accounts
const selector = new BuyModeSelector(accounts)
const selection = await selector.selectAccount(buyModeArgument, maxMinutes)
if (!selection) {
log('main', 'BUY-MODE', 'Buy mode cancelled: no account selected', 'warn')
return
}
const { account, maxMinutes: sessionMaxMinutes } = selection
log('main', 'BUY-MODE', `Buy mode ENABLED for ${account.email}. Opening 2 tabs: (1) monitor tab (auto-refresh), (2) your browsing tab`, 'log', 'green')
log('main', 'BUY-MODE', `Session duration: ${sessionMaxMinutes} minutes. Monitor tab refreshes every ~10s. Use the other tab for your actions.`, 'log', 'yellow')
this.bot.isMobile = false
// Access private browserFactory via type assertion
const browserFactory = (this.bot as unknown as { browserFactory: { createBrowser: (proxy: Account['proxy'], email: string) => Promise<BrowserContext> } }).browserFactory
const browser = await browserFactory.createBrowser(account.proxy, account.email)
// Open the monitor tab FIRST so auto-refresh happens out of the way
let monitor = await browser.newPage()
// Access private login via type assertion (use 'any' for internal page type - unavoidable)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loginModule = (this.bot as any).login
await loginModule.login(monitor, account.email, account.password, account.totp)
await this.bot.browser.func.goHome(monitor)
this.bot.log(false, 'BUY-MODE', 'Opened MONITOR tab (auto-refreshes to track points).', 'log', 'yellow')
// Then open the user free-browsing tab SECOND so users don't see the refreshes
const page = await browser.newPage()
await this.bot.browser.func.goHome(page)
this.bot.log(false, 'BUY-MODE', 'Opened USER tab (use this one to redeem/purchase freely).', 'log', 'green')
// Helper to recreate monitor tab if the user closes it
const recreateMonitor = async () => {
try { if (!monitor.isClosed()) await monitor.close() } catch { /* ignore */ }
monitor = await browser.newPage()
await this.bot.browser.func.goHome(monitor)
}
// Helper to send an immediate spend notice via webhooks/NTFY
const sendSpendNotice = async (delta: number, nowPts: number, cumulativeSpent: number) => {
try {
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
await ConclusionWebhook(
this.bot.config,
'💳 Spend Detected',
`**Account:** ${account.email}\n**Spent:** -${delta} points\n**Current:** ${nowPts} points\n**Session spent:** ${cumulativeSpent} points`,
undefined,
0xFFAA00
)
} catch (e) {
this.bot.log(false, 'BUY-MODE', `Failed to send spend notice: ${e instanceof Error ? e.message : e}`, 'warn')
}
}
// Get initial points
let initial = 0
try {
const data = await this.bot.browser.func.getDashboardData(monitor)
initial = data.userStatus.availablePoints || 0
} catch {/* ignore */}
const pointMonitor = new BuyModeMonitor(initial)
this.bot.log(false, 'BUY-MODE', `Logged in as ${account.email}. Starting passive point monitoring (session: ${sessionMaxMinutes} min)`)
// Passive watcher: poll points periodically without clicking.
const start = Date.now()
const endAt = start + sessionMaxMinutes * 60 * 1000
while (Date.now() < endAt) {
await this.bot.utils.wait(10000)
// If monitor tab was closed by user, recreate it quietly
try {
if (monitor.isClosed()) {
this.bot.log(false, 'BUY-MODE', 'Monitor tab was closed; reopening in background...', 'warn')
await recreateMonitor()
}
} catch (e) {
this.bot.log(false, 'BUY-MODE', `Failed to check/recreate monitor tab: ${e instanceof Error ? e.message : String(e)}`, 'warn')
}
try {
const data = await this.bot.browser.func.getDashboardData(monitor)
const nowPts = data.userStatus.availablePoints || 0
const spendInfo = pointMonitor.checkSpending(nowPts)
if (spendInfo) {
this.bot.log(false, 'BUY-MODE', `Detected spend: -${spendInfo.spent} points (current: ${spendInfo.current})`)
await sendSpendNotice(spendInfo.spent, spendInfo.current, spendInfo.total)
}
} catch (err) {
// If we lost the page context, recreate the monitor tab and continue
const msg = err instanceof Error ? err.message : String(err)
if (/Target closed|page has been closed|browser has been closed/i.test(msg)) {
this.bot.log(false, 'BUY-MODE', 'Monitor page closed or lost; recreating...', 'warn')
try {
await recreateMonitor()
} catch (e) {
this.bot.log(false, 'BUY-MODE', `Failed to recreate monitor: ${e instanceof Error ? e.message : String(e)}`, 'warn')
}
} else {
this.bot.log(false, 'BUY-MODE', `Dashboard check error: ${msg}`, 'warn')
}
}
}
// Save cookies and close monitor; keep main page open for user until they close it themselves
try {
await saveSessionData(this.bot.config.sessionPath, browser, account.email, this.bot.isMobile)
} catch (e) {
log(false, 'BUY-MODE', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn')
}
try {
if (!monitor.isClosed()) await monitor.close()
} catch (e) {
log(false, 'BUY-MODE', `Failed to close monitor tab: ${e instanceof Error ? e.message : String(e)}`, 'warn')
}
// Send a final minimal conclusion webhook for this manual session
const monitorSummary = pointMonitor.getSummary()
const summary: AccountSummary = {
email: account.email,
durationMs: monitorSummary.duration,
desktopCollected: 0,
mobileCollected: 0,
totalCollected: -monitorSummary.spent, // negative indicates spend
initialTotal: monitorSummary.initial,
endTotal: monitorSummary.current,
errors: [],
banned: { status: false, reason: '' }
}
// Access private sendConclusion via type assertion
const sendConclusion = (this.bot as unknown as { sendConclusion: (summaries: AccountSummary[]) => Promise<void> }).sendConclusion
await sendConclusion.call(this.bot, [summary])
this.bot.log(false, 'BUY-MODE', 'Buy mode session finished (monitoring period ended). You can close the browser when done.')
} catch (e) {
this.bot.log(false, 'BUY-MODE', `Error in buy mode: ${e instanceof Error ? e.message : String(e)}`, 'error')
}
}
}

View File

@@ -1,11 +1,10 @@
// ------------------------------- // -------------------------------
// REFACTORING STATUS: COMPLETED // REFACTORING STATUS: COMPLETED ✅
// ------------------------------- // -------------------------------
// Successfully modularized into separate flow modules: // Successfully modularized into separate flow modules:
// DesktopFlow.ts (Desktop automation logic) - INTEGRATED // ✅ DesktopFlow.ts (Desktop automation logic) - INTEGRATED
// MobileFlow.ts (Mobile automation logic) - INTEGRATED // ✅ MobileFlow.ts (Mobile automation logic) - INTEGRATED
// SummaryReporter.ts (Report generation) - INTEGRATED // ✅ SummaryReporter.ts (Report generation) - INTEGRATED
// ✅ BuyModeManual.ts (Manual spending mode) - CREATED (integration pending)
// This improved testability and maintainability by 31% code reduction. // This improved testability and maintainability by 31% code reduction.
// ------------------------------- // -------------------------------
@@ -17,16 +16,14 @@ import path from 'path'
import type { Page } from 'playwright' import type { Page } from 'playwright'
import { createInterface } from 'readline' import { createInterface } from 'readline'
import Browser from './browser/Browser'
import BrowserFunc from './browser/BrowserFunc' import BrowserFunc from './browser/BrowserFunc'
import BrowserUtil from './browser/BrowserUtil' import BrowserUtil from './browser/BrowserUtil'
import Axios from './util/Axios' import Axios from './util/Axios'
import { detectBanReason } from './util/BanDetector' import { detectBanReason } from './util/BanDetector'
import { BuyModeMonitor, BuyModeSelector } from './util/BuyMode'
import Humanizer from './util/Humanizer' import Humanizer from './util/Humanizer'
import JobState from './util/JobState' import JobState from './util/JobState'
import { loadAccounts, loadConfig, saveSessionData } from './util/Load' import { loadAccounts, loadConfig } from './util/Load'
import { log } from './util/Logger' import { log } from './util/Logger'
import { MobileRetryTracker } from './util/MobileRetryTracker' import { MobileRetryTracker } from './util/MobileRetryTracker'
import { QueryDiversityEngine } from './util/QueryDiversityEngine' import { QueryDiversityEngine } from './util/QueryDiversityEngine'
@@ -34,7 +31,6 @@ import { StartupValidator } from './util/StartupValidator'
import { Util } from './util/Utils' import { Util } from './util/Utils'
import { Activities } from './functions/Activities' import { Activities } from './functions/Activities'
import { Login } from './functions/Login'
import { Workers } from './functions/Workers' import { Workers } from './functions/Workers'
import { DesktopFlow } from './flows/DesktopFlow' import { DesktopFlow } from './flows/DesktopFlow'
@@ -65,12 +61,8 @@ export class MicrosoftRewardsBot {
public compromisedReason?: string public compromisedReason?: string
private activeWorkers: number private activeWorkers: number
private browserFactory: Browser = new Browser(this)
private accounts: Account[] private accounts: Account[]
public workers: Workers // Made public for DesktopFlow access public workers: Workers // Made public for DesktopFlow access
private login = new Login(this)
private buyModeEnabled: boolean = false
private buyModeArgument?: string
// Summary collection (per process) // Summary collection (per process)
private accountSummaries: AccountSummary[] = [] private accountSummaries: AccountSummary[] = []
@@ -107,27 +99,6 @@ export class MicrosoftRewardsBot {
cacheMinutes: this.config.queryDiversity.cacheMinutes cacheMinutes: this.config.queryDiversity.cacheMinutes
}) })
} }
// Buy mode: CLI args take precedence over config
const idx = process.argv.indexOf('-buy')
if (idx >= 0) {
this.buyModeEnabled = true
this.buyModeArgument = process.argv[idx + 1]
} else {
// Fallback to config if no CLI flag
const buyModeConfig = this.config.buyMode as { enabled?: boolean } | undefined
if (buyModeConfig?.enabled === true) {
this.buyModeEnabled = true
}
}
}
public isBuyModeEnabled(): boolean {
return this.buyModeEnabled === true
}
public getBuyModeTarget(): string | undefined {
return this.buyModeArgument
} }
async initialize() { async initialize() {
@@ -214,7 +185,7 @@ export class MicrosoftRewardsBot {
}) })
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
rl.question('\n⚠️ Reset job state and run all accounts again? (y/N): ', (answer) => { rl.question('\n⚠️ Reset job state and run all accounts again? (y/N): ', (answer) => {
rl.close() rl.close()
const trimmed = answer.trim().toLowerCase() const trimmed = answer.trim().toLowerCase()
resolve(trimmed === 'y' || trimmed === 'yes') resolve(trimmed === 'y' || trimmed === 'yes')
@@ -242,12 +213,6 @@ export class MicrosoftRewardsBot {
this.printBanner() this.printBanner()
log('main', 'MAIN', `Bot started with ${this.config.clusters} clusters`) log('main', 'MAIN', `Bot started with ${this.config.clusters} clusters`)
// If buy mode is enabled, run single-account interactive session without automation
if (this.buyModeEnabled) {
await this.runBuyMode()
return
}
// Only cluster when there's more than 1 cluster demanded // Only cluster when there's more than 1 cluster demanded
if (this.config.clusters > 1) { if (this.config.clusters > 1) {
if (cluster.isPrimary) { if (cluster.isPrimary) {
@@ -269,170 +234,20 @@ export class MicrosoftRewardsBot {
} }
} }
} }
/** Manual spending session: login, then leave control to user while we passively monitor points. */
private async runBuyMode() {
try {
await this.initialize()
const buyModeConfig = this.config.buyMode as { maxMinutes?: number } | undefined
const maxMinutes = buyModeConfig?.maxMinutes ?? 45
const selector = new BuyModeSelector(this.accounts)
const selection = await selector.selectAccount(this.buyModeArgument, maxMinutes)
if (!selection) {
log('main', 'BUY-MODE', 'Buy mode cancelled: no account selected', 'warn')
return
}
const { account, maxMinutes: sessionMaxMinutes } = selection
log('main', 'BUY-MODE', `Buy mode ENABLED for ${account.email}. Opening 2 tabs: (1) monitor tab (auto-refresh), (2) your browsing tab`, 'log', 'green')
log('main', 'BUY-MODE', `Session duration: ${sessionMaxMinutes} minutes. Monitor tab refreshes every ~10s. Use the other tab for your actions.`, 'log', 'yellow')
this.isMobile = false
this.axios = new Axios(account.proxy)
const browser = await this.browserFactory.createBrowser(account.proxy, account.email)
// Open the monitor tab FIRST so auto-refresh happens out of the way
let monitor = await browser.newPage()
await this.login.login(monitor, account.email, account.password, account.totp)
await this.browser.func.goHome(monitor)
this.log(false, 'BUY-MODE', 'Opened MONITOR tab (auto-refreshes to track points).', 'log', 'yellow')
// Then open the user free-browsing tab SECOND so users dont see the refreshes
const page = await browser.newPage()
await this.browser.func.goHome(page)
this.log(false, 'BUY-MODE', 'Opened USER tab (use this one to redeem/purchase freely).', 'log', 'green')
// Helper to recreate monitor tab if the user closes it
const recreateMonitor = async () => {
try { if (!monitor.isClosed()) await monitor.close() } catch { /* ignore */ }
monitor = await browser.newPage()
await this.browser.func.goHome(monitor)
}
// Helper to send an immediate spend notice via webhooks/NTFY
const sendSpendNotice = async (delta: number, nowPts: number, cumulativeSpent: number) => {
try {
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
await ConclusionWebhook(
this.config,
'💳 Spend Detected',
`**Account:** ${account.email}\n**Spent:** -${delta} points\n**Current:** ${nowPts} points\n**Session spent:** ${cumulativeSpent} points`,
undefined,
0xFFAA00
)
} catch (e) {
this.log(false, 'BUY-MODE', `Failed to send spend notice: ${e instanceof Error ? e.message : e}`, 'warn')
}
}
// Get initial points
let initial = 0
try {
const data = await this.browser.func.getDashboardData(monitor)
initial = data.userStatus.availablePoints || 0
} catch {/* ignore */}
const pointMonitor = new BuyModeMonitor(initial)
this.log(false, 'BUY-MODE', `Logged in as ${account.email}. Starting passive point monitoring (session: ${sessionMaxMinutes} min)`)
// Passive watcher: poll points periodically without clicking.
const start = Date.now()
const endAt = start + sessionMaxMinutes * 60 * 1000
while (Date.now() < endAt) {
await this.utils.wait(10000)
// If monitor tab was closed by user, recreate it quietly
try {
if (monitor.isClosed()) {
this.log(false, 'BUY-MODE', 'Monitor tab was closed; reopening in background...', 'warn')
await recreateMonitor()
}
} catch (e) {
this.log(false, 'BUY-MODE', `Failed to check/recreate monitor tab: ${e instanceof Error ? e.message : String(e)}`, 'warn')
}
try {
const data = await this.browser.func.getDashboardData(monitor)
const nowPts = data.userStatus.availablePoints || 0
const spendInfo = pointMonitor.checkSpending(nowPts)
if (spendInfo) {
this.log(false, 'BUY-MODE', `Detected spend: -${spendInfo.spent} points (current: ${spendInfo.current})`)
await sendSpendNotice(spendInfo.spent, spendInfo.current, spendInfo.total)
}
} catch (err) {
// If we lost the page context, recreate the monitor tab and continue
const msg = err instanceof Error ? err.message : String(err)
if (/Target closed|page has been closed|browser has been closed/i.test(msg)) {
this.log(false, 'BUY-MODE', 'Monitor page closed or lost; recreating...', 'warn')
try {
await recreateMonitor()
} catch (e) {
this.log(false, 'BUY-MODE', `Failed to recreate monitor: ${e instanceof Error ? e.message : String(e)}`, 'warn')
}
} else {
this.log(false, 'BUY-MODE', `Dashboard check error: ${msg}`, 'warn')
}
}
}
// Save cookies and close monitor; keep main page open for user until they close it themselves
try {
await saveSessionData(this.config.sessionPath, browser, account.email, this.isMobile)
} catch (e) {
log(false, 'BUY-MODE', `Failed to save session: ${e instanceof Error ? e.message : String(e)}`, 'warn')
}
try {
if (!monitor.isClosed()) await monitor.close()
} catch (e) {
log(false, 'BUY-MODE', `Failed to close monitor tab: ${e instanceof Error ? e.message : String(e)}`, 'warn')
}
// Send a final minimal conclusion webhook for this manual session
const monitorSummary = pointMonitor.getSummary()
const summary: AccountSummary = {
email: account.email,
durationMs: monitorSummary.duration,
desktopCollected: 0,
mobileCollected: 0,
totalCollected: -monitorSummary.spent, // negative indicates spend
initialTotal: monitorSummary.initial,
endTotal: monitorSummary.current,
errors: [],
banned: { status: false, reason: '' }
}
await this.sendConclusion([summary])
this.log(false, 'BUY-MODE', 'Buy mode session finished (monitoring period ended). You can close the browser when done.')
} catch (e) {
this.log(false, 'BUY-MODE', `Error in buy mode: ${e instanceof Error ? e.message : String(e)}`, 'error')
}
}
private printBanner() { private printBanner() {
if (this.config.clusters > 1 && !cluster.isPrimary) return if (this.config.clusters > 1 && !cluster.isPrimary) return
const version = this.getVersion() const version = this.getVersion()
const mode = this.buyModeEnabled ? 'Manual Mode' : 'Automated Mode'
log('main', 'BANNER', `Microsoft Rewards Bot v${version} - ${mode}`) log('main', 'BANNER', `Microsoft Rewards Bot v${version}`)
log('main', 'BANNER', `PID: ${process.pid} | Workers: ${this.config.clusters}`) log('main', 'BANNER', `PID: ${process.pid} | Workers: ${this.config.clusters}`)
if (this.buyModeEnabled) { const upd = this.config.update || {}
log('main', 'BANNER', `Target: ${this.buyModeArgument || 'Interactive selection'}`) const updTargets: string[] = []
} else { if (upd.method && upd.method !== 'zip') updTargets.push(`Update: ${upd.method}`)
const upd = this.config.update || {} if (upd.docker) updTargets.push('Docker')
const updTargets: string[] = [] if (updTargets.length > 0) {
if (upd.method && upd.method !== 'zip') updTargets.push(`Update: ${upd.method}`) log('main', 'BANNER', `Auto-Update: ${updTargets.join(', ')}`)
if (upd.docker) updTargets.push('Docker')
if (updTargets.length > 0) {
log('main', 'BANNER', `Auto-Update: ${updTargets.join(', ')}`)
}
} }
} }
@@ -540,7 +355,7 @@ export class MicrosoftRewardsBot {
try { try {
const updateCode = await this.runAutoUpdate() const updateCode = await this.runAutoUpdate()
if (updateCode === 0) { if (updateCode === 0) {
log('main', 'UPDATE', ' Update successful - next run will use new version', 'log', 'green') log('main', 'UPDATE', '✅ Update successful - next run will use new version', 'log', 'green')
} }
} catch (e) { } catch (e) {
log('main', 'UPDATE', `Auto-update failed: ${e instanceof Error ? e.message : String(e)}`, 'warn') log('main', 'UPDATE', `Auto-update failed: ${e instanceof Error ? e.message : String(e)}`, 'warn')
@@ -812,7 +627,7 @@ export class MicrosoftRewardsBot {
// If update was successful (code 0), restart the script to use the new version // If update was successful (code 0), restart the script to use the new version
// This is critical for cron jobs - they need to apply updates immediately // This is critical for cron jobs - they need to apply updates immediately
if (updateResult === 0) { if (updateResult === 0) {
log('main', 'UPDATE', ' Update successful - restarting with new version...', 'log', 'green') log('main', 'UPDATE', '✅ Update successful - restarting with new version...', 'log', 'green')
// On Raspberry Pi/Linux with cron, just exit - cron will handle next run // On Raspberry Pi/Linux with cron, just exit - cron will handle next run
// No need to restart immediately, next scheduled run will use new code // No need to restart immediately, next scheduled run will use new code
log('main', 'UPDATE', 'Next scheduled run will use the updated code', 'log') log('main', 'UPDATE', 'Next scheduled run will use the updated code', 'log')
@@ -829,7 +644,7 @@ export class MicrosoftRewardsBot {
const { ConclusionWebhook } = await import('./util/ConclusionWebhook') const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
await ConclusionWebhook( await ConclusionWebhook(
this.config, this.config,
'🚫 Ban Detected', '🚫 Ban Detected',
`**Account:** ${email}\n**Reason:** ${reason || 'detected by heuristics'}`, `**Account:** ${email}\n**Reason:** ${reason || 'detected by heuristics'}`,
undefined, undefined,
DISCORD.COLOR_RED DISCORD.COLOR_RED
@@ -968,7 +783,7 @@ export class MicrosoftRewardsBot {
const { ConclusionWebhook } = await import('./util/ConclusionWebhook') const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
await ConclusionWebhook( await ConclusionWebhook(
this.config, this.config,
'🚨 Critical Security Alert', '🚨 Critical Security Alert',
`@everyone\n\n**Account:** ${email}\n**Issue:** ${reason}\n**Status:** All accounts paused pending review`, `@everyone\n\n**Account:** ${email}\n**Issue:** ${reason}\n**Status:** All accounts paused pending review`,
undefined, undefined,
DISCORD.COLOR_RED DISCORD.COLOR_RED

View File

@@ -23,7 +23,6 @@ export interface Config {
ntfy: ConfigNtfy; ntfy: ConfigNtfy;
update?: ConfigUpdate; update?: ConfigUpdate;
passesPerRun?: number; passesPerRun?: number;
buyMode?: ConfigBuyMode; // Optional manual spending mode
vacation?: ConfigVacation; // Optional monthly contiguous off-days vacation?: ConfigVacation; // Optional monthly contiguous off-days
crashRecovery?: ConfigCrashRecovery; // Automatic restart / graceful shutdown crashRecovery?: ConfigCrashRecovery; // Automatic restart / graceful shutdown
riskManagement?: ConfigRiskManagement; // NEW: Risk-aware throttling and ban prediction riskManagement?: ConfigRiskManagement; // NEW: Risk-aware throttling and ban prediction
@@ -90,11 +89,6 @@ export interface ConfigUpdate {
autoUpdateAccounts?: boolean; // if true, allow auto-update of accounts.json when remote changes it (default: false to preserve credentials) autoUpdateAccounts?: boolean; // if true, allow auto-update of accounts.json when remote changes it (default: false to preserve credentials)
} }
export interface ConfigBuyMode {
enabled?: boolean; // if true, force buy mode session
maxMinutes?: number; // session duration cap
}
export interface ConfigVacation { export interface ConfigVacation {
enabled?: boolean; // default false enabled?: boolean; // default false
minDays?: number; // default 3 minDays?: number; // default 3

View File

@@ -1,219 +0,0 @@
import { createInterface } from 'readline'
import type { Account } from '../interface/Account'
import { log } from './Logger'
export interface BuyModeSelection {
account: Account
maxMinutes: number
}
export class BuyModeSelector {
private accounts: Account[]
constructor(accounts: Account[]) {
this.accounts = accounts.filter(acc => acc.enabled !== false)
}
/**
* Parse the buy mode argument from CLI.
* Supports: email, numeric index (1-based), or undefined for interactive selection.
*/
async selectAccount(
argument?: string,
maxMinutes: number = 45
): Promise<BuyModeSelection | null> {
if (this.accounts.length === 0) {
log('main', 'BUY-MODE', 'No enabled accounts found. Please enable at least one account in accounts.jsonc', 'error')
return null
}
let selectedAccount: Account | null = null
if (!argument) {
selectedAccount = await this.promptInteractiveSelection()
} else if (this.isNumericIndex(argument)) {
selectedAccount = this.selectByIndex(argument)
} else if (this.isEmail(argument)) {
selectedAccount = this.selectByEmail(argument)
} else {
log('main', 'BUY-MODE', `Invalid argument: "${argument}". Expected email or numeric index.`, 'error')
return null
}
if (!selectedAccount) {
return null
}
return {
account: selectedAccount,
maxMinutes: Math.max(10, maxMinutes)
}
}
private isNumericIndex(value: string): boolean {
return /^\d+$/.test(value)
}
private isEmail(value: string): boolean {
return /@/.test(value)
}
private selectByIndex(indexStr: string): Account | null {
const index = parseInt(indexStr, 10)
if (index < 1 || index > this.accounts.length) {
log('main', 'BUY-MODE', `Invalid account index: ${index}. Valid range: 1-${this.accounts.length}`, 'error')
this.displayAccountList()
return null
}
const account = this.accounts[index - 1]
log('main', 'BUY-MODE', `Selected account #${index}: ${this.maskEmail(account!.email)}`, 'log', 'green')
return account!
}
private selectByEmail(email: string): Account | null {
const account = this.accounts.find(acc => acc.email.toLowerCase() === email.toLowerCase())
if (!account) {
log('main', 'BUY-MODE', `Account not found: ${email}`, 'error')
this.displayAccountList()
return null
}
log('main', 'BUY-MODE', `Selected account: ${this.maskEmail(account.email)}`, 'log', 'green')
return account
}
private async promptInteractiveSelection(): Promise<Account | null> {
log('main', 'BUY-MODE', 'No account specified. Please select an account:', 'log', 'cyan')
this.displayAccountList()
const rl = createInterface({
input: process.stdin,
output: process.stdout
})
return new Promise<Account | null>((resolve) => {
rl.question('\nEnter account number (1-' + this.accounts.length + ') or email: ', (answer) => {
rl.close()
const trimmed = answer.trim()
if (!trimmed) {
log('main', 'BUY-MODE', 'No selection made. Exiting buy mode.', 'warn')
resolve(null)
return
}
let selected: Account | null = null
if (this.isNumericIndex(trimmed)) {
selected = this.selectByIndex(trimmed)
} else if (this.isEmail(trimmed)) {
selected = this.selectByEmail(trimmed)
} else {
log('main', 'BUY-MODE', `Invalid input: "${trimmed}". Expected number or email.`, 'error')
}
resolve(selected)
})
})
}
private displayAccountList(): void {
// Note: console.log is intentionally used here for interactive user prompts
// This is a CLI menu, not system logging - should go directly to stdout
console.log('\nAvailable accounts:')
console.log('─'.repeat(60))
this.accounts.forEach((acc, idx) => {
const num = `[${idx + 1}]`.padEnd(5)
const email = this.maskEmail(acc.email).padEnd(35)
const proxy = acc.proxy?.url ? '🔒 Proxy' : 'Direct'
console.log(`${num} ${email} ${proxy}`)
})
console.log('─'.repeat(60))
}
private maskEmail(email: string): string {
const [local, domain] = email.split('@')
if (!local || !domain) return email
if (local.length <= 3) {
return `${local[0]}***@${domain}`
}
const visibleStart = local.slice(0, 2)
const visibleEnd = local.slice(-1)
return `${visibleStart}***${visibleEnd}@${domain}`
}
}
export class BuyModeMonitor {
private initialPoints: number = 0
private lastPoints: number = 0
private totalSpent: number = 0
private monitorStartTime: number = 0
constructor(initialPoints: number) {
this.initialPoints = initialPoints
this.lastPoints = initialPoints
this.monitorStartTime = Date.now()
}
/**
* Update the current points and detect spending.
* Returns spending info if points decreased, null otherwise.
*/
checkSpending(currentPoints: number): { spent: number; current: number; total: number } | null {
if (currentPoints < this.lastPoints) {
const spent = this.lastPoints - currentPoints
this.totalSpent += spent
this.lastPoints = currentPoints
return {
spent,
current: currentPoints,
total: this.totalSpent
}
}
if (currentPoints > this.lastPoints) {
this.lastPoints = currentPoints
}
return null
}
getTotalSpent(): number {
return this.totalSpent
}
getSessionDuration(): number {
return Date.now() - this.monitorStartTime
}
getCurrentPoints(): number {
return this.lastPoints
}
getInitialPoints(): number {
return this.initialPoints
}
getSummary(): {
initial: number
current: number
spent: number
duration: number
} {
return {
initial: this.initialPoints,
current: this.lastPoints,
spent: this.totalSpent,
duration: this.getSessionDuration()
}
}
}

View File

@@ -145,11 +145,6 @@ function normalizeConfig(raw: unknown): Config {
const conclusionWebhook = notifications.conclusionWebhook ?? n.conclusionWebhook ?? { enabled: false, url: '' } const conclusionWebhook = notifications.conclusionWebhook ?? n.conclusionWebhook ?? { enabled: false, url: '' }
const ntfy = notifications.ntfy ?? n.ntfy ?? { enabled: false, url: '', topic: '', authToken: '' } const ntfy = notifications.ntfy ?? n.ntfy ?? { enabled: false, url: '', topic: '', authToken: '' }
// Buy Mode
const buyMode = n.buyMode ?? {}
const buyModeEnabled = typeof buyMode.enabled === 'boolean' ? buyMode.enabled : false
const buyModeMax = typeof buyMode.maxMinutes === 'number' ? buyMode.maxMinutes : 45
// Fingerprinting // Fingerprinting
const saveFingerprint = (n.fingerprinting?.saveFingerprint ?? n.saveFingerprint) ?? { mobile: false, desktop: false } const saveFingerprint = (n.fingerprinting?.saveFingerprint ?? n.saveFingerprint) ?? { mobile: false, desktop: false }
@@ -244,7 +239,6 @@ function normalizeConfig(raw: unknown): Config {
update: n.update, update: n.update,
passesPerRun: passesPerRun, passesPerRun: passesPerRun,
vacation: n.vacation, vacation: n.vacation,
buyMode: { enabled: buyModeEnabled, maxMinutes: buyModeMax },
crashRecovery: n.crashRecovery || {}, crashRecovery: n.crashRecovery || {},
riskManagement, riskManagement,
dryRun, dryRun,