THIS IS A HOTFIX TO THE CURRENT STATE TO FIX SOME DOCKER AND STABILITY RELATED ISSUES. ALSO TO REMOVE SOME DEAD CODE.
A PROPER VERSION OF "V2" IS BEING WORKED ON!

- Migrated configuration files from JSONC to JSON
- Removed deprecated setup scripts
- Updated dependencies in package.json and package-lock.json
- Updated README with expanded setup, configuration, and feature documentation
This commit is contained in:
TheNetsky
2025-11-10 10:56:57 +01:00
parent bd96aeb20c
commit 2738c85030
24 changed files with 855 additions and 1556 deletions

4
.gitignore vendored
View File

@@ -6,11 +6,7 @@ node_modules/
diagnostic/
report/
accounts.json
accounts.jsonc
notes
accounts.dev.json
accounts.dev.jsonc
accounts.main.json
accounts.main.jsonc
.DS_Store
.playwright-chromium-installed

128
NOTICE
View File

@@ -1,128 +0,0 @@
# IMPORTANT LEGAL NOTICES
## 🚨 Terms of Service Violation Warning
**Using this software violates Microsoft's Terms of Service.**
Microsoft Rewards explicitly prohibits:
- Automated point collection
- Bot usage for completing tasks
- Any form of automation on their platform
### Potential Consequences:
- ❌ **Immediate account suspension**
- ❌ **Permanent ban from Microsoft Rewards**
- ❌ **Forfeiture of all accumulated points**
- ❌ **Loss of redemption history**
- ⚠️ Possible restrictions on other Microsoft services
---
## 🏢 Commercial Use Prohibited
This software is licensed for **PERSONAL, NON-COMMERCIAL USE ONLY**.
### ❌ Prohibited Activities:
- Selling this software or modifications
- Offering it as a paid service
- Using it for business purposes
- Creating commercial automation services
- Bulk account management for profit
- Integration into commercial platforms
### ✅ Permitted Activities:
- Personal use for your own accounts
- Educational purposes and learning
- Private modifications for personal use
- Sharing with family/friends (non-commercial)
---
## ⚖️ Legal Disclaimer
### No Warranty
This software is provided "AS IS" without any warranty of any kind.
### No Liability
The authors and contributors:
- Are NOT responsible for account suspensions
- Are NOT responsible for lost points or rewards
- Are NOT responsible for any damages
- Do NOT encourage ToS violations
- Provide this for educational purposes ONLY
### Your Responsibility
You are solely responsible for:
- Your use of this software
- Compliance with Microsoft's policies
- Any consequences from automation
- Legal implications in your jurisdiction
---
## 🎓 Educational Purpose Statement
This project is developed and maintained for **educational purposes**:
- To demonstrate browser automation techniques
- To showcase TypeScript and Playwright capabilities
- To teach software architecture patterns
- To explore anti-detection methodologies
**The authors do not condone using this software in violation of any Terms of Service.**
---
## 🔒 Privacy & Security
### Your Data:
- This software stores credentials **locally only**
- No data is sent to third parties
- Sessions are stored in the `sessions/` folder
- You can delete all data by removing local files
### Third-Party Services:
- Google Trends (for search queries)
- Bing Search (for automation)
- Discord/NTFY (optional, for notifications)
### Your Responsibility:
- Protect your `accounts.json` file
- Use strong passwords
- Enable 2FA where possible
- Don't share your configuration publicly
---
## 🌍 Geographic Restrictions
Microsoft Rewards availability and terms vary by region:
- Available in select countries only
- Region-specific earning rates
- Local laws may apply
- Check your local regulations
**By using this software, you confirm:**
1. Microsoft Rewards is available in your region
2. You understand the risks of automation
3. You accept full responsibility for your actions
4. You will not use this for commercial purposes
---
## 📞 Contact & Reporting
**Questions about licensing?**
Open an issue at: https://github.com/TheNetsky/Microsoft-Rewards-Script/issues
**Found a security issue?**
See: SECURITY.md
**General discussion?**
Join Discord: https://discord.gg/KRBFxxsU
---
**Last Updated:** October 2025
**Applies to:** Microsoft Rewards Script V2.1.5 and later
**BY USING THIS SOFTWARE, YOU ACKNOWLEDGE THAT YOU HAVE READ AND UNDERSTOOD THESE NOTICES.**

456
README.md
View File

@@ -2,208 +2,378 @@
# Quick Setup (Recommended)
**Easiest way to get started — download and run:**
1. Clone this repository or download the latest release ZIP.
2. Run the setup script:
1. **Clone the branch** or download the zip.
2. **Run the setup script:**
* **Windows:** double-click `setup/setup.bat` or run it from a command prompt
* **Windows:**
Double-click `setup/setup.bat`
* **Linux / macOS / WSL:**
```bash
bash setup/setup.sh
```
* **Alternative (any platform):**
```bash
npm run setup
```
3. **Follow the setup prompts.** The script will:
* Rename `accounts.example.json` → `accounts.json`
* Ask for Microsoft account credentials
3. Follow the prompts — the setup script will:
* Copy `accounts.example.json` → `accounts.json`
* Ask for your Microsoft account credentials
* Remind you to review `config.json`
* Install dependencies (`npm install`)
* Build the project (`npm run build`)
* Build (`npm run build`)
* Optionally start the script
**That's it — the setup script handles the rest.**
That's it — the setup script handles the rest.
---
# Advanced Setup Options
### Nix Users
1. Install Nix from [https://nixos.org/](https://nixos.org/)
2. Run:
If using Nix:
```bash
./run.sh
```
### Manual Setup (if setup script fails)
1. Copy `src/accounts.example.json` → `src/accounts.json` and add accounts.
2. Edit `src/config.json` as needed.
3. Install dependencies:
```bash
npm install
```
4. Build:
```bash
npm run build
```
5. Start:
```bash
npm run start
```
1. Copy:
```bash
cp src/accounts.example.json src/accounts.json
```
2. Edit `src/accounts.json` and `src/config.json`.
3. Install and build:
```bash
npm install
npm run build
npm run start
```
---
# Docker Setup (Experimental)
# Docker Setup (Recommended for Scheduling)
**Before starting**
## Before Starting
* Remove local `/node_modules` and `/dist` if previously built.
* Remove old Docker volumes if upgrading from older versions.
* You can reuse your old `accounts.json`.
* Remove local `/node_modules` and `/dist` if you previously built.
* Remove old Docker volumes when upgrading from v1.4 or earlier.
* You can reuse older `accounts.json`.
**Quick Docker (recommended for scheduling)**
1. Clone v2 and configure `accounts.json`.
2. Ensure `config.json` has `"headless": true`.
## Quick Start
1. Clone v2 and configure `accounts.json`
2. Ensure `config.json` has `"headless": true`
3. Edit `compose.yaml`:
* Set `TZ` (timezone)
* Set `CRON_SCHEDULE` (use crontab.guru for help)
* Set your timezone (`TZ`)
* Set cron schedule (`CRON_SCHEDULE`)
* Optional: `RUN_ON_START=true`
4. Start:
```bash
docker compose up -d
```
5. Monitor logs:
```bash
docker logs microsoft-rewards-script
```
```bash
docker compose up -d
The container randomly delays scheduled runs by approximately 550 minutes to appear more natural (configurable, see notes below).
## Example compose.yaml
```yaml
services:
microsoft-rewards-script:
image: ghcr.io/your-org/microsoft-rewards-script:latest
container_name: microsoft-rewards-script
restart: unless-stopped
# Mount your configuration and persistent session storage
volumes:
# Read-only config files from your working directory into the container
- ./src/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
- ./src/config.json:/usr/src/microsoft-rewards-script/dist/config.json:ro
# Persist browser sessions/fingerprints between runs
- ./sessions:/usr/src/microsoft-rewards-script/dist/sessions
# Optional: persist job state directory (if you set jobState.dir to a folder inside dist/)
# - ./jobstate:/usr/src/microsoft-rewards-script/dist/jobstate
environment:
# Timezone for scheduling
TZ: "Europe/Amsterdam"
# Node runtime
NODE_ENV: "production"
# Cron schedule for automatic runs (UTC inside container)
# Example: run at 07:00, 16:00, and 20:00 every day
CRON_SCHEDULE: "0 7,16,20 * * *"
# Run immediately on container start (in addition to CRON_SCHEDULE)
RUN_ON_START: "true"
# Randomize scheduled start-time between MIN..MAX minutes
# Comment these to use defaults (about 550 minutes)
# MIN_SLEEP_MINUTES: "5"
# MAX_SLEEP_MINUTES: "50"
# Optional: disable randomization entirely
# SKIP_RANDOM: "true"
# Optional: limit resources if desired
deploy:
resources:
limits:
cpus: "1.0"
memory: "1g"
```
5. Monitor:
```bash
docker logs microsoft-rewards-script
```
> The container randomly delays scheduled runs by ~550 minutes to appear more natural.
---
# Usage Notes
* **Headless=false cleanup:** If you stop the script without closing browser windows, use Task Manager / `npm run kill-chrome-win` to close leftover instances.
* **Scheduling advice:** Run at least twice daily. Use `"runOnZeroPoints": false` in config to skip runs with no points.
* **Multiple accounts:** Use `clusters` in `config.json` to run accounts in parallel.
### compose.yaml Notes
- `volumes`
- `accounts.json` and `config.json` are mounted read-only to avoid accidental in-container edits. Edit them on the host.
- `sessions` persists your login sessions and fingerprints across restarts and updates.
- If you enable `jobState.enabled` and set `jobState.dir`, consider mounting that path as a volume too.
- `CRON_SCHEDULE`
- Standard crontab format. Use a site like crontab.guru to generate expressions.
- The schedule is evaluated inside the container; ensure `TZ` matches your desired timezone.
- `RUN_ON_START`
- If `"true"`, the script runs once immediately when the container is started, then on the cron schedule.
- Randomization
- By default, a randomized delay prevents runs from happening at exactly the same time every day.
- You can tune it with `MIN_SLEEP_MINUTES` and `MAX_SLEEP_MINUTES`, or disable with `SKIP_RANDOM`.
---
# Configuration Reference
Edit `src/config.json` to customize behavior.
Edit `src/config.json` to customize the bots behavior.
### Core Settings (examples)
## Core
| Setting | Description | Default |
| ----------------- | -------------------------------: | -------------------------: |
| `baseURL` | Microsoft Rewards URL | `https://rewards.bing.com` |
| `sessionPath` | Session/fingerprint storage | `sessions` |
| `headless` | Run browser in background | `false` |
| `parallel` | Run mobile/desktop tasks at once | `true` |
| `runOnZeroPoints` | Run when no points available | `false` |
| `clusters` | Concurrent account instances | `1` |
### Fingerprint Settings
| Setting | Description | Default |
| ------------------------- | ------------------------: | ------: |
| `saveFingerprint.mobile` | Reuse mobile fingerprint | `false` |
| `saveFingerprint.desktop` | Reuse desktop fingerprint | `false` |
### Task Settings (important ones)
| Setting | Description | Default |
| -------------------------- | -----------------: | ------: |
| `workers.doDailySet` | Do daily set | `true` |
| `workers.doMorePromotions` | Promotional offers | `true` |
| `workers.doPunchCards` | Punchcard tasks | `true` |
| `workers.doDesktopSearch` | Desktop searches | `true` |
| `workers.doMobileSearch` | Mobile searches | `true` |
| `workers.doDailyCheckIn` | Daily check-in | `true` |
| `workers.doReadToEarn` | Read-to-earn | `true` |
### Search Settings
| Setting | Description | Default |
| ---------------------------------------- | ---------------------: | ------------: |
| `searchOnBingLocalQueries` | Use local queries | `false` |
| `searchSettings.useGeoLocaleQueries` | Geo-based queries | `false` |
| `searchSettings.scrollRandomResults` | Random scrolling | `true` |
| `searchSettings.clickRandomResults` | Random link clicks | `true` |
| `searchSettings.searchDelay` | Delay between searches | `3-5 minutes` |
| `searchSettings.retryMobileSearchAmount` | Mobile retry attempts | `2` |
### Advanced Settings
| Setting | Description | Default |
| ------------------------- | --------------------------: | ------------------: |
| `globalTimeout` | Action timeout | `30s` |
| `logExcludeFunc` | Exclude functions from logs | `SEARCH-CLOSE-TABS` |
| `proxy.proxyGoogleTrends` | Proxy Google Trends | `true` |
| `proxy.proxyBingTerms` | Proxy Bing Terms | `true` |
### Webhook Settings
| Setting | Description | Default |
| --------------------------- | ---------------------------: | ------: |
| `webhook.enabled` | Enable Discord notifications | `false` |
| `webhook.url` | Discord webhook URL | `null` |
| `conclusionWebhook.enabled` | Summary-only webhook | `false` |
| `conclusionWebhook.url` | Summary webhook URL | `null` |
|----------|-------------|----------|
| `baseURL` | Microsoft Rewards base URL | `https://rewards.bing.com` |
| `sessionPath` | Folder to store browser sessions | `sessions` |
| `dryRun` | Simulate without running tasks | `false` |
---
# Features
## Browser
**Account & Session**
| Setting | Description | Default |
|----------|-------------|----------|
| `browser.headless` | Run browser invisibly | `false` |
| `browser.globalTimeout` | Timeout for actions | `"30s"` |
* Multi-account support
* Persistent sessions & fingerprints
* 2FA support & passwordless options
---
**Automation**
## Fingerprinting
* Headless operation & clustering
* Selectable task sets
* Proxy support & scheduling (Docker)
| Setting | Description | Default |
|----------|-------------|----------|
| `fingerprinting.saveFingerprint.mobile` | Reuse mobile fingerprint | `true` |
| `fingerprinting.saveFingerprint.desktop` | Reuse desktop fingerprint | `true` |
**Search & Rewards**
---
* Desktop & mobile searches
* Emulated browsing, scrolling, clicks
* Daily sets, promotions, punchcards, quizzes
## Execution
**Interactions**
| Setting | Description | Default |
|----------|-------------|----------|
| `execution.parallel` | Run desktop and mobile at once | `false` |
| `execution.runOnZeroPoints` | Run even with no points | `false` |
| `execution.clusters` | Concurrent account clusters | `1` |
* Quiz solving (10 & 3040 point variants)
* Polls, ABC quizzes, “This or That” answers
---
**Notifications**
## Job State
* Discord webhooks and summary webhooks
* Extensive logs for debugging
| Setting | Description | Default |
|----------|-------------|----------|
| `jobState.enabled` | Save last job state | `true` |
| `jobState.dir` | Directory for job data | `""` |
---
## Workers (Tasks)
| Setting | Description | Default |
|----------|-------------|----------|
| `doDailySet` | Complete daily set | `true` |
| `doMorePromotions` | Complete more promotions | `true` |
| `doPunchCards` | Complete punchcards | `true` |
| `doDesktopSearch` | Perform desktop searches | `true` |
| `doMobileSearch` | Perform mobile searches | `true` |
| `doDailyCheckIn` | Complete daily check-in | `true` |
| `doReadToEarn` | Complete Read-to-Earn | `true` |
| `bundleDailySetWithSearch` | Combine daily set and searches | `true` |
---
## Search
| Setting | Description | Default |
|----------|-------------|----------|
| `search.useLocalQueries` | Use local query list | `true` |
| `search.settings.useGeoLocaleQueries` | Use region-based queries | `true` |
| `search.settings.scrollRandomResults` | Random scrolling | `true` |
| `search.settings.clickRandomResults` | Random link clicking | `true` |
| `search.settings.retryMobileSearchAmount` | Retry mobile searches | `2` |
| `search.settings.delay.min` | Minimum delay between searches | `1min` |
| `search.settings.delay.max` | Maximum delay between searches | `5min` |
---
## Query Diversity
| Setting | Description | Default |
|----------|-------------|----------|
| `queryDiversity.enabled` | Enable multiple query sources | `true` |
| `queryDiversity.sources` | Query providers | `["google-trends", "reddit", "local-fallback"]` |
| `queryDiversity.maxQueriesPerSource` | Limit per source | `10` |
| `queryDiversity.cacheMinutes` | Cache lifetime | `30` |
---
## Humanization
| Setting | Description | Default |
|----------|-------------|----------|
| `humanization.enabled` | Enable human behavior | `true` |
| `stopOnBan` | Stop immediately on ban | `true` |
| `immediateBanAlert` | Alert instantly if banned | `true` |
| `actionDelay.min` | Minimum delay per action (ms) | `500` |
| `actionDelay.max` | Maximum delay per action (ms) | `2200` |
| `gestureMoveProb` | Chance of random mouse movement | `0.65` |
| `gestureScrollProb` | Chance of random scrolls | `0.4` |
---
## Vacation Mode
| Setting | Description | Default |
|----------|-------------|----------|
| `vacation.enabled` | Enable random pauses | `true` |
| `minDays` | Minimum days off | `2` |
| `maxDays` | Maximum days off | `4` |
---
## Risk Management
| Setting | Description | Default |
|----------|-------------|----------|
| `enabled` | Enable risk-based adjustments | `true` |
| `autoAdjustDelays` | Adapt delays dynamically | `true` |
| `stopOnCritical` | Stop on critical warning | `false` |
| `banPrediction` | Predict bans based on signals | `true` |
| `riskThreshold` | Risk tolerance level | `75` |
---
## Retry Policy
| Setting | Description | Default |
|----------|-------------|----------|
| `maxAttempts` | Maximum retry attempts | `3` |
| `baseDelay` | Initial retry delay | `1000` |
| `maxDelay` | Maximum retry delay | `30s` |
| `multiplier` | Backoff multiplier | `2` |
| `jitter` | Random jitter factor | `0.2` |
---
## Proxy
| Setting | Description | Default |
|----------|-------------|----------|
| `proxy.proxyGoogleTrends` | Proxy Google Trends | `true` |
| `proxy.proxyBingTerms` | Proxy Bing Terms | `true` |
---
## Notifications
| Setting | Description | Default |
|----------|-------------|----------|
| `notifications.webhook.enabled` | Enable Discord webhook | `false` |
| `notifications.webhook.url` | Discord webhook URL | `""` |
| `notifications.conclusionWebhook.enabled` | Enable summary webhook | `false` |
| `notifications.conclusionWebhook.url` | Summary webhook URL | `""` |
| `notifications.ntfy.enabled` | Enable Ntfy push alerts | `false` |
| `notifications.ntfy.url` | Ntfy server URL | `""` |
| `notifications.ntfy.topic` | Ntfy topic name | `"rewards"` |
---
## Logging
| Setting | Description | Default |
|----------|-------------|----------|
| `excludeFunc` | Exclude from console logs | `["SEARCH-CLOSE-TABS", "LOGIN-NO-PROMPT", "FLOW"]` |
| `webhookExcludeFunc` | Exclude from webhook logs | `["SEARCH-CLOSE-TABS", "LOGIN-NO-PROMPT", "FLOW"]` |
| `redactEmails` | Hide emails in logs | `true` |
---
# Account Configuration
Edit `src/accounts.json`:
```json
{
"accounts": [
{
"enabled": true,
"email": "email_1@outlook.com",
"password": "password_1",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
},
{
"enabled": false,
"email": "email_2@outlook.com",
"password": "password_2",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
}
]
}
```
---
# Features Overview
- Multi-account and session handling
- Persistent browser fingerprints
- Parallel task execution
- Proxy and retry support
- Human-like delays and scrolling
- Full daily set automation
- Mobile and desktop search support
- Vacation and risk protection
- Webhook and Ntfy notifications
- Docker scheduling support
---
# Disclaimer
**Use at your own risk.** Automation may cause suspension or banning of Microsoft Rewards accounts. This project is provided for educational purposes only. The maintainers are **not** responsible for account actions taken by Microsoft.
Use at your own risk.
Automation of Microsoft Rewards may lead to account suspension or bans.
This software is provided for educational purposes only.
The authors are not responsible for any actions taken by Microsoft.

View File

@@ -6,8 +6,8 @@ services:
# Volume mounts: Specify a location where you want to save the files on your local machine.
volumes:
- ./src/accounts.jsonc:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
- ./src/config.jsonc:/usr/src/microsoft-rewards-script/dist/config.json:ro
- ./src/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
- ./src/config.json:/usr/src/microsoft-rewards-script/dist/config.json:ro
- ./sessions:/usr/src/microsoft-rewards-script/dist/browser/sessions # Optional, saves your login session
environment:

View File

@@ -24,7 +24,7 @@
playwright-test
# fixes "waiting until load" issue compared to
# setting headless in config.jsonc
# setting headless in config.json
xvfb-run
];

136
package-lock.json generated
View File

@@ -1,20 +1,20 @@
{
"name": "microsoft-rewards-script",
"version": "2.4.0",
"version": "2.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "microsoft-rewards-script",
"version": "2.4.0",
"version": "2.4.1",
"license": "ISC",
"dependencies": {
"axios": "^1.8.4",
"axios": "^1.13.2",
"chalk": "^4.1.2",
"cheerio": "^1.0.0",
"cron-parser": "^4.9.0",
"fingerprint-generator": "^2.1.66",
"fingerprint-injector": "^2.1.66",
"cron-parser": "^5.4.0",
"fingerprint-generator": "^2.1.76",
"fingerprint-injector": "^2.1.76",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"luxon": "^3.5.0",
@@ -316,6 +316,7 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
@@ -584,6 +585,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"license": "MIT",
"engines": {
"node": ">=12.0"
}
@@ -661,9 +663,10 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@@ -677,9 +680,10 @@
"dev": true
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz",
"integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==",
"version": "2.8.25",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz",
"integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
@@ -711,9 +715,9 @@
}
},
"node_modules/browserslist": {
"version": "4.26.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
"integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
"integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
"funding": [
{
"type": "opencollective",
@@ -728,12 +732,13 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
"electron-to-chromium": "^1.5.218",
"node-releases": "^2.0.21",
"update-browserslist-db": "^1.1.3"
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
"electron-to-chromium": "^1.5.238",
"node-releases": "^2.0.26",
"update-browserslist-db": "^1.1.4"
},
"bin": {
"browserslist": "cli.js"
@@ -763,9 +768,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001746",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz",
"integrity": "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==",
"version": "1.0.30001754",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
"integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
"funding": [
{
"type": "opencollective",
@@ -779,7 +784,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/chalk": {
"version": "4.1.2",
@@ -875,15 +881,15 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
"node_modules/cron-parser": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.4.0.tgz",
"integrity": "sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA==",
"license": "MIT",
"dependencies": {
"luxon": "^3.2.1"
"luxon": "^3.7.1"
},
"engines": {
"node": ">=12.0.0"
"node": ">=18"
}
},
"node_modules/cross-spawn": {
@@ -1043,6 +1049,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
"license": "MIT",
"dependencies": {
"is-obj": "^2.0.0"
},
@@ -1073,9 +1080,10 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.5.227",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz",
"integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA=="
"version": "1.5.249",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.249.tgz",
"integrity": "sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg==",
"license": "ISC"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@@ -1151,6 +1159,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -1440,12 +1449,13 @@
}
},
"node_modules/fingerprint-generator": {
"version": "2.1.74",
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.74.tgz",
"integrity": "sha512-GWaUwAiy0hn1EjEG0/aQbjTTISdPN94hWwXwb5riVgvli4+XotLtkIr8aEKBYWb3FcqyW3AbSYgn5OAtPKWLSw==",
"version": "2.1.76",
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.76.tgz",
"integrity": "sha512-nynXZnqCBtBbEgqqdHS5mGm+R9JRRJxNun+lpZlCxGVt0BzgQJGibOvYCe5I54hIIVsaTldZ+jOb4btRPfPD6g==",
"license": "Apache-2.0",
"dependencies": {
"generative-bayesian-network": "^2.1.74",
"header-generator": "^2.1.74",
"generative-bayesian-network": "^2.1.76",
"header-generator": "^2.1.76",
"tslib": "^2.4.0"
},
"engines": {
@@ -1453,11 +1463,12 @@
}
},
"node_modules/fingerprint-injector": {
"version": "2.1.74",
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.74.tgz",
"integrity": "sha512-lTeueOgY0TIT8XGzqFI7IY9pR6wHRxdn3t3PN8g6YNA9ga9A0hc9Ig5xXmi+PFsn/+U4TTtkpxfrWYxYKFGW8g==",
"version": "2.1.76",
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.76.tgz",
"integrity": "sha512-ySlMhYmj7D5ND92BHd/46b6g+izGkj1Vm8qTjSORC3o7HbdFr6eQz8JuL8pVmiJIsC2tsd2ohVvu584HRKurNQ==",
"license": "Apache-2.0",
"dependencies": {
"fingerprint-generator": "^2.1.74",
"fingerprint-generator": "^2.1.76",
"tslib": "^2.4.0"
},
"engines": {
@@ -1633,9 +1644,10 @@
}
},
"node_modules/generative-bayesian-network": {
"version": "2.1.74",
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.74.tgz",
"integrity": "sha512-AlEQVo1I6FWVObaF8SfPP+qssCv3Heu1Bdu1THMIkB5h4sP9E0AR9zo8qVL936IcdC3tckA4A/fJWhPia7GXgg==",
"version": "2.1.76",
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.76.tgz",
"integrity": "sha512-e9BByo5UEXPsrOii4RM94a02y1JXhP5XZKbzC5GWDz62Bbh2jWbrkY0ta2cF1rxrv8pqLu4c98yQC2F50Eqa7A==",
"license": "Apache-2.0",
"dependencies": {
"adm-zip": "^0.5.9",
"tslib": "^2.4.0"
@@ -1823,12 +1835,13 @@
}
},
"node_modules/header-generator": {
"version": "2.1.74",
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.74.tgz",
"integrity": "sha512-0t4mRzBf3mtbwU+Clwm6sDvwEvOM5TIvCicFkYM5z48oR9DGLFPnVNKKOSJIDiJG08r1mY30ovI97JybJ6/Fug==",
"version": "2.1.76",
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.76.tgz",
"integrity": "sha512-Lqk4zU/MIHkm29Sfle6E3Jo2gUoscoG9x12jDt1RbH3kRq/RN+NRSoRRYggmkI0GQSS0wiOIfWwjgIRrA9nHqA==",
"license": "Apache-2.0",
"dependencies": {
"browserslist": "^4.21.1",
"generative-bayesian-network": "^2.1.74",
"generative-bayesian-network": "^2.1.76",
"ow": "^0.28.1",
"tslib": "^2.4.0"
},
@@ -1869,6 +1882,7 @@
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
@@ -2002,6 +2016,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
@@ -2107,7 +2122,8 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -2213,7 +2229,8 @@
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/natural-compare": {
"version": "1.4.0",
@@ -2222,9 +2239,10 @@
"dev": true
},
"node_modules/node-releases": {
"version": "2.0.21",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
"integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"license": "MIT"
},
"node_modules/nth-check": {
"version": "2.1.1",
@@ -2267,6 +2285,7 @@
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz",
"integrity": "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^4.2.0",
"callsites": "^3.1.0",
@@ -2429,7 +2448,8 @@
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -2935,9 +2955,9 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
"funding": [
{
"type": "opencollective",
@@ -2952,6 +2972,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
@@ -2981,6 +3002,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
"integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "microsoft-rewards-script",
"version": "2.4.0",
"version": "2.4.1",
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
"private": true,
"main": "index.js",
@@ -25,7 +25,6 @@
"dev": "ts-node ./src/index.ts -dev",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"prepare": "npm run build",
"setup": "node ./setup/update/setup.mjs",
"kill-chrome-win": "powershell -Command \"Get-Process | Where-Object { $_.MainModule.FileVersionInfo.FileDescription -eq 'Google Chrome for Testing' } | ForEach-Object { Stop-Process -Id $_.Id -Force }\"",
"create-docker": "docker build -t microsoft-rewards-script-docker ."
},
@@ -39,20 +38,10 @@
"Cheerio"
],
"author": "Netsky",
"contributors": [
"TheNetsky (https://github.com/TheNetsky)",
"LightZirconite (https://github.com/LightZirconite)",
"Mgrimace (https://github.com/mgrimace)",
"hmcdat (https://github.com/hmcdat)"
],
"license": "ISC",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/TheNetsky"
},
"devDependencies": {
"@types/node": "^20.14.11",
"@types/ms": "^0.7.34",
"@types/node": "^20.14.11",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"eslint": "^8.57.0",
"eslint-plugin-modules-newline": "^0.0.6",
@@ -60,16 +49,16 @@
"typescript": "^5.5.4"
},
"dependencies": {
"axios": "^1.8.4",
"cron-parser": "^4.9.0",
"axios": "^1.13.2",
"chalk": "^4.1.2",
"cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.66",
"fingerprint-injector": "^2.1.66",
"cron-parser": "^5.4.0",
"fingerprint-generator": "^2.1.76",
"fingerprint-injector": "^2.1.76",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"ms": "^2.1.3",
"luxon": "^3.5.0",
"ms": "^2.1.3",
"playwright": "1.52.0",
"rebrowser-playwright": "1.52.0",
"socks-proxy-agent": "^8.0.5",

View File

@@ -1,214 +0,0 @@
#!/usr/bin/env node
/**
* Unified cross-platform setup script for Microsoft Rewards Script V2.
*
* Features:
* - Renames accounts.example.jsonc -> accounts.json (idempotent)
* - Guides user through account configuration (email, password, TOTP, proxy)
* - Explains config.jsonc structure and key settings
* - Installs dependencies (npm install)
* - Builds TypeScript project (npm run build)
* - Installs Playwright Chromium browser (idempotent with marker)
* - Optional immediate start or manual start instructions
*
* V2 Updates:
* - Enhanced prompts for new config.jsonc structure
* - Explains humanization, scheduling, notifications
* - References updated documentation (docs/config.md, docs/accounts.md)
* - Improved user guidance for first-time setup
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { spawn } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Project root = two levels up from setup/update directory
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
function log(msg) { console.log(msg); }
function warn(msg) { console.warn(msg); }
function error(msg) { console.error(msg); }
function renameAccountsIfNeeded() {
const accounts = path.join(SRC_DIR, 'accounts.json');
const example = path.join(SRC_DIR, 'accounts.example.jsonc');
if (fs.existsSync(accounts)) {
log('accounts.json already exists - skipping rename.');
return;
}
if (fs.existsSync(example)) {
log('Renaming accounts.example.jsonc to accounts.json...');
fs.renameSync(example, accounts);
} else {
warn('Neither accounts.json nor accounts.example.jsonc found.');
}
}
async function prompt(question) {
return await new Promise(resolve => {
process.stdout.write(question);
const onData = (data) => {
const ans = data.toString().trim();
process.stdin.off('data', onData);
resolve(ans);
};
process.stdin.on('data', onData);
});
}
async function loopForAccountsConfirmation() {
log('\n📝 Please configure your Microsoft accounts:');
log(' - Open: src/accounts.json');
log(' - Add your email and password for each account');
log(' - Optional: Add TOTP secret for 2FA (see docs/accounts.md)');
log(' - Optional: Configure proxy settings per account');
log(' - Save the file (Ctrl+S or Cmd+S)\n');
// Keep asking until user says yes
for (;;) {
const ans = (await prompt('Have you configured your accounts in accounts.json? (yes/no): ')).toLowerCase();
if (['yes', 'y'].includes(ans)) break;
if (['no', 'n'].includes(ans)) {
log('Please configure accounts.json and save the file, then answer yes.');
continue;
}
log('Please answer yes or no.');
}
}
function runCommand(cmd, args, opts = {}) {
return new Promise((resolve, reject) => {
log(`Running: ${cmd} ${args.join(' ')}`);
const child = spawn(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', ...opts });
child.on('exit', (code) => {
if (code === 0) return resolve();
reject(new Error(`${cmd} exited with code ${code}`));
});
});
}
async function ensureNpmAvailable() {
try {
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['-v']);
} catch (e) {
throw new Error('npm not found in PATH. Install Node.js first.');
}
}
async function startOnly() {
log('Starting program (npm run start)...');
await ensureNpmAvailable();
// Assume user already installed & built; if dist missing inform user.
const distIndex = path.join(PROJECT_ROOT, 'dist', 'index.js');
if (!fs.existsSync(distIndex)) {
warn('Build output not found. Running build first.');
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']);
await installPlaywrightBrowsers();
} else {
// Even if build exists, ensure browsers are installed once.
await installPlaywrightBrowsers();
}
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'start']);
}
async function fullSetup() {
renameAccountsIfNeeded();
await loopForAccountsConfirmation();
log('\n⚙ Configuration Options (src/config.jsonc):');
log(' - browser.headless: Set to true for background operation');
log(' - execution.clusters: Number of parallel account processes');
log(' - workers: Enable/disable specific tasks (dailySet, searches, etc.)');
log(' - humanization: Add natural delays and behavior (recommended: enabled)');
log(' - schedule: Configure automated daily runs');
log(' - notifications: Discord webhooks, NTFY push alerts');
log(' 📚 Full guide: docs/config.md\n');
const reviewConfig = (await prompt('Do you want to review config.jsonc now? (yes/no): ')).toLowerCase();
if (['yes', 'y'].includes(reviewConfig)) {
log('⏸️ Setup paused. Please review src/config.jsonc, then re-run this setup.');
log(' Common settings to check:');
log(' - browser.headless (false = visible browser, true = background)');
log(' - execution.runOnZeroPoints (false = skip when no points available)');
log(' - humanization.enabled (true = natural behavior, recommended)');
log(' - schedule.enabled (false = manual runs, true = automated scheduling)');
log('\n After editing config.jsonc, run: npm run setup');
process.exit(0);
}
await ensureNpmAvailable();
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install']);
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']);
await installPlaywrightBrowsers();
log('\n✅ Setup complete!');
log(' - Accounts configured: src/accounts.json');
log(' - Configuration: src/config.jsonc');
log(' - Documentation: docs/index.md\n');
const start = (await prompt('Do you want to start the automation now? (yes/no): ')).toLowerCase();
if (['yes', 'y'].includes(start)) {
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'start']);
} else {
log('\nFinished setup. To start later, run: npm start');
log('For automated scheduling, run: npm run start:schedule');
}
}
async function installPlaywrightBrowsers() {
const PLAYWRIGHT_MARKER = path.join(PROJECT_ROOT, '.playwright-chromium-installed');
// Idempotent: skip if marker exists
if (fs.existsSync(PLAYWRIGHT_MARKER)) {
log('Playwright chromium already installed (marker found).');
return;
}
log('Ensuring Playwright chromium browser is installed...');
try {
await runCommand(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['playwright', 'install', 'chromium']);
fs.writeFileSync(PLAYWRIGHT_MARKER, new Date().toISOString());
log('Playwright chromium install complete.');
} catch (e) {
warn('Failed to install Playwright chromium automatically. You can manually run: npx playwright install chromium');
}
}
async function main() {
if (!fs.existsSync(SRC_DIR)) {
error('[ERROR] Cannot find src directory at ' + SRC_DIR);
process.exit(1);
}
process.chdir(PROJECT_ROOT);
for (;;) {
log('============================');
log(' Microsoft Rewards Setup ');
log('============================');
log('Select an option:');
log(' 1) Start program now (skip setup)');
log(' 2) Full first-time setup');
log(' 3) Exit');
const choice = (await prompt('Enter choice (1/2/3): ')).trim();
if (choice === '1') { await startOnly(); break; }
if (choice === '2') { await fullSetup(); break; }
if (choice === '3') { log('Exiting.'); process.exit(0); }
log('\nInvalid choice. Please select 1, 2 or 3.\n');
}
// After completing action, optionally pause if launched by double click on Windows (no TTY detection simple heuristic)
if (process.platform === 'win32' && process.stdin.isTTY) {
log('\nDone. Press Enter to close.');
await prompt('');
}
process.exit(0);
}
// Allow clean Ctrl+C
process.on('SIGINT', () => { console.log('\nInterrupted.'); process.exit(1); });
main().catch(err => {
error('\nSetup failed: ' + err.message);
process.exit(1);
});

View File

@@ -1,225 +0,0 @@
/* eslint-disable linebreak-style */
/**
* Smart Auto-Update Script
*
* Intelligently updates while preserving user settings:
* - ALWAYS updates code files (*.ts, *.js, etc.)
* - ONLY updates config.jsonc if remote has changes to it
* - ONLY updates accounts.json if remote has changes to it
* - KEEPS user passwords/emails/settings otherwise
*
* Usage:
* node setup/update/update.mjs --git
* node setup/update/update.mjs --docker
*/
import { spawn, execSync } from 'node:child_process'
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
import { join } from 'node:path'
function run(cmd, args, opts = {}) {
return new Promise((resolve) => {
const child = spawn(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', ...opts })
child.on('close', (code) => resolve(code ?? 0))
child.on('error', () => resolve(1))
})
}
async function which(cmd) {
const probe = process.platform === 'win32' ? 'where' : 'which'
const code = await run(probe, [cmd], { stdio: 'ignore' })
return code === 0
}
function exec(cmd) {
try {
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
} catch {
return null
}
}
async function updateGit() {
const hasGit = await which('git')
if (!hasGit) {
console.log('Git not found. Skipping update.')
return 1
}
console.log('\n' + '='.repeat(60))
console.log('Smart Git Update')
console.log('='.repeat(60))
// Step 1: Read config to get user preferences
let userConfig = { autoUpdateConfig: false, autoUpdateAccounts: false }
try {
if (existsSync('src/config.jsonc')) {
const configContent = readFileSync('src/config.jsonc', 'utf8')
.replace(/\/\/.*$/gm, '') // remove comments
.replace(/\/\*[\s\S]*?\*\//g, '') // remove multi-line comments
const config = JSON.parse(configContent)
if (config.update) {
userConfig.autoUpdateConfig = config.update.autoUpdateConfig ?? false
userConfig.autoUpdateAccounts = config.update.autoUpdateAccounts ?? false
}
}
} catch (e) {
console.log('Warning: Could not read config.jsonc, using defaults (preserve local files)')
}
console.log('\nUser preferences:')
console.log(` Auto-update config.jsonc: ${userConfig.autoUpdateConfig}`)
console.log(` Auto-update accounts.json: ${userConfig.autoUpdateAccounts}`)
// Step 2: Fetch
console.log('\nFetching latest changes...')
await run('git', ['fetch', '--all', '--prune'])
// Step 3: Get current branch
const currentBranch = exec('git branch --show-current')
if (!currentBranch) {
console.log('Could not determine current branch.')
return 1
}
// Step 4: Check which files changed in remote
const remoteBranch = `origin/${currentBranch}`
const filesChanged = exec(`git diff --name-only HEAD ${remoteBranch}`)
if (!filesChanged) {
console.log('Already up to date!')
return 0
}
const changedFiles = filesChanged.split('\n').filter(f => f.trim())
const configChanged = changedFiles.includes('src/config.jsonc')
const accountsChanged = changedFiles.includes('src/accounts.json')
// Step 5: ALWAYS backup config and accounts (smart strategy!)
const backupDir = join(process.cwd(), '.update-backup')
mkdirSync(backupDir, { recursive: true })
const filesToRestore = []
if (existsSync('src/config.jsonc')) {
console.log('\nBacking up config.jsonc...')
writeFileSync(join(backupDir, 'config.jsonc'), readFileSync('src/config.jsonc', 'utf8'))
// Restore if: remote changed it AND user doesn't want auto-update
if (configChanged && !userConfig.autoUpdateConfig) {
filesToRestore.push('config.jsonc')
}
}
if (existsSync('src/accounts.json')) {
console.log('Backing up accounts.json...')
writeFileSync(join(backupDir, 'accounts.json'), readFileSync('src/accounts.json', 'utf8'))
// Restore if: remote changed it AND user doesn't want auto-update
if (accountsChanged && !userConfig.autoUpdateAccounts) {
filesToRestore.push('accounts.json')
}
}
// Show what will happen
console.log('\nRemote changes:')
if (configChanged) {
console.log(` config.jsonc: ${userConfig.autoUpdateConfig ? 'WILL UPDATE' : 'KEEPING LOCAL (restoring from backup)'}`)
} else {
console.log(' config.jsonc: no changes in remote')
}
if (accountsChanged) {
console.log(` accounts.json: ${userConfig.autoUpdateAccounts ? 'WILL UPDATE' : 'KEEPING LOCAL (restoring from backup)'}`)
} else {
console.log(' accounts.json: no changes in remote')
}
// Step 6: Stash changes
const hasChanges = exec('git status --porcelain')
if (hasChanges) {
console.log('\nStashing local changes...')
await run('git', ['stash', 'push', '-m', 'Auto-update backup'])
}
// Step 7: Pull
console.log('\nPulling latest code...')
const pullCode = await run('git', ['pull', '--rebase'])
if (pullCode !== 0) {
console.log('Pull failed.')
if (hasChanges) await run('git', ['stash', 'pop'])
return pullCode
}
// Step 8: Restore files based on user preferences
if (filesToRestore.length > 0) {
console.log('\nRestoring local files (per your config preferences)...')
for (const file of filesToRestore) {
const content = readFileSync(join(backupDir, file), 'utf8')
writeFileSync(join('src', file), content)
console.log(` ✓ Restored ${file}`)
}
}
// Step 9: Restore stash
if (hasChanges) {
await run('git', ['stash', 'pop'])
}
// Step 9: Install & build
const hasNpm = await which('npm')
if (!hasNpm) return 0
console.log('\nInstalling dependencies...')
await run('npm', ['ci'])
console.log('\nBuilding project...')
const buildCode = await run('npm', ['run', 'build'])
console.log('\n' + '='.repeat(60))
console.log('Update completed!')
console.log('='.repeat(60))
// Optional: Show community info for advanced features
if (Math.random() > 0.7) {
console.log('\n💡 Tip: Looking for faster updates and advanced features?')
console.log(' Check out enhanced builds with better anti-detection.')
console.log(' Community: https://discord.gg/kn3695Kx32')
}
console.log('')
return buildCode
}
async function updateDocker() {
const hasDocker = await which('docker')
if (!hasDocker) return 1
// Prefer compose v2 (docker compose)
await run('docker', ['compose', 'pull'])
return run('docker', ['compose', 'up', '-d'])
}
async function main() {
const args = new Set(process.argv.slice(2))
const doGit = args.has('--git')
const doDocker = args.has('--docker')
let code = 0
if (doGit) {
code = await updateGit()
}
if (doDocker && code === 0) {
code = await updateDocker()
}
// Only exit if not called from scheduler
// When FROM_SCHEDULER=1, the parent script will handle process lifecycle
if (process.env.FROM_SCHEDULER !== '1') {
process.exit(code)
}
}
main().catch(() => {
// Only exit on error if not called from scheduler
if (process.env.FROM_SCHEDULER !== '1') {
process.exit(1)
}
})

32
src/accounts.example.json Normal file
View File

@@ -0,0 +1,32 @@
{
"accounts": [
{
"enabled": true,
"email": "email_1@outlook.com",
"password": "password_1",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
},
{
"enabled": false,
"email": "email_2@outlook.com",
"password": "password_2",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
}
]
}

View File

@@ -1,151 +0,0 @@
{
// ============================================================
// 📧 MICROSOFT ACCOUNTS CONFIGURATION
// ============================================================
// ⚠️ IMPORTANT SECURITY NOTICE
// This file contains sensitive credentials. Never commit the real accounts.jsonc to version control.
// The .gitignore is configured to exclude accounts.jsonc but you should verify it's not tracked.
// 📊 MICROSOFT ACCOUNT LIMITS (Unofficial Guidelines)
// - New accounts per IP per day: ~3 (official soft limit)
// - Recommended active accounts per household IP: ~5 (to avoid suspicion)
// - Creating too many accounts quickly may trigger verification (phone, OTP, captcha)
// - Unusual activity can result in temporary blocks or account restrictions
"accounts": [
{
// ============================================================
// 👤 ACCOUNT 1
// ============================================================
// Enable or disable this account (true = active, false = skip)
"enabled": true,
// Microsoft account email address
"email": "email_1@outlook.com",
// Account password
"password": "password_1",
// Two-Factor Authentication (2FA) TOTP secret (optional but HIGHLY recommended for security)
// Get this from your authenticator app (e.g., Microsoft Authenticator, Google Authenticator)
// Format: base32 secret key (e.g., "JBSWY3DPEHPK3PXP")
// Leave empty "" if 2FA is not enabled
"totp": "",
// ⚠️ REQUIRED: Recovery email address associated with this Microsoft account
// During login, Microsoft shows the first 2 characters and the domain of the recovery email (e.g., "ab***@example.com")
// This field is MANDATORY to detect account compromise or bans:
// - The script compares what Microsoft displays with this configured recovery email
// - If they don't match, it alerts you that the account may be compromised or the recovery email was changed
// - This security check helps identify hijacked accounts before they cause issues
// Format: Full recovery email address (e.g., "backup@gmail.com")
"recoveryEmail": "your_email@domain.com",
// ============================================================
// 🌐 PROXY CONFIGURATION (Optional)
// ============================================================
"proxy": {
// Enable proxy for HTTP requests (axios/API calls)
// If false, proxy is only used for browser automation
"proxyAxios": true,
// Proxy server URL (without protocol)
// Examples: "proxy.example.com", "123.45.67.89"
// Leave empty "" to disable proxy for this account
"url": "",
// Proxy port number
"port": 0,
// Proxy authentication username (leave empty if no auth required)
"username": "",
// Proxy authentication password (leave empty if no auth required)
"password": ""
}
},
{
// ============================================================
// 👤 ACCOUNT 2
// ============================================================
"enabled": false,
"email": "email_2@outlook.com",
"password": "password_2",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
},
{
// ============================================================
// 👤 ACCOUNT 3
// ============================================================
"enabled": false,
"email": "email_3@outlook.com",
"password": "password_3",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
},
{
// ============================================================
// 👤 ACCOUNT 4
// ============================================================
"enabled": false,
"email": "email_4@outlook.com",
"password": "password_4",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
},
{
// ============================================================
// 👤 ACCOUNT 5
// ============================================================
"enabled": false,
"email": "email_5@outlook.com",
"password": "password_5",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
}
]
}

View File

@@ -25,23 +25,14 @@ class Browser {
}
async createBrowser(proxy: AccountProxy, email: string): Promise<BrowserContext> {
// Optional automatic browser installation (set AUTO_INSTALL_BROWSERS=1)
if (process.env.AUTO_INSTALL_BROWSERS === '1') {
try {
// Dynamically import child_process to avoid overhead otherwise
const { execSync } = await import('child_process')
execSync('npx playwright install chromium', { stdio: 'ignore' })
} catch { /* silent */ }
}
let browser: import('rebrowser-playwright').Browser
let browser: playwright.Browser
try {
// FORCE_HEADLESS env takes precedence (used in Docker with headless shell only)
const envForceHeadless = process.env.FORCE_HEADLESS === '1'
// Support legacy config.headless OR nested config.browser.headless
const legacyHeadless = (this.bot.config as { headless?: boolean }).headless
const nestedHeadless = (this.bot.config.browser as { headless?: boolean } | undefined)?.headless
let headlessValue = envForceHeadless ? true : (legacyHeadless ?? nestedHeadless ?? false)
const headlessValue = envForceHeadless ? true : (legacyHeadless ?? nestedHeadless ?? false)
const headless: boolean = Boolean(headlessValue)
const engineName = 'chromium' // current hard-coded engine
@@ -64,7 +55,7 @@ class Browser {
const msg = (e instanceof Error ? e.message : String(e))
// Common missing browser executable guidance
if (/Executable doesn't exist/i.test(msg)) {
this.bot.log(this.bot.isMobile, 'BROWSER', 'Chromium not installed for Playwright. Run "npm run pre-build" to install all dependencies (or set AUTO_INSTALL_BROWSERS=1 to auto-attempt).', 'error')
this.bot.log(this.bot.isMobile, 'BROWSER', 'Chromium not installed for Playwright. Run "npm run pre-build" to install all dependencies', 'error')
} else {
this.bot.log(this.bot.isMobile, 'BROWSER', 'Failed to launch browser: ' + msg, 'error')
}

View File

@@ -25,80 +25,134 @@ export default class BrowserFunc {
* @param {Page} page Playwright page
*/
async goHome(page: Page) {
const navigateHome = async () => {
try {
await page.goto(this.bot.config.baseURL, {
waitUntil: 'domcontentloaded',
timeout: 30000
})
} catch (e: any) {
if (typeof e?.message === 'string' && e.message.includes('ERR_ABORTED')) {
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Navigation aborted, retrying...', 'warn')
await this.bot.utils.wait(1500)
await page.goto(this.bot.config.baseURL, {
waitUntil: 'domcontentloaded',
timeout: 30000
})
} else {
throw e
}
}
}
try {
const dashboardURL = new URL(this.bot.config.baseURL)
if (page.url() === dashboardURL.href) {
return
if (new URL(page.url()).hostname !== dashboardURL.hostname) {
await navigateHome()
}
await page.goto(this.bot.config.baseURL)
let success = false
for (let iteration = 1; iteration <= RETRY_LIMITS.GO_HOME_MAX; iteration++) {
await this.bot.utils.wait(TIMEOUTS.LONG)
await this.bot.browser.utils.tryDismissAllMessages(page)
try {
// If activities are found, exit the loop (SUCCESS - account is OK)
await page.waitForSelector(SELECTORS.MORE_ACTIVITIES, { timeout: 1000 })
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Visited homepage successfully')
success = true
break
} catch (error) {
// Activities not found yet - check if it's because account is suspended
// Only check suspension if we can't find activities (reduces false positives)
const suspendedByHeader = await page.waitForSelector(SELECTORS.SUSPENDED_ACCOUNT, { state: 'visible', timeout: 500 }).then(() => true).catch(() => false)
} catch {
const suspendedByHeader = await page
.waitForSelector(SELECTORS.SUSPENDED_ACCOUNT, { state: 'visible', timeout: 500 })
.then(() => true)
.catch(() => false)
if (suspendedByHeader) {
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by header selector (iteration ${iteration})`, 'error')
this.bot.log(
this.bot.isMobile,
'GO-HOME',
`Account suspension detected by header selector (iteration ${iteration})`,
'error'
)
throw new Error('Account has been suspended!')
}
// Secondary check: look for suspension text in main content area only
try {
const mainContent = (await page.locator('#contentContainer, #main, .main-content').first().textContent({ timeout: 500 }).catch(() => '')) || ''
const mainContent =
(await page
.locator('#contentContainer, #main, .main-content')
.first()
.textContent({ timeout: 500 })
.catch(() => '')) || ''
const suspensionPatterns = [
/account\s+has\s+been\s+suspended/i,
/suspended\s+due\s+to\s+unusual\s+activity/i,
/your\s+account\s+is\s+temporarily\s+suspended/i
]
const isSuspended = suspensionPatterns.some(pattern => pattern.test(mainContent))
const isSuspended = suspensionPatterns.some((p) => p.test(mainContent))
if (isSuspended) {
this.bot.log(this.bot.isMobile, 'GO-HOME', `Account suspension detected by content text (iteration ${iteration})`, 'error')
this.bot.log(
this.bot.isMobile,
'GO-HOME',
`Account suspension detected by content text (iteration ${iteration})`,
'error'
)
throw new Error('Account has been suspended!')
}
} catch (e) {
// Ignore errors in text check - not critical
this.bot.log(this.bot.isMobile, 'GO-HOME', `Suspension text check skipped: ${e}`, 'warn')
this.bot.log(
this.bot.isMobile,
'GO-HOME',
`Suspension text check skipped: ${e instanceof Error ? e.message : String(e)}`,
'warn'
)
}
// Not suspended, just activities not loaded yet - continue to next iteration
this.bot.log(this.bot.isMobile, 'GO-HOME', `Activities not found yet (iteration ${iteration}/${RETRY_LIMITS.GO_HOME_MAX}), retrying...`, 'warn')
}
// Below runs if the homepage was unable to be visited
const currentURL = new URL(page.url())
if (currentURL.hostname !== dashboardURL.hostname) {
await this.bot.browser.utils.tryDismissAllMessages(page)
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
await page.goto(this.bot.config.baseURL)
try {
await navigateHome()
} catch (e: any) {
if (typeof e?.message === 'string' && e.message.includes('ERR_ABORTED')) {
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Navigation aborted again; continuing...', 'warn')
} else {
this.bot.log(this.bot.isMobile, 'GO-HOME', 'Visited homepage successfully')
break
throw e
}
}
} else {
this.bot.log(
this.bot.isMobile,
'GO-HOME',
`Activities not found yet (iteration ${iteration}/${RETRY_LIMITS.GO_HOME_MAX}), retrying...`,
'warn'
)
}
}
await this.bot.utils.wait(TIMEOUTS.VERY_LONG)
const backoff = Math.min(TIMEOUTS.VERY_LONG, 1000 + iteration * 500)
await this.bot.utils.wait(backoff)
}
if (!success) {
throw new Error('Failed to reach homepage or find activities within retry limit')
}
} catch (error) {
throw this.bot.log(this.bot.isMobile, 'GO-HOME', 'An error occurred:' + error, 'error')
throw this.bot.log(
this.bot.isMobile,
'GO-HOME',
'An error occurred:' + (error instanceof Error ? ` ${error.message}` : ` ${String(error)}`),
'error'
)
}
}
/**
* Fetch user dashboard data
* @returns {DashboardData} Object of user bing rewards dashboard data
@@ -131,7 +185,7 @@ export default class BrowserFunc {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn')
try {
await this.goHome(target)
} catch {/* ignore */}
} catch {/* ignore */ }
} else {
break
}
@@ -174,7 +228,7 @@ export default class BrowserFunc {
const scripts = Array.from(document.querySelectorAll('script'))
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
return targetScript?.innerText ? targetScript.innerText : null
}).catch(()=>null)
}).catch(() => null)
if (!retryContent) {
// Log additional debug info

121
src/config.json Normal file
View File

@@ -0,0 +1,121 @@
{
"baseURL": "https://rewards.bing.com",
"sessionPath": "sessions",
"dryRun": false,
"browser": {
"headless": false,
"globalTimeout": "30s"
},
"fingerprinting": {
"saveFingerprint": {
"mobile": true,
"desktop": true
}
},
"execution": {
"parallel": false,
"runOnZeroPoints": false,
"clusters": 1
},
"jobState": {
"enabled": true,
"dir": ""
},
"workers": {
"doDailySet": true,
"doMorePromotions": true,
"doPunchCards": true,
"doDesktopSearch": true,
"doMobileSearch": true,
"doDailyCheckIn": true,
"doReadToEarn": true,
"bundleDailySetWithSearch": true
},
"search": {
"useLocalQueries": true,
"settings": {
"useGeoLocaleQueries": true,
"scrollRandomResults": true,
"clickRandomResults": true,
"retryMobileSearchAmount": 2,
"delay": {
"min": "2min",
"max": "5min"
}
}
},
"queryDiversity": {
"enabled": true,
"sources": [
"google-trends",
"reddit",
"local-fallback"
],
"maxQueriesPerSource": 10,
"cacheMinutes": 30
},
"humanization": {
"enabled": true,
"stopOnBan": true,
"immediateBanAlert": true,
"actionDelay": {
"min": 500,
"max": 2200
},
"gestureMoveProb": 0.65,
"gestureScrollProb": 0.4,
"allowedWindows": []
},
"vacation": {
"enabled": true,
"minDays": 2,
"maxDays": 4
},
"riskManagement": {
"enabled": true,
"autoAdjustDelays": true,
"stopOnCritical": false,
"banPrediction": true,
"riskThreshold": 75
},
"retryPolicy": {
"maxAttempts": 3,
"baseDelay": 1000,
"maxDelay": "30s",
"multiplier": 2,
"jitter": 0.2
},
"proxy": {
"proxyGoogleTrends": true,
"proxyBingTerms": true
},
"notifications": {
"webhook": {
"enabled": false,
"url": ""
},
"conclusionWebhook": {
"enabled": false,
"url": ""
},
"ntfy": {
"enabled": false,
"url": "",
"topic": "rewards",
"authToken": ""
}
},
"logging": {
"excludeFunc": [
"SEARCH-CLOSE-TABS",
"LOGIN-NO-PROMPT",
"FLOW"
],
"webhookExcludeFunc": [
"SEARCH-CLOSE-TABS",
"LOGIN-NO-PROMPT",
"FLOW"
],
"redactEmails": true
}
}

View File

@@ -1,241 +0,0 @@
{
// ============================================================
// 🌐 GENERAL CONFIGURATION
// ============================================================
// Base URL for Microsoft Rewards dashboard (do not change unless necessary)
"baseURL": "https://rewards.bing.com",
// Directory to store sessions (cookies, browser fingerprints)
"sessionPath": "sessions",
// Dry-run mode: simulate execution without actually running tasks (useful for testing)
"dryRun": false,
// ============================================================
// 🖥️ BROWSER CONFIGURATION
// ============================================================
"browser": {
// false = visible window | true = headless mode (invisible)
"headless": false,
// Max timeout for operations (supports: 30000, "30s", "2min")
"globalTimeout": "30s"
},
"fingerprinting": {
// Persist browser fingerprints to improve consistency across runs
"saveFingerprint": {
"mobile": true,
"desktop": true
}
},
// ============================================================
// ⚙️ EXECUTION & PERFORMANCE
// ============================================================
"execution": {
// true = Desktop + Mobile in parallel (faster, more resources)
// false = Sequential (slower, fewer resources)
"parallel": false,
// If false, skip execution when 0 points are available
"runOnZeroPoints": false,
// Number of account clusters (processes) to run concurrently
"clusters": 1,
// How many times to run through all accounts in sequence (1 = process each account once, 2 = twice, etc.)
// Higher values can catch missed tasks but increase detection risk
"passesPerRun": 3
},
"jobState": {
// Save state to avoid duplicate work across restarts
"enabled": true,
// Custom state directory (empty = defaults to sessionPath/job-state)
"dir": ""
},
// ============================================================
// 🎯 TASKS & WORKERS
// ============================================================
"workers": {
// Select which tasks the bot should complete on desktop/mobile
"doDailySet": true, // Daily set tasks
"doMorePromotions": true, // More promotions section
"doPunchCards": true, // Punch cards
"doDesktopSearch": true, // Desktop searches
"doMobileSearch": true, // Mobile searches
"doDailyCheckIn": true, // Daily check-in
"doReadToEarn": true, // Read to earn
// If true, run desktop searches right after Daily Set
"bundleDailySetWithSearch": true
},
// ============================================================
// 🔍 SEARCH CONFIGURATION
// ============================================================
"search": {
// Use locale-specific query sources
"useLocalQueries": true,
"settings": {
// Use region-specific queries (at, fr, us, etc.)
"useGeoLocaleQueries": true,
// Randomly scroll search result pages (more natural behavior)
"scrollRandomResults": true,
// Occasionally click a result (safe targets only)
"clickRandomResults": true,
// Number of retries if mobile searches don't progress
"retryMobileSearchAmount": 2,
// Delay between searches
"delay": {
"min": "1min",
"max": "5min"
}
}
},
"queryDiversity": {
// Multi-source query generation: Reddit, News, Wikipedia instead of only Google Trends
"enabled": true,
// Available sources: google-trends, reddit, news, wikipedia, local-fallback
"sources": ["google-trends", "reddit", "local-fallback"],
// Max queries to fetch per source
"maxQueriesPerSource": 10,
// Cache duration in minutes (avoids hammering APIs)
"cacheMinutes": 30
},
// ============================================================
// 🤖 HUMANIZATION & NATURAL BEHAVIOR
// ============================================================
"humanization": {
// Human Mode: adds subtle micro-gestures & pauses to mimic real users
"enabled": true,
// If a ban is detected on any account, stop processing remaining accounts
"stopOnBan": true,
// Immediately send an alert (webhook/NTFY) when a ban is detected
"immediateBanAlert": true,
// Extra random pause between actions
"actionDelay": {
"min": 500, // 0.5 seconds minimum
"max": 2200 // 2.2 seconds maximum
},
// Probability (0-1) to move mouse slightly between actions
"gestureMoveProb": 0.65,
// Probability (0-1) to perform a small scroll
"gestureScrollProb": 0.4,
// Optional execution time windows (e.g., ["08:30-11:00", "19:00-22:00"])
// If specified, waits until inside a window before starting
"allowedWindows": []
},
"vacation": {
// Monthly "vacation" block: skip a random range of days each month
// Each month, a random period between minDays and maxDays is selected
// and all runs within that date range are skipped (more human-like behavior)
"enabled": true,
"minDays": 2,
"maxDays": 4
},
// ============================================================
// 🛡️ RISK MANAGEMENT & SECURITY
// ============================================================
"riskManagement": {
// Dynamic delay adjustment based on detected risk signals
"enabled": true,
// Automatically increase delays when captchas/errors are detected
"autoAdjustDelays": true,
// Stop execution if risk level reaches critical threshold
"stopOnCritical": false,
// Enable ML-based ban prediction based on patterns
"banPrediction": true,
// Risk threshold (0-100). If exceeded, bot pauses or alerts you
"riskThreshold": 75
},
"retryPolicy": {
// Generic retry/backoff for transient failures
"maxAttempts": 3,
"baseDelay": 1000,
"maxDelay": "30s",
"multiplier": 2,
"jitter": 0.2
},
// ============================================================
// 🌐 PROXY
// ============================================================
"proxy": {
// Control which outbound calls go through your proxy
"proxyGoogleTrends": true,
"proxyBingTerms": true
},
// ============================================================
// 🔔 NOTIFICATIONS
// ============================================================
"notifications": {
// Live logs webhook (Discord or similar). URL = your webhook endpoint
"webhook": {
"enabled": false,
"url": "",
// Optional: Customize webhook appearance
"username": "Live Logs",
"avatarUrl": "https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png"
},
// Rich end-of-run summary webhook (Discord or similar)
"conclusionWebhook": {
"enabled": false,
"url": "",
// Optional: Customize webhook appearance (overrides webhook settings for conclusion messages)
"username": "Microsoft Rewards",
"avatarUrl": "https://media.discordapp.net/attachments/1421163952972369931/1421929950377939125/Gc.png"
},
// NTFY push notifications (plain text)
"ntfy": {
"enabled": false,
"url": "",
"topic": "rewards",
"authToken": ""
}
},
// ============================================================
// 📊 LOGGING & DIAGNOSTICS
// ============================================================
"logging": {
// Logging controls (see docs/config.md)
// Filter out noisy log buckets locally and for webhook summaries
"excludeFunc": [
"SEARCH-CLOSE-TABS",
"LOGIN-NO-PROMPT",
"FLOW"
],
"webhookExcludeFunc": [
"SEARCH-CLOSE-TABS",
"LOGIN-NO-PROMPT",
"FLOW"
],
// Email redaction toggle (true = secure, false = full emails)
"redactEmails": true
}
}

View File

@@ -88,11 +88,11 @@ export class Login {
await page.goto('https://www.bing.com/rewards/dashboard')
await this.disableFido(page)
await page.waitForLoadState('domcontentloaded').catch(()=>{})
await page.waitForLoadState('domcontentloaded').catch(() => { })
await this.bot.browser.utils.reloadBadPage(page)
await this.checkAccountLocked(page)
const already = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 8000 }).then(()=>true).catch(()=>false)
const already = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 8000 }).then(() => true).catch(() => false)
if (!already) {
await this.performLoginFlow(page, email, password)
} else {
@@ -145,7 +145,7 @@ export class Login {
const req: AxiosRequestConfig = { url: this.tokenUrl, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: form.toString() }
const resp = await this.bot.axios.request(req)
const data: OAuth = resp.data
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Authorized in ${Math.round((Date.now()-start)/1000)}s`)
this.bot.log(this.bot.isMobile, 'LOGIN-APP', `Authorized in ${Math.round((Date.now() - start) / 1000)}s`)
return data.access_token
}
@@ -157,7 +157,7 @@ export class Login {
await this.bot.utils.wait(500)
await this.tryRecoveryMismatchCheck(page, email)
if (this.bot.compromisedModeActive && this.bot.compromisedReason === 'recovery-mismatch') {
this.bot.log(this.bot.isMobile,'LOGIN','Recovery mismatch detected stopping before password entry','warn')
this.bot.log(this.bot.isMobile, 'LOGIN', 'Recovery mismatch detected stopping before password entry', 'warn')
return
}
// Try switching to password if a locale link is present (FR/EN)
@@ -173,26 +173,26 @@ export class Login {
// --------------- Input Steps ---------------
private async inputEmail(page: Page, email: string) {
const field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(()=>null)
const field = await page.waitForSelector(SELECTORS.emailInput, { timeout: 5000 }).catch(() => null)
if (!field) { this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not present', 'warn'); return }
const prefilled = await page.waitForSelector('#userDisplayName', { timeout: 1500 }).catch(()=>null)
const prefilled = await page.waitForSelector('#userDisplayName', { timeout: 1500 }).catch(() => null)
if (!prefilled) {
await page.fill(SELECTORS.emailInput, '')
await page.fill(SELECTORS.emailInput, email)
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email prefilled')
}
const next = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null)
if (next) { await next.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email') }
const next = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(() => null)
if (next) { await next.click().catch(() => { }); this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email') }
}
private async inputPasswordOr2FA(page: Page, password: string) {
// Some flows require switching to password first
const switchBtn = await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 1500 }).catch(()=>null)
if (switchBtn) { await switchBtn.click().catch(()=>{}); await this.bot.utils.wait(1000) }
const switchBtn = await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 1500 }).catch(() => null)
if (switchBtn) { await switchBtn.click().catch(() => { }); await this.bot.utils.wait(1000) }
// Rare flow: list of methods -> choose password
const passwordField = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 4000 }).catch(()=>null)
const passwordField = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 4000 }).catch(() => null)
if (!passwordField) {
const blocked = await this.detectSignInBlocked(page)
if (blocked) return
@@ -204,13 +204,13 @@ export class Login {
const otherWaysHandled = await this.handleOtherWaysToSignIn(page)
if (otherWaysHandled) {
// Try to find password field again after clicking "Other ways"
const passwordFieldAfter = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 3000 }).catch(()=>null)
const passwordFieldAfter = await page.waitForSelector(SELECTORS.passwordInput, { timeout: 3000 }).catch(() => null)
if (passwordFieldAfter) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password field found after "Other ways" flow')
await page.fill(SELECTORS.passwordInput, '')
await page.fill(SELECTORS.passwordInput, password)
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null)
if (submit) { await submit.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') }
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(() => null)
if (submit) { await submit.click().catch(() => { }); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') }
return
}
}
@@ -226,8 +226,8 @@ export class Login {
await page.fill(SELECTORS.passwordInput, '')
await page.fill(SELECTORS.passwordInput, password)
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null)
if (submit) { await submit.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') }
const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(() => null)
if (submit) { await submit.click().catch(() => { }); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') }
}
@@ -247,7 +247,7 @@ export class Login {
for (const selector of otherWaysSelectors) {
const element = await page.waitForSelector(selector, { timeout: 1000 }).catch(() => null)
if (element && await element.isVisible().catch(() => false)) {
await element.click().catch(() => {})
await element.click().catch(() => { })
this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Other ways to sign in"')
await this.bot.utils.wait(2000) // Wait for options to appear
clicked = true
@@ -273,7 +273,7 @@ export class Login {
for (const selector of usePasswordSelectors) {
const element = await page.waitForSelector(selector, { timeout: 1500 }).catch(() => null)
if (element && await element.isVisible().catch(() => false)) {
await element.click().catch(() => {})
await element.click().catch(() => { })
this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Use your password"')
await this.bot.utils.wait(2000) // Wait for password field to appear
return true
@@ -320,13 +320,13 @@ export class Login {
if (this.bot.config.parallel) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Parallel mode: throttling authenticator push requests', 'log', 'yellow')
for (let attempts = 0; attempts < 6; attempts++) { // max 6 minutes retry window
const resend = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { timeout: 1500 }).catch(()=>null)
const resend = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { timeout: 1500 }).catch(() => null)
if (!resend) break
await this.bot.utils.wait(60000)
await resend.click().catch(()=>{})
await resend.click().catch(() => { })
}
}
await page.click('button[aria-describedby="confirmSendTitle"]').catch(()=>{})
await page.click('button[aria-describedby="confirmSendTitle"]').catch(() => { })
await this.bot.utils.wait(1500)
try {
const el = await page.waitForSelector('#displaySign, div[data-testid="displaySign"]>span', { timeout: 2000 })
@@ -344,14 +344,14 @@ export class Login {
return
} catch {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Authenticator code expired refreshing')
const retryBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 3000 }).catch(()=>null)
if (retryBtn) await retryBtn.click().catch(()=>{})
const retryBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 3000 }).catch(() => null)
if (retryBtn) await retryBtn.click().catch(() => { })
const refreshed = await this.fetchAuthenticatorNumber(page)
if (!refreshed) { this.bot.log(this.bot.isMobile, 'LOGIN', 'Could not refresh authenticator code', 'warn'); return }
numberToPress = refreshed
}
}
this.bot.log(this.bot.isMobile,'LOGIN','Authenticator approval loop exited (max cycles reached)','warn')
this.bot.log(this.bot.isMobile, 'LOGIN', 'Authenticator approval loop exited (max cycles reached)', 'warn')
}
private async handleSMSOrTotp(page: Page) {
@@ -363,7 +363,7 @@ export class Login {
await this.submitTotpCode(page, totpSelector)
return
}
} catch {/* ignore */}
} catch {/* ignore */ }
}
// Manual prompt with periodic page check
@@ -395,7 +395,7 @@ export class Login {
rl.close()
userInput = 'skip' // Signal to skip submission
}
} catch {/* ignore */}
} catch {/* ignore */ }
}, 2000)
const code = await inputPromise
@@ -411,7 +411,7 @@ export class Login {
} finally {
// Ensure cleanup happens even if errors occur
if (checkInterval) clearInterval(checkInterval)
try { rl.close() } catch {/* ignore */}
try { rl.close() } catch {/* ignore */ }
}
}
@@ -448,7 +448,7 @@ export class Login {
try {
const code = generateTOTP(this.currentTotpSecret!.trim())
const input = page.locator(selector).first()
if (!await input.isVisible().catch(()=>false)) {
if (!await input.isVisible().catch(() => false)) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'TOTP input unexpectedly hidden', 'warn')
return
}
@@ -457,9 +457,9 @@ export class Login {
// Use unified selector system
const submit = await this.findFirstVisibleLocator(page, Login.TOTP_SELECTORS.submit)
if (submit) {
await submit.click().catch(()=>{})
await submit.click().catch(() => { })
} else {
await page.keyboard.press('Enter').catch(()=>{})
await page.keyboard.press('Enter').catch(() => { })
}
this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted TOTP automatically')
} catch (error) {
@@ -529,7 +529,7 @@ export class Login {
for (const sel of selectors) {
const loc = page.locator(sel).first()
if (await loc.isVisible().catch(() => false)) {
await loc.click().catch(()=>{})
await loc.click().catch(() => { })
return true
}
}
@@ -560,7 +560,7 @@ export class Login {
while (Date.now() - start < timeoutMs) {
for (const sel of selectors) {
const loc = page.locator(sel).first()
if (await loc.isVisible().catch(()=>false)) {
if (await loc.isVisible().catch(() => false)) {
return sel
}
}
@@ -588,7 +588,7 @@ export class Login {
if (!portalSelector) {
try {
await this.bot.browser.func.goHome(page)
} catch {/* ignore fallback errors */}
} catch {/* ignore fallback errors */ }
const fallbackSelector = await this.waitForRewardsRoot(page, 6000)
if (!fallbackSelector) {
@@ -605,60 +605,60 @@ export class Login {
try {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Verifying Bing auth context')
await page.goto('https://www.bing.com/fd/auth/signin?action=interactive&provider=windows_live_id&return_url=https%3A%2F%2Fwww.bing.com%2F')
for (let i=0;i<5;i++) {
for (let i = 0; i < 5; i++) {
const u = new URL(page.url())
if (u.hostname === 'www.bing.com' && u.pathname === '/') {
await this.bot.browser.utils.tryDismissAllMessages(page)
const ok = await page.waitForSelector('#id_n', { timeout: 3000 }).then(()=>true).catch(()=>false)
if (ok || this.bot.isMobile) { this.bot.log(this.bot.isMobile,'LOGIN-BING','Bing verification passed'); break }
const ok = await page.waitForSelector('#id_n', { timeout: 3000 }).then(() => true).catch(() => false)
if (ok || this.bot.isMobile) { this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing verification passed'); break }
}
await this.bot.utils.wait(1000)
}
} catch (e) {
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing verification error: '+e, 'warn')
this.bot.log(this.bot.isMobile, 'LOGIN-BING', 'Bing verification error: ' + e, 'warn')
}
}
private async checkAccountLocked(page: Page) {
const locked = await page.waitForSelector('#serviceAbuseLandingTitle', { timeout: 1200 }).then(()=>true).catch(()=>false)
if (locked) throw this.bot.log(this.bot.isMobile,'CHECK-LOCKED','Account locked by Microsoft (serviceAbuseLandingTitle)','error')
const locked = await page.waitForSelector('#serviceAbuseLandingTitle', { timeout: 1200 }).then(() => true).catch(() => false)
if (locked) throw this.bot.log(this.bot.isMobile, 'CHECK-LOCKED', 'Account locked by Microsoft (serviceAbuseLandingTitle)', 'error')
}
// --------------- Passkey / Dialog Handling ---------------
private async handlePasskeyPrompts(page: Page, context: 'main' | 'oauth') {
let did = false
// Video heuristic
const biometric = await page.waitForSelector(SELECTORS.biometricVideo, { timeout: 500 }).catch(()=>null)
const biometric = await page.waitForSelector(SELECTORS.biometricVideo, { timeout: 500 }).catch(() => null)
if (biometric) {
const btn = await page.$(SELECTORS.passkeySecondary)
if (btn) { await btn.click().catch(()=>{}); did = true; this.logPasskeyOnce('video heuristic') }
if (btn) { await btn.click().catch(() => { }); did = true; this.logPasskeyOnce('video heuristic') }
}
if (!did) {
const titleEl = await page.waitForSelector(SELECTORS.passkeyTitle, { timeout: 500 }).catch(()=>null)
const secBtn = await page.waitForSelector(SELECTORS.passkeySecondary, { timeout: 500 }).catch(()=>null)
const primBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 500 }).catch(()=>null)
const titleEl = await page.waitForSelector(SELECTORS.passkeyTitle, { timeout: 500 }).catch(() => null)
const secBtn = await page.waitForSelector(SELECTORS.passkeySecondary, { timeout: 500 }).catch(() => null)
const primBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 500 }).catch(() => null)
const title = (titleEl ? (await titleEl.textContent()) : '')?.trim() || ''
const looksLike = /sign in faster|passkey|fingerprint|face|pin/i.test(title)
if (looksLike && secBtn) { await secBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('title heuristic '+title) }
if (looksLike && secBtn) { await secBtn.click().catch(() => { }); did = true; this.logPasskeyOnce('title heuristic ' + title) }
else if (!did && secBtn && primBtn) {
const text = (await secBtn.textContent()||'').trim()
if (/skip for now/i.test(text)) { await secBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('secondary button text') }
const text = (await secBtn.textContent() || '').trim()
if (/skip for now/i.test(text)) { await secBtn.click().catch(() => { }); did = true; this.logPasskeyOnce('secondary button text') }
}
if (!did) {
const textBtn = await page.locator('xpath=//button[contains(normalize-space(.),"Skip for now")]').first()
if (await textBtn.isVisible().catch(()=>false)) { await textBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('text fallback') }
if (await textBtn.isVisible().catch(() => false)) { await textBtn.click().catch(() => { }); did = true; this.logPasskeyOnce('text fallback') }
}
if (!did) {
const close = await page.$('#close-button')
if (close) { await close.click().catch(()=>{}); did = true; this.logPasskeyOnce('close button') }
if (close) { await close.click().catch(() => { }); did = true; this.logPasskeyOnce('close button') }
}
}
// KMSI prompt
const kmsi = await page.waitForSelector(SELECTORS.kmsiVideo, { timeout: 400 }).catch(()=>null)
const kmsi = await page.waitForSelector(SELECTORS.kmsiVideo, { timeout: 400 }).catch(() => null)
if (kmsi) {
const yes = await page.$(SELECTORS.passkeyPrimary)
if (yes) { await yes.click().catch(()=>{}); did = true; this.bot.log(this.bot.isMobile,'LOGIN-KMSI','Accepted KMSI prompt') }
if (yes) { await yes.click().catch(() => { }); did = true; this.bot.log(this.bot.isMobile, 'LOGIN-KMSI', 'Accepted KMSI prompt') }
}
if (!did && context === 'main') {
@@ -666,7 +666,7 @@ export class Login {
const now = Date.now()
if (this.noPromptIterations === 1 || now - this.lastNoPromptLog > 10000) {
this.lastNoPromptLog = now
this.bot.log(this.bot.isMobile,'LOGIN-NO-PROMPT',`No dialogs (x${this.noPromptIterations})`)
this.bot.log(this.bot.isMobile, 'LOGIN-NO-PROMPT', `No dialogs (x${this.noPromptIterations})`)
if (this.noPromptIterations > 50) this.noPromptIterations = 0
}
} else if (did) {
@@ -677,7 +677,7 @@ export class Login {
private logPasskeyOnce(reason: string) {
if (this.passkeyHandled) return
this.passkeyHandled = true
this.bot.log(this.bot.isMobile,'LOGIN-PASSKEY',`Dismissed passkey prompt (${reason})`)
this.bot.log(this.bot.isMobile, 'LOGIN-PASSKEY', `Dismissed passkey prompt (${reason})`)
}
// --------------- Security Detection ---------------
@@ -685,11 +685,11 @@ export class Login {
if (this.bot.compromisedModeActive && this.bot.compromisedReason === 'sign-in-blocked') return true
try {
let text = ''
for (const sel of ['[data-testid="title"]','h1','div[role="heading"]','div.text-title']) {
const el = await page.waitForSelector(sel, { timeout: 600 }).catch(()=>null)
for (const sel of ['[data-testid="title"]', 'h1', 'div[role="heading"]', 'div.text-title']) {
const el = await page.waitForSelector(sel, { timeout: 600 }).catch(() => null)
if (el) {
const t = (await el.textContent()||'').trim()
if (t && t.length < 300) text += ' '+t
const t = (await el.textContent() || '').trim()
if (t && t.length < 300) text += ' ' + t
}
}
const lower = text.toLowerCase()
@@ -697,57 +697,53 @@ export class Login {
for (const p of SIGN_IN_BLOCK_PATTERNS) { if (p.re.test(lower)) { matched = p.label; break } }
if (!matched) return false
const email = this.bot.currentAccountEmail || 'unknown'
const docsUrl = this.getDocsUrl('we-cant-sign-you-in')
const incident: SecurityIncident = {
kind: 'We can\'t sign you in (blocked)',
account: email,
details: [matched ? `Pattern: ${matched}` : 'Pattern: unknown'],
next: ['Manual recovery required before continuing'],
docsUrl
next: ['Manual recovery required before continuing']
}
await this.sendIncidentAlert(incident,'warn')
await this.sendIncidentAlert(incident, 'warn')
this.bot.compromisedModeActive = true
this.bot.compromisedReason = 'sign-in-blocked'
this.startCompromisedInterval()
await this.bot.engageGlobalStandby('sign-in-blocked', email).catch(()=>{})
// Open security docs for immediate guidance (best-effort)
await this.openDocsTab(page, docsUrl).catch(()=>{})
await this.bot.engageGlobalStandby('sign-in-blocked', email).catch(() => { })
return true
} catch { return false }
}
private async tryRecoveryMismatchCheck(page: Page, email: string) { try { await this.detectAndHandleRecoveryMismatch(page, email) } catch {/* ignore */} }
private async tryRecoveryMismatchCheck(page: Page, email: string) { try { await this.detectAndHandleRecoveryMismatch(page, email) } catch {/* ignore */ } }
private async detectAndHandleRecoveryMismatch(page: Page, email: string) {
try {
const recoveryEmail: string | undefined = this.bot.currentAccountRecoveryEmail
if (!recoveryEmail || !/@/.test(recoveryEmail)) return
const accountEmail = email
const parseRef = (val: string) => { const [l,d] = val.split('@'); return { local: l||'', domain:(d||'').toLowerCase(), prefix2:(l||'').slice(0,2).toLowerCase() } }
const refs = [parseRef(recoveryEmail), parseRef(accountEmail)].filter(r=>r.domain && r.prefix2)
const parseRef = (val: string) => { const [l, d] = val.split('@'); return { local: l || '', domain: (d || '').toLowerCase(), prefix2: (l || '').slice(0, 2).toLowerCase() } }
const refs = [parseRef(recoveryEmail), parseRef(accountEmail)].filter(r => r.domain && r.prefix2)
if (refs.length === 0) return
const candidates: string[] = []
// Direct selectors (Microsoft variants + French spans)
const sel = '[data-testid="recoveryEmailHint"], #recoveryEmail, [id*="ProofEmail"], [id*="EmailProof"], [data-testid*="Email"], span:has(span.fui-Text)'
const el = await page.waitForSelector(sel, { timeout: 1500 }).catch(()=>null)
if (el) { const t = (await el.textContent()||'').trim(); if (t) candidates.push(t) }
const el = await page.waitForSelector(sel, { timeout: 1500 }).catch(() => null)
if (el) { const t = (await el.textContent() || '').trim(); if (t) candidates.push(t) }
// List items
const li = page.locator('[role="listitem"], li')
const liCount = await li.count().catch(()=>0)
for (let i=0;i<liCount && i<12;i++) { const t = (await li.nth(i).textContent().catch(()=>''))?.trim()||''; if (t && /@/.test(t)) candidates.push(t) }
const liCount = await li.count().catch(() => 0)
for (let i = 0; i < liCount && i < 12; i++) { const t = (await li.nth(i).textContent().catch(() => ''))?.trim() || ''; if (t && /@/.test(t)) candidates.push(t) }
// XPath generic masked patterns
const xp = page.locator('xpath=//*[contains(normalize-space(.), "@") and (contains(normalize-space(.), "*") or contains(normalize-space(.), "•"))]')
const xpCount = await xp.count().catch(()=>0)
for (let i=0;i<xpCount && i<12;i++) { const t = (await xp.nth(i).textContent().catch(()=>''))?.trim()||''; if (t && t.length<300) candidates.push(t) }
const xpCount = await xp.count().catch(() => 0)
for (let i = 0; i < xpCount && i < 12; i++) { const t = (await xp.nth(i).textContent().catch(() => ''))?.trim() || ''; if (t && t.length < 300) candidates.push(t) }
// Normalize
const seen = new Set<string>()
const norm = (s:string)=>s.replace(/\s+/g,' ').trim()
const uniq = candidates.map(norm).filter(t=>t && !seen.has(t) && seen.add(t))
const norm = (s: string) => s.replace(/\s+/g, ' ').trim()
const uniq = candidates.map(norm).filter(t => t && !seen.has(t) && seen.add(t))
// Masked filter
let masked = uniq.filter(t=>/@/.test(t) && /[*•]/.test(t))
let masked = uniq.filter(t => /@/.test(t) && /[*•]/.test(t))
if (masked.length === 0) {
// Fallback full HTML scan
@@ -758,14 +754,14 @@ export class Login {
const found = new Set<string>()
let m: RegExpExecArray | null
while ((m = generic.exec(html)) !== null) found.add(m[0])
while ((m = frPhrase.exec(html)) !== null) { const raw = m[1]?.replace(/<[^>]+>/g,'').trim(); if (raw) found.add(raw) }
while ((m = frPhrase.exec(html)) !== null) { const raw = m[1]?.replace(/<[^>]+>/g, '').trim(); if (raw) found.add(raw) }
if (found.size > 0) masked = Array.from(found)
} catch {/* ignore */}
} catch {/* ignore */ }
}
if (masked.length === 0) return
// Prefer one mentioning email/adresse
const preferred = masked.find(t=>/email|courriel|adresse|mail/i.test(t)) || masked[0]!
const preferred = masked.find(t => /email|courriel|adresse|mail/i.test(t)) || masked[0]!
// Extract the masked email: Microsoft sometimes shows only first 1 char (k*****@domain) or 2 chars (ko*****@domain).
// We ONLY compare (1 or 2) leading visible alphanumeric chars + full domain (case-insensitive).
// This avoids false positives when the displayed mask hides the 2nd char.
@@ -784,7 +780,7 @@ export class Login {
}
if (!observedPrefix && extractedLower.includes('@')) {
const parts = extractedLower.split('@')
observedPrefix = (parts[0] || '').replace(/[^a-z0-9]/gi,'').slice(0,2)
observedPrefix = (parts[0] || '').replace(/[^a-z0-9]/gi, '').slice(0, 2)
}
// Determine if any reference (recoveryEmail or accountEmail) matches observed mask logic
@@ -798,55 +794,52 @@ export class Login {
})
if (!matchRef) {
const docsUrl = this.getDocsUrl('recovery-email-mismatch')
const incident: SecurityIncident = {
kind:'Recovery email mismatch',
kind: 'Recovery email mismatch',
account: email,
details:[
details: [
`MaskedShown: ${preferred}`,
`Extracted: ${extracted}`,
`Observed => ${observedPrefix || '??'}**@${observedDomain || '??'}`,
`Expected => ${refs.map(r=>`${r.prefix2}**@${r.domain}`).join(' OR ')}`
`Expected => ${refs.map(r => `${r.prefix2}**@${r.domain}`).join(' OR ')}`
],
next:[
next: [
'Automation halted globally (standby engaged).',
'Verify account security & recovery email in Microsoft settings.',
'Update accounts.json if the change was legitimate before restart.'
],
docsUrl
]
}
await this.sendIncidentAlert(incident,'critical')
await this.sendIncidentAlert(incident, 'critical')
this.bot.compromisedModeActive = true
this.bot.compromisedReason = 'recovery-mismatch'
this.startCompromisedInterval()
await this.bot.engageGlobalStandby('recovery-mismatch', email).catch(()=>{})
await this.openDocsTab(page, docsUrl).catch(()=>{})
await this.bot.engageGlobalStandby('recovery-mismatch', email).catch(() => { })
} else {
const mode = observedPrefix.length === 1 ? 'lenient' : 'strict'
this.bot.log(this.bot.isMobile,'LOGIN-RECOVERY',`Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`)
this.bot.log(this.bot.isMobile, 'LOGIN-RECOVERY', `Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`)
}
} catch {/* non-fatal */}
} catch {/* non-fatal */ }
}
private async switchToPasswordLink(page: Page) {
try {
const link = await page.locator('xpath=//span[@role="button" and (contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"use your password") or contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"utilisez votre mot de passe"))]').first()
if (await link.isVisible().catch(()=>false)) {
await link.click().catch(()=>{})
if (await link.isVisible().catch(() => false)) {
await link.click().catch(() => { })
await this.bot.utils.wait(800)
this.bot.log(this.bot.isMobile,'LOGIN','Clicked "Use your password" link')
this.bot.log(this.bot.isMobile, 'LOGIN', 'Clicked "Use your password" link')
}
} catch {/* ignore */}
} catch {/* ignore */ }
}
// --------------- Incident Helpers ---------------
private async sendIncidentAlert(incident: SecurityIncident, severity: 'warn'|'critical'='warn') {
const lines = [ `[Incident] ${incident.kind}`, `Account: ${incident.account}` ]
private async sendIncidentAlert(incident: SecurityIncident, severity: 'warn' | 'critical' = 'warn') {
const lines = [`[Incident] ${incident.kind}`, `Account: ${incident.account}`]
if (incident.details?.length) lines.push(`Details: ${incident.details.join(' | ')}`)
if (incident.next?.length) lines.push(`Next: ${incident.next.join(' -> ')}`)
if (incident.docsUrl) lines.push(`Docs: ${incident.docsUrl}`)
const level: 'warn'|'error' = severity === 'critical' ? 'error' : 'warn'
this.bot.log(this.bot.isMobile,'SECURITY',lines.join(' | '), level)
const level: 'warn' | 'error' = severity === 'critical' ? 'error' : 'warn'
this.bot.log(this.bot.isMobile, 'SECURITY', lines.join(' | '), level)
try {
const { ConclusionWebhook } = await import('../util/ConclusionWebhook')
const fields = [
@@ -858,36 +851,18 @@ export class Login {
await ConclusionWebhook(
this.bot.config,
`🔐 ${incident.kind}`,
'_Security check by @Light_',
'_Security check',
fields,
severity === 'critical' ? 0xFF0000 : 0xFFAA00
)
} catch {/* ignore */}
}
private getDocsUrl(anchor?: string) {
const base = process.env.DOCS_BASE?.trim() || 'https://github.com/LightZirconite/Microsoft-Rewards-Script-Private/blob/v2/docs/security.md'
const map: Record<string,string> = {
'recovery-email-mismatch':'#recovery-email-mismatch',
'we-cant-sign-you-in':'#we-cant-sign-you-in-blocked'
}
return anchor && map[anchor] ? `${base}${map[anchor]}` : base
} catch {/* ignore */ }
}
private startCompromisedInterval() {
if (this.compromisedInterval) clearInterval(this.compromisedInterval)
this.compromisedInterval = setInterval(()=>{
try { this.bot.log(this.bot.isMobile,'SECURITY','Account in security standby. Review before proceeding. Security check by @Light','warn') } catch {/* ignore */}
}, 5*60*1000)
}
private async openDocsTab(page: Page, url: string) {
try {
const ctx = page.context()
const tab = await ctx.newPage()
await tab.goto(url, { waitUntil: 'domcontentloaded' })
} catch {/* ignore */}
this.compromisedInterval = setInterval(() => {
try { this.bot.log(this.bot.isMobile, 'SECURITY', 'Account in security standby. Review before proceeding.', 'warn') } catch {/* ignore */ }
}, 5 * 60 * 1000)
}
// --------------- Infrastructure ---------------
@@ -898,6 +873,6 @@ export class Login {
body.isFidoSupported = false
route.continue({ postData: JSON.stringify(body) })
} catch { route.continue() }
}).catch(()=>{})
}).catch(() => { })
}
}

View File

@@ -31,7 +31,8 @@ export class DailyCheckIn extends Workers {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
'X-Rewards-Language': 'en',
'X-Rewards-ismobile': 'true'
},
data: JSON.stringify(jsonData)
}

View File

@@ -47,7 +47,8 @@ export class ReadToEarn extends Workers {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
'X-Rewards-Language': 'en',
'X-Rewards-ismobile': 'true'
},
data: JSON.stringify(jsonData)
}

View File

@@ -378,7 +378,7 @@ export class MicrosoftRewardsBot {
}
// If any account is flagged compromised, do NOT exit; keep the process alive so the browser stays open
if (this.compromisedModeActive || this.globalStandby.active) {
log('main','SECURITY','Compromised or banned detected. Global standby engaged: we will NOT proceed to other accounts until resolved. Keeping process alive. Press CTRL+C to exit when done. Security check by @Light','warn','yellow')
log('main','SECURITY','Compromised or banned detected. Global standby engaged: we will NOT proceed to other accounts until resolved. Keeping process alive. Press CTRL+C to exit when done.','warn','yellow')
const standbyInterval = setInterval(() => {
log('main','SECURITY','Still in standby: session(s) held open for manual recovery / review...','warn','yellow')
}, 5 * 60 * 1000)
@@ -467,13 +467,13 @@ export class MicrosoftRewardsBot {
if (this.compromisedModeActive) {
// User wants the page to remain open for manual recovery. Do not proceed to tasks.
const reason = this.compromisedReason || 'security-issue'
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving the browser open and skipping all activities for ${account.email}. Security check by @Light`, 'warn', 'yellow')
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving the browser open and skipping all activities for ${account.email}.`, 'warn', 'yellow')
try {
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
await ConclusionWebhook(
this.config,
'🔐 Security Alert (Post-Login)',
`**Account:** ${account.email}\n**Reason:** ${reason}\n**Action:** Leaving browser open; skipping tasks\n\n_Security check by @Light_`,
`**Account:** ${account.email}\n**Reason:** ${reason}\n**Action:** Leaving browser open; skipping tasks`,
undefined,
0xFFAA00
)
@@ -571,13 +571,13 @@ export class MicrosoftRewardsBot {
await this.login.login(this.homePage, account.email, account.password, account.totp)
if (this.compromisedModeActive) {
const reason = this.compromisedReason || 'security-issue'
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving mobile browser open and skipping mobile activities for ${account.email}. Security check by @Light`, 'warn', 'yellow')
log(this.isMobile, 'SECURITY', `Account flagged as compromised (${reason}). Leaving mobile browser open and skipping mobile activities for ${account.email}.`, 'warn', 'yellow')
try {
const { ConclusionWebhook } = await import('./util/ConclusionWebhook')
await ConclusionWebhook(
this.config,
'🔐 Security Alert (Mobile)',
`**Account:** ${account.email}\n**Reason:** ${reason}\n**Action:** Leaving mobile browser open; skipping tasks\n\n_Security check by @Light_`,
`**Account:** ${account.email}\n**Reason:** ${reason}\n**Action:** Leaving mobile browser open; skipping tasks`,
undefined,
0xFFAA00
)
@@ -790,7 +790,7 @@ export class MicrosoftRewardsBot {
await ConclusionWebhook(
this.config,
'🚨 Global Security Standby Engaged',
`@everyone\n\n**Account:** ${email}\n**Reason:** ${reason}\n**Action:** Pausing all further accounts. We will not proceed until this is resolved.\n\n_Security check by @Light_`,
`@everyone\n\n**Account:** ${email}\n**Reason:** ${reason}\n**Action:** Pausing all further accounts. We will not proceed until this is resolved.`,
undefined,
DISCORD.COLOR_RED
)

View File

@@ -22,7 +22,6 @@ export interface Config {
webhook: ConfigWebhook;
conclusionWebhook?: ConfigWebhook; // Optional secondary webhook for final summary
ntfy: ConfigNtfy;
passesPerRun?: number;
vacation?: ConfigVacation; // Optional monthly contiguous off-days
crashRecovery?: ConfigCrashRecovery; // Automatic restart / graceful shutdown
riskManagement?: ConfigRiskManagement; // NEW: Risk-aware throttling and ban prediction

View File

@@ -29,12 +29,6 @@ interface DiscordEmbed {
}
}
interface WebhookPayload {
username: string
avatar_url: string
embeds: DiscordEmbed[]
}
interface AccountSummary {
email: string
totalCollected: number
@@ -89,23 +83,17 @@ export async function ConclusionWebhook(
embed.fields = fields
}
// Use custom webhook settings if provided, otherwise fall back to defaults
const webhookUsername = config.webhook?.username || config.conclusionWebhook?.username || 'Microsoft Rewards'
const webhookAvatarUrl = config.webhook?.avatarUrl || config.conclusionWebhook?.avatarUrl || DISCORD.AVATAR_URL
const payload: WebhookPayload = {
username: webhookUsername,
avatar_url: webhookAvatarUrl,
embeds: [embed]
}
const postWebhook = async (url: string, label: string) => {
const maxAttempts = 3
let lastError: unknown = null
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await axios.post(url, payload, {
await axios.post(url,
{
embeds: [embed]
},
{
headers: { 'Content-Type': 'application/json' },
timeout: 15000
})
@@ -199,14 +187,14 @@ export async function ConclusionWebhookEnhanced(config: Config, data: Conclusion
// Build global statistics field
const globalStats = [
`**💎 Total Points Earned**`,
'**💎 Total Points Earned**',
`\`${data.totalInitial.toLocaleString()}\`\`${data.totalEnd.toLocaleString()}\` **(+${data.totalCollected.toLocaleString()})**`,
'',
`**📊 Accounts Processed**`,
'**📊 Accounts Processed**',
`✅ Success: **${data.successes}** | ⚠️ Errors: **${data.accountsWithErrors}** | 🚫 Banned: **${data.accountsBanned}**`,
`Total: **${data.totalAccounts}** ${data.totalAccounts === 1 ? 'account' : 'accounts'}`,
'',
`**⚡ Performance**`,
'**⚡ Performance**',
`Average: **${data.avgPointsPerAccount}pts/account** in **${formatDuration(data.avgDuration)}**`,
`Total Runtime: **${formatDuration(data.totalDuration)}**`
].join('\n')
@@ -295,15 +283,6 @@ export async function ConclusionWebhookEnhanced(config: Config, data: Conclusion
})
}
// Use custom webhook settings
const webhookUsername = config.conclusionWebhook?.username || config.webhook?.username || 'Microsoft Rewards'
const webhookAvatarUrl = config.conclusionWebhook?.avatarUrl || config.webhook?.avatarUrl || DISCORD.AVATAR_URL
const payload: WebhookPayload = {
username: webhookUsername,
avatar_url: webhookAvatarUrl,
embeds
}
const postWebhook = async (url: string, label: string) => {
const maxAttempts = 3
@@ -311,7 +290,9 @@ export async function ConclusionWebhookEnhanced(config: Config, data: Conclusion
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await axios.post(url, payload, {
await axios.post(url, {
embeds: embeds
}, {
headers: { 'Content-Type': 'application/json' },
timeout: 15000
})
@@ -339,7 +320,7 @@ export async function ConclusionWebhookEnhanced(config: Config, data: Conclusion
// Optional NTFY notification (simplified summary)
if (config.ntfy?.enabled && config.ntfy.url && config.ntfy.topic) {
const message = [
`🎯 Microsoft Rewards Summary`,
'🎯 Microsoft Rewards Summary',
`Status: ${statusText}`,
`Points: ${data.totalInitial}${data.totalEnd} (+${data.totalCollected})`,
`Accounts: ${data.successes}/${data.totalAccounts} successful`,

View File

@@ -15,7 +15,7 @@ export interface ValidationResult {
}
/**
* ConfigValidator performs intelligent validation of config.jsonc and accounts.json
* ConfigValidator performs intelligent validation of config.json and accounts.json
* before execution to catch common mistakes, conflicts, and security issues.
*/
export class ConfigValidator {
@@ -400,7 +400,6 @@ export class ConfigValidator {
const configRaw = fs.readFileSync(configPath, 'utf-8')
const accountsRaw = fs.readFileSync(accountsPath, 'utf-8')
// Remove JSONC comments (basic approach)
const configJson = configRaw.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')
const config: Config = JSON.parse(configJson)
const accounts: Account[] = JSON.parse(accountsRaw)

View File

@@ -10,62 +10,6 @@ import { Config, ConfigSaveFingerprint } from '../interface/Config'
let configCache: Config
let configSourcePath = ''
// Basic JSON comment stripper (supports // line and /* block */ comments while preserving strings)
function stripJsonComments(input: string): string {
let out = ''
let inString = false
let stringChar = ''
let inLine = false
let inBlock = false
for (let i = 0; i < input.length; i++) {
const ch = input[i]!
const next = input[i + 1]
if (inLine) {
if (ch === '\n' || ch === '\r') {
inLine = false
out += ch
}
continue
}
if (inBlock) {
if (ch === '*' && next === '/') {
inBlock = false
i++
}
continue
}
if (inString) {
out += ch
if (ch === '\\') { // escape next char
i++
if (i < input.length) out += input[i]
continue
}
if (ch === stringChar) {
inString = false
}
continue
}
if (ch === '"' || ch === '\'') {
inString = true
stringChar = ch
out += ch
continue
}
if (ch === '/' && next === '/') {
inLine = true
i++
continue
}
if (ch === '/' && next === '*') {
inBlock = true
i++
continue
}
out += ch
}
return out
}
// Normalize both legacy (flat) and new (nested) config schemas into the flat Config interface
function normalizeConfig(raw: unknown): Config {
@@ -79,7 +23,6 @@ function normalizeConfig(raw: unknown): Config {
const parallel = n.execution?.parallel ?? n.parallel ?? false
const runOnZeroPoints = n.execution?.runOnZeroPoints ?? n.runOnZeroPoints ?? false
const clusters = n.execution?.clusters ?? n.clusters ?? 1
const passesPerRun = n.execution?.passesPerRun ?? n.passesPerRun
// Search
const useLocalQueries = n.search?.useLocalQueries ?? n.searchOnBingLocalQueries ?? false
@@ -176,7 +119,6 @@ function normalizeConfig(raw: unknown): Config {
webhook,
conclusionWebhook,
ntfy,
passesPerRun: passesPerRun,
vacation: n.vacation,
crashRecovery: n.crashRecovery || {}
}
@@ -196,41 +138,35 @@ export function loadAccounts(): Account[] {
const envJson = process.env.ACCOUNTS_JSON
const envFile = process.env.ACCOUNTS_FILE
let raw: string | undefined
let json: string | undefined
if (envJson && envJson.trim().startsWith('[')) {
raw = envJson
json = envJson
} else if (envFile && envFile.trim()) {
const full = path.isAbsolute(envFile) ? envFile : path.join(process.cwd(), envFile)
if (!fs.existsSync(full)) {
throw new Error(`ACCOUNTS_FILE not found: ${full}`)
}
raw = fs.readFileSync(full, 'utf-8')
json = fs.readFileSync(full, 'utf-8')
} else {
// Try multiple locations to support both root mounts and dist mounts
// Support both .json and .jsonc extensions
// Support both .json and .json extensions
const candidates = [
path.join(__dirname, '../', file), // root/accounts.json (preferred)
path.join(__dirname, '../', file + 'c'), // root/accounts.jsonc
path.join(__dirname, '../src', file), // fallback: file kept inside src/
path.join(__dirname, '../src', file + 'c'), // src/accounts.jsonc
path.join(process.cwd(), file), // cwd override
path.join(process.cwd(), file + 'c'), // cwd/accounts.jsonc
path.join(process.cwd(), 'src', file), // cwd/src/accounts.json
path.join(process.cwd(), 'src', file + 'c'), // cwd/src/accounts.jsonc
path.join(__dirname, file), // dist/accounts.json (legacy)
path.join(__dirname, file + 'c') // dist/accounts.jsonc
path.join(__dirname, '../', file),
path.join(__dirname, '../src', file),
path.join(process.cwd(), file),
path.join(process.cwd(), 'src', file),
path.join(__dirname, file)
]
let chosen: string | null = null
for (const p of candidates) {
try { if (fs.existsSync(p)) { chosen = p; break } } catch { /* ignore */ }
}
if (!chosen) throw new Error(`accounts file not found in: ${candidates.join(' | ')}`)
raw = fs.readFileSync(chosen, 'utf-8')
json = fs.readFileSync(chosen, 'utf-8')
}
// Support comments in accounts file (same as config)
const cleaned = stripJsonComments(raw)
const parsedUnknown = JSON.parse(cleaned)
const parsedUnknown = JSON.parse(json)
// Accept either a root array or an object with an `accounts` array, ignore `_note`
const parsed = Array.isArray(parsedUnknown) ? parsedUnknown : (parsedUnknown && typeof parsedUnknown === 'object' && Array.isArray((parsedUnknown as { accounts?: unknown }).accounts) ? (parsedUnknown as { accounts: unknown[] }).accounts : null)
if (!Array.isArray(parsed)) throw new Error('accounts must be an array')
@@ -257,8 +193,8 @@ export function loadConfig(): Config {
return configCache
}
// Resolve configuration file from common locations (supports .jsonc and .json)
const names = ['config.jsonc', 'config.json']
// Resolve configuration file from common locations
const names = ['config.json']
const bases = [
path.join(__dirname, '../'), // dist root when compiled
path.join(__dirname, '../src'), // fallback: running dist but config still in src
@@ -278,8 +214,8 @@ export function loadConfig(): Config {
}
if (!cfgPath) throw new Error(`config.json not found in: ${candidates.join(' | ')}`)
const config = fs.readFileSync(cfgPath, 'utf-8')
const text = config.replace(/^\uFEFF/, '')
const raw = JSON.parse(stripJsonComments(text))
const json = config.replace(/^\uFEFF/, '')
const raw = JSON.parse(json)
const normalized = normalizeConfig(raw)
configCache = normalized // Set as cache
configSourcePath = cfgPath

View File

@@ -5,8 +5,6 @@ import { Ntfy } from './Ntfy'
import { loadConfig } from './Load'
import { DISCORD } from '../constants'
const DEFAULT_LIVE_LOG_USERNAME = 'MS Rewards - Live Logs'
type WebhookBuffer = {
lines: string[]
sending: boolean
@@ -45,11 +43,6 @@ async function sendBatch(url: string, buf: WebhookBuffer) {
if (buf.sending) return
buf.sending = true
// Load config to get webhook settings
const configData = loadConfig()
const webhookUsername = configData.webhook?.username || DEFAULT_LIVE_LOG_USERNAME
const webhookAvatarUrl = configData.webhook?.avatarUrl || DISCORD.AVATAR_URL
while (buf.lines.length > 0) {
const chunk: string[] = []
let currentLength = 0
@@ -69,8 +62,6 @@ async function sendBatch(url: string, buf: WebhookBuffer) {
// Enhanced webhook payload with embed, username and avatar
const payload = {
username: webhookUsername,
avatar_url: webhookAvatarUrl,
embeds: [{
description: `\`\`\`\n${content}\n\`\`\``,
color: determineColorFromContent(content),
@@ -149,7 +140,7 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
const loggingCfg: LoggingCfg = (configAny.logging || {}) as LoggingCfg
const shouldRedact = !!loggingCfg.redactEmails
const redact = (s: string) => shouldRedact ? s.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig, (m) => {
const [u, d] = m.split('@'); return `${(u||'').slice(0,2)}***@${d||''}`
const [u, d] = m.split('@'); return `${(u || '').slice(0, 2)}***@${d || ''}`
}) : s
const cleanStr = redact(`[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${platformText} [${title}] ${message}`)