* first commit

* Addition of a personalized activity manager and refactoring of the logic of activities

* Adding diagnostics management, including screenshot and HTML content, as well as improvements to humanize page interactions and +.

* Adding the management of newspapers and webhook settings, including filtering messages and improving the structure of the summaries sent.

* Adding a post-execution auto-date functionality, including options to update via Git and Docker, as well as a new configuration interface to manage these parameters.

* Adding accounts in Docker, with options to use an environmental file or online JSON data, as well as minimum validations for responsible accounts.

* Improving the Microsoft Rewards script display with a new headband and better log management, including colors and improved formatting for the console.

* v2

* Refactor ESLint configuration and scripts for improved TypeScript support and project structure

* Addition of the detection of suspended accounts with the gesture of the improved errors and journalization of banishment reasons

* Adding an integrated planner for programmed task execution, with configuration in Config.json and +

* Edit

* Remove texte

* Updating of documentation and adding the management of humanization in the configuration and +.

* Adding manual purchase method allowing users to spend points without automation, with monitoring of expenses and notifications.

* Correction of documentation and improvement of configuration management for manual purchase mode, adding complete documentation and appropriate banner display.

* Add comprehensive documentation for job state persistence, NTFY notifications, proxy configuration, scheduling, and auto-update features

- Introduced job state persistence documentation to track progress and resume tasks.
- Added NTFY push notifications integration guide for real-time alerts.
- Documented proxy configuration options for enhanced privacy and network management.
- Included scheduling configuration for automated script execution.
- Implemented auto-update configuration to keep installations current with Git and Docker options.

* Ajout d'Unt Système de Rapport d'Erreurs Communautaire pour Améliorerer le Débogage, incluant la Configuration et l'Envoi de Résumés D'Erreurs Anonyés à un webhook Discord.

* Mini Edit

* Mise à Jour du Readme.md pour Améliorerer la Présentation et La Claté, Ajout d'Un section sur les notifications en Temps Raine et Mise à Jour des badges pour la meille unibilité.

* Documentation update

* Edit README.md

* Edit

* Update README with legacy version link

* Improvement of location data management and webhooks, adding configurations normalization

* Force update for PR

* Improvement of documentation and configuration options for Cron integration and Docker use

* Improvement of planning documentation and adding a multi-pan-pancake in the daily execution script

* Deletion of the CommunityReport functionality in accordance with the project policy

* Addition of randomization of start -up schedules and surveillance time for planner executions

* Refactor Docker setup to use built-in scheduler, removing cron dependencies and simplifying configuration options

* Adding TOTP support for authentication, update of interfaces and configuration files to include Totp secret, and automatic generation of the Totp code when connecting.

* Fix [LOGIN-NO-PROMPT] No dialogs (xX)

* Reset the Totp field for email_1 in the accounts.example.json file

* Reset the Totp field for email_1 in the Readme.md file

* Improvement of Bing Research: Use of the 'Attacked' method for the research field, management of overlays and adding direct navigation in the event of entry failure.

* Adding a complete security policy, including directives on vulnerability management, coordinated disclosure and user security advice.

* Remove advanced environment variables section from README

* Configuration and dockerfile update: Passage to Node 22, addition of management of the purchase method, deletion of obsolete scripts

* Correction of the order of the sections in the Readme.md for better readability

* Update of Readm and Security Policy: Addition of the method of purchase and clarification of security and confidentiality practices.

* Improvement of the readability of the Readm and deletion of the mention of reporting of vulnerabilities in the security document.

* Addition of humanization management and adaptive throttling to simulate more human behavior in bot activities.

* Addition of humanization management: activation/deactivation of human gestures, configuration update and adding documentation on human mode.

* Deletion of community error report functionality to respect the privacy policy

* Addition of immediate banning alerts and vacation configuration in the Microsoft Rewards bot

* Addition of immediate banning alerts and vacation configuration in the Microsoft Rewards bot

* Added scheduling support: support for 12h and 24h formats, added options for time zone, and immediate execution on startup.

* Added window size normalization and page rendering to fit typical screens, with injected CSS styles to prevent excessive zooming.

* Added security incident management: detection of hidden recovery emails, automation blocking, and global alerts. Updated configuration files and interfaces to include recovery emails. Improved security incident documentation.

* Refactor incident alert handling: unified alert sender

* s

* Added security incident management: detect recovery email inconsistencies and send unified alerts. Implemented helper methods to manage alerts and compromised modes.

* Added heartbeat management for the scheduler: integrated a heartbeat file to report liveliness and adjusted the watchdog configuration to account for heartbeat updates.

* Edit webook

* Updated security alert management: fixed the recovery email hidden in the documentation and enabled the conclusion webhook for notifications.

* Improved security alert handling: added structured sending to webhooks for better visibility and updated callback interval in compromised mode.

* Edit conf

* Improved dependency installation: Added the --ignore-scripts option for npm ci and npm install. Updated comments in compose.yaml for clarity.

* Refactor documentation structure and enhance logging:
- Moved documentation files from 'information' to 'docs' directory for better organization.
- Added live logging configuration to support webhook logs with email redaction.
- Updated file paths in configuration and loading functions to accommodate new structure.
- Adjusted scheduler behavior to prevent immediate runs unless explicitly set.
- Improved error handling for account and config file loading.
- Enhanced security incident documentation with detailed recovery steps.

* Fix docs

* Remove outdated documentation on NTFY, Proxy, Scheduling, Security, and Auto-Update configurations; update Browser class to prioritize headless mode based on environment variable.

* Addition of documentation for account management and Totp, Docker Guide, and Update of the Documentation Index.

* Updating Docker documentation: simplification of instructions and adding links to detailed guides. Revision of configuration options and troubleshooting sections.

* Edit

* Edit docs

* Enhance documentation for Scheduler, Security, and Auto-Update features

- Revamped the Scheduler documentation to include detailed features, configuration options, and usage examples.
- Expanded the Security guide with comprehensive incident response strategies, privacy measures, and monitoring practices.
- Updated the Auto-Update section to clarify configuration, methods, and best practices for maintaining system integrity.

* Improved error handling and added crash recovery in the Microsoft Rewards bot. Added configuration for automatic restart and handling of local search queries when trends fail.

* Fixed initial point counting in MicrosoftRewardsBot and improved error handling when sending summaries to webhooks.

* Added unified support for notifications and improved handling of webhook configurations in the normalizeConfig and log functions.

* UPDATE LOGIN

* EDIT LOGIN

* Improved login error handling: added recovery mismatch detection and the ability to switch to password authentication.

* Added a full reference to configuration in the documentation and improved log and error handling in the code.

* Added context management for conclusion webhooks and improved user configuration for notifications.

* Mini edit

* Improved logic for extracting masked emails for more accurate matching during account recovery.
This commit is contained in:
Light
2025-09-26 18:58:33 +02:00
committed by GitHub
parent 02160a07d9
commit 15f62963f8
60 changed files with 8186 additions and 1355 deletions

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
dist/
node_modules/
setup/

View File

@@ -16,10 +16,7 @@ module.exports = {
'@typescript-eslint'
],
'rules': {
'linebreak-style': [
'error',
'unix'
],
'linebreak-style': 'off',
'quotes': [
'error',
'single'

28
.eslintrc.json Normal file
View File

@@ -0,0 +1,28 @@
{
"root": true,
"env": {
"es2021": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.json"],
"sourceType": "module",
"ecmaVersion": 2021
},
"plugins": ["@typescript-eslint", "modules-newline"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"modules-newline/import-declaration-newline": ["warn", { "count": 3 }],
"@typescript-eslint/consistent-type-imports": ["warn", { "prefer": "type-imports" }],
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": "off",
"no-console": ["warn", { "allow": ["error", "warn"] }],
"quotes": ["error", "double", { "avoidEscape": true }],
"linebreak-style": "off"
},
"ignorePatterns": ["dist/**", "node_modules/**", "setup/**"]
}

4
.gitignore vendored
View File

@@ -1,9 +1,11 @@
sessions/
dist/
node_modules/
.github/
package-lock.json
accounts.json
notes
accounts.dev.json
accounts.main.json
.DS_Store
.DS_Store
.playwright-chromium-installed

View File

@@ -1,7 +1,7 @@
###############################################################################
# Stage 1: Builder (compile TypeScript)
###############################################################################
FROM node:18-slim AS builder
FROM node:22-slim AS builder
WORKDIR /usr/src/microsoft-rewards-script
@@ -11,13 +11,13 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
# Copy package manifests
COPY package*.json ./
COPY package*.json tsconfig.json ./
# Conditional install: npm ci if lockfile exists, else npm install
RUN if [ -f package-lock.json ]; then \
npm ci; \
npm ci --ignore-scripts; \
else \
npm install; \
npm install --ignore-scripts; \
fi
# Copy source code
@@ -29,18 +29,16 @@ RUN npm run build
###############################################################################
# Stage 2: Runtime (Playwright image)
###############################################################################
FROM mcr.microsoft.com/playwright:v1.52.0-jammy
FROM node:22-slim AS runtime
WORKDIR /usr/src/microsoft-rewards-script
# Install cron, gettext-base (for envsubst), tzdata noninteractively
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
cron gettext-base tzdata \
&& rm -rf /var/lib/apt/lists/*
# Ensure Playwright uses preinstalled browsers
ENV NODE_ENV=production
ENV TZ=UTC
# Use shared location for Playwright browsers so both 'playwright' and 'rebrowser-playwright' can find them
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# Force headless in container to be compatible with Chromium Headless Shell
ENV FORCE_HEADLESS=1
# Copy package files first for better caching
COPY --from=builder /usr/src/microsoft-rewards-script/package*.json ./
@@ -52,17 +50,14 @@ RUN if [ -f package-lock.json ]; then \
npm install --production --ignore-scripts; \
fi
# Install only Chromium Headless Shell and its OS deps (smaller than full browser set)
# This will install required apt packages internally; we clean up afterwards to keep the image slim.
RUN npx playwright install --with-deps --only-shell \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/*.bin || true
# Copy built application
COPY --from=builder /usr/src/microsoft-rewards-script/dist ./dist
# Copy runtime scripts with proper permissions from the start
COPY --chmod=755 src/run_daily.sh ./src/run_daily.sh
COPY --chmod=644 src/crontab.template /etc/cron.d/microsoft-rewards-cron.template
COPY --chmod=755 entrypoint.sh /usr/local/bin/entrypoint.sh
# Default TZ (overridden by user via environment)
ENV TZ=UTC
# Entrypoint handles TZ, initial run toggle, cron templating & launch
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["sh", "-c", "echo 'Container started; cron is running.'"]
# Default command runs the built-in scheduler; can be overridden by docker-compose
CMD ["npm", "run", "start:schedule"]

341
README.md
View File

@@ -1,191 +1,238 @@
# Microsoft-Rewards-Script
Automated Microsoft Rewards script built with TypeScript, Cheerio and Playwright.
<div align="center">
Under development, however mainly for personal use!
# 🎯 Microsoft Rewards Script V2
```
███╗ ███╗███████╗ ██████╗ ███████╗██╗ ██╗ █████╗ ██████╗ ██████╗ ███████╗
████╗ ████║██╔════╝ ██╔══██╗██╔════╝██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔════╝
██╔████╔██║███████╗ ██████╔╝█████╗ ██║ █╗ ██║███████║██████╔╝██║ ██║███████╗
██║╚██╔╝██║╚════██║ ██╔══██╗██╔══╝ ██║███╗██║██╔══██║██╔══██╗██║ ██║╚════██║
██║ ╚═╝ ██║███████║ ██║ ██║███████╗╚███╔███╔╝██║ ██║██║ ██║██████╔╝███████║
╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝
```
**🤖 Intelligent automation meets Microsoft Rewards**
*Earn points effortlessly while you sleep*
[Legacy-1.5.3](https://github.com/LightZirconite/Microsoft-Rewards-Script-Private/tree/Legacy-1.5.3)
[![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Node.js](https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/)
[![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com/)
[![Playwright](https://img.shields.io/badge/Playwright-2EAD33?style=for-the-badge&logo=playwright&logoColor=white)](https://playwright.dev/)
<a href="https://github.com/TheNetsky/Microsoft-Rewards-Script/graphs/contributors">
<img alt="Contributors" src="https://img.shields.io/github/contributors/TheNetsky/Microsoft-Rewards-Script?style=for-the-badge&label=Contributors&color=FF6B6B&labelColor=4ECDC4" />
</a>
<img alt="Stars" src="https://img.shields.io/github/stars/TheNetsky/Microsoft-Rewards-Script?style=for-the-badge&color=FFD93D&labelColor=6BCF7F" />
<img alt="Version" src="https://img.shields.io/badge/Version-2.0-9B59B6?style=for-the-badge&labelColor=3498DB" />
</div>
---
## 🚀 Quick Setup (Recommended)
<div align="center">
**The easiest way to get started - just download and run!**
## 🚀 **Big Update Alert — V2 is here!**
1. **Download or clone** the source code
2. **Run the setup script:**
**Windows:** Double-click `setup/setup.bat` or run it from command line
**Linux/macOS/WSL:** `bash setup/setup.sh`
**Alternative (any platform):** `npm run setup`
<table>
<tr>
<td width="33%" align="center">
<img src="https://github.com/TheNetsky.png" width="80" style="border-radius: 50%;" /><br />
<strong><a href="https://github.com/TheNetsky/">TheNetsky</a></strong> 🙌<br />
<em>Foundation Architect</em><br />
<sub>Building the massive foundation</sub>
</td>
<td width="33%" align="center">
<img src="https://github.com/mgrimace.png" width="80" style="border-radius: 50%;" /><br />
<strong><a href="https://github.com/mgrimace">Mgrimace</a></strong> 🔥<br />
<em>Active Developer</em><br />
<sub>Regular updates & <a href="./docs/ntfy.md">NTFY mode</a></sub>
</td>
<td width="33%" align="center">
<img src="https://github.com/LightZirconite.png" width="80" style="border-radius: 50%;" /><br />
<strong><a href="https://github.com/LightZirconite">Light</a></strong><br />
<em>V2 Mastermind</em><br />
<sub>Massive feature overhaul</sub>
</td>
</tr>
</table>
3. **Follow the prompts:** The setup script will automatically:
- Rename `accounts.example.json` to `accounts.json`
- Ask you to enter your Microsoft account credentials
- Remind you to review configuration options in `config.json`
- Install all dependencies (`npm install`)
- Build the project (`npm run build`)
- Optionally start the script immediately
**💡 Welcome to V2 — There are honestly so many changes that even I can't list them all!**
*Trust me, you've got a **massive upgrade** in front of you. Enjoy the ride!* 🎢
**That's it!** The setup script handles everything for you.
</div>
---
## ⚙️ Advanced Setup Options
## 🎯 **What Does This Script Do?**
### Nix Users
1. Get [Nix](https://nixos.org/)
2. Run `./run.sh`
3. Done!
<div align="center">
### Manual Setup (Troubleshooting)
If the automatic setup script doesn't work for your environment:
**Automatically earn Microsoft Rewards points by completing daily tasks:**
- 🔍 **Daily Searches** — Desktop & Mobile Bing searches
- 📅 **Daily Set** — Complete daily quizzes and activities
- 🎁 **Promotions** — Bonus point opportunities
- 🃏 **Punch Cards** — Multi-day reward challenges
-**Daily Check-in** — Simple daily login rewards
- 📚 **Read to Earn** — News article reading points
1. Manually rename `src/accounts.example.json` to `src/accounts.json`
2. Add your Microsoft account details to `accounts.json`
3. Customize `src/config.json` to your preferences
4. Install dependencies: `npm install`
5. Build the project: `npm run build`
6. Start the script: `npm run start`---
*All done automatically while you sleep! 💤*
## 🐳 Docker Setup (Experimental)
For automated scheduling and containerized deployment.
### Before Starting
- Remove `/node_modules` and `/dist` folders if you previously built locally
- Remove old Docker volumes if upgrading from version 1.4 or earlier
- Old `accounts.json` files can be reused
### Quick Docker Setup
1. **Download source code** and configure `accounts.json`
2. **Edit `config.json`** - ensure `"headless": true`
3. **Customize `compose.yaml`:**
- Set your timezone (`TZ` variable)
- Configure schedule (`CRON_SCHEDULE`) - use [crontab.guru](https://crontab.guru) for help
- Optional: Set `RUN_ON_START=true` for immediate execution
4. **Start container:** `docker compose up -d`
5. **Monitor logs:** `docker logs microsoft-rewards-script`
**Note:** The container adds 550 minutes random delay to scheduled runs for more natural behavior.
</div>
---
## 📋 Usage Notes
## ⚡ Quick Start
- **Browser Instances:** If you stop the script without closing browser windows (headless=false), use Task Manager or `npm run kill-chrome-win` to clean up
- **Automation Scheduling:** Run at least twice daily, set `"runOnZeroPoints": false` to skip when no points available
- **Multiple Accounts:** The script supports clustering - configure `clusters` in `config.json`
```bash
# 🪟 Windows — One command setup
setup/setup.bat
---
## ⚙️ Configuration Reference
# 🐧 Linux/macOS/WSL
bash setup/setup.sh
Customize behavior by editing `src/config.json`:
# 🌍 Any platform
npm run setup
```
### Core Settings
| Setting | Description | Default |
|---------|-------------|---------|
| `baseURL` | Microsoft Rewards page URL | `https://rewards.bing.com` |
| `sessionPath` | Session/fingerprint storage location | `sessions` |
| `headless` | Run browser in background | `false` (visible) |
| `parallel` | Run mobile/desktop tasks simultaneously | `true` |
| `runOnZeroPoints` | Continue when no points available | `false` |
| `clusters` | Number of concurrent account instances | `1` |
**That's it!** The setup wizard configures accounts, installs dependencies, builds the project, and starts earning points.
### Fingerprint Settings
| Setting | Description | Default |
|---------|-------------|---------|
| `saveFingerprint.mobile` | Reuse mobile browser fingerprint | `false` |
| `saveFingerprint.desktop` | Reuse desktop browser fingerprint | `false` |
<details>
<summary><strong>📖 Manual Setup</strong></summary>
### Task Settings
| Setting | Description | Default |
|---------|-------------|---------|
| `workers.doDailySet` | Complete daily set activities | `true` |
| `workers.doMorePromotions` | Complete promotional offers | `true` |
| `workers.doPunchCards` | Complete punchcard activities | `true` |
| `workers.doDesktopSearch` | Perform desktop searches | `true` |
| `workers.doMobileSearch` | Perform mobile searches | `true` |
| `workers.doDailyCheckIn` | Complete daily check-in | `true` |
| `workers.doReadToEarn` | Complete read-to-earn activities | `true` |
```bash
# 1⃣ Configure your Microsoft accounts
cp src/accounts.example.json src/accounts.json
# Edit accounts.json with your credentials
### Search Settings
| Setting | Description | Default |
|---------|-------------|---------|
| `searchOnBingLocalQueries` | Use local queries vs. fetched | `false` |
| `searchSettings.useGeoLocaleQueries` | Generate location-based queries | `false` |
| `searchSettings.scrollRandomResults` | Randomly scroll search results | `true` |
| `searchSettings.clickRandomResults` | Click random result links | `true` |
| `searchSettings.searchDelay` | Delay between searches (min/max) | `3-5 minutes` |
| `searchSettings.retryMobileSearchAmount` | Mobile search retry attempts | `2` |
# 2⃣ Install & Build
npm install && npm run build
### Advanced Settings
| Setting | Description | Default |
|---------|-------------|---------|
| `globalTimeout` | Action timeout duration | `30s` |
| `logExcludeFunc` | Functions to exclude from logs | `SEARCH-CLOSE-TABS` |
| `webhookLogExcludeFunc` | Functions to exclude from webhooks | `SEARCH-CLOSE-TABS` |
| `proxy.proxyGoogleTrends` | Proxy Google Trends requests | `true` |
| `proxy.proxyBingTerms` | Proxy Bing Terms requests | `true` |
# 3⃣ Run once or start scheduler
npm start # Single run
npm run start:schedule # Automated daily runs
```
### Webhook Settings
| Setting | Description | Default |
|---------|-------------|---------|
| `webhook.enabled` | Enable Discord notifications | `false` |
| `webhook.url` | Discord webhook URL | `null` |
| `conclusionWebhook.enabled` | Enable summary-only webhook | `false` |
| `conclusionWebhook.url` | Summary webhook URL | `null` |
</details>
---
## ✨ Features
## 📑 Documentation
**Account Management:**
- ✅ Multi-Account Support
- ✅ Session Storage & Persistence
- ✅ 2FA Support
- ✅ Passwordless Login Support
| Topic | Description |
|-------|-------------|
| **[🚀 Getting Started](./docs/getting-started.md)** | Complete setup guide from zero to running |
| **[👤 Accounts & 2FA](./docs/accounts.md)** | Microsoft account setup + TOTP authentication |
| **[🐳 Docker](./docs/docker.md)** | Containerized deployment with slim headless image |
| **[⏰ Scheduling](./docs/schedule.md)** | Automated daily runs with built-in scheduler |
| **[🛠️ Diagnostics](./docs/diagnostics.md)** | Troubleshooting, error capture, and logs |
| **[⚙️ Configuration](./docs/config.md)** | Full config.json reference |
**Automation & Control:**
- ✅ Headless Browser Operation
- ✅ Clustering Support (Multiple accounts simultaneously)
- ✅ Configurable Task Selection
- ✅ Proxy Support
- ✅ Automatic Scheduling (Docker)
**[📚 Full Documentation Index →](./docs/index.md)**
**Search & Activities:**
- ✅ Desktop & Mobile Searches
- ✅ Microsoft Edge Search Simulation
- ✅ Geo-Located Search Queries
- ✅ Emulated Scrolling & Link Clicking
- ✅ Daily Set Completion
- ✅ Promotional Activities
- ✅ Punchcard Completion
- ✅ Daily Check-in
- ✅ Read to Earn Activities
## 🎮 Commands
**Quiz & Interactive Content:**
- ✅ Quiz Solving (10 & 30-40 point variants)
- ✅ This Or That Quiz (Random answers)
- ✅ ABC Quiz Solving
- ✅ Poll Completion
- ✅ Click Rewards
```bash
# 🚀 Run the automation once
npm start
**Notifications & Monitoring:**
- ✅ Discord Webhook Integration
- ✅ Dedicated Summary Webhook
- ✅ Comprehensive Logging
- ✅ Docker Support with Monitoring
# <20> Start automated daily scheduler
npm run start:schedule
# 💳 Manual points redemption mode
npm start -- -buy your@email.com
# <20> Deploy with Docker
docker compose up -d
# <20> Development mode
npm run dev
```
---
## ✨ Key Features
<div align="center">
| Feature | Description |
|---------|-------------|
| **🔐 Multi-Account** | Support multiple Microsoft accounts with 2FA |
| **🤖 Human-like** | Natural delays, scrolling, clicking patterns |
| **📱 Cross-Platform** | Desktop + Mobile search automation |
| **🎯 Smart Activities** | Quizzes, polls, daily sets, punch cards |
| **🔔 Notifications** | Discord webhooks + NTFY push alerts |
| **🐳 Docker Ready** | Slim headless container deployment |
| **🛡️ Resilient** | Session persistence, job state recovery |
| **🕸️ Proxy Support** | Per-account proxy configuration |
</div>
---
## 🚀 Advanced Features
**[💳 Buy Mode](./docs/buy-mode.md)** — Manual redemption with live points monitoring
**[🧠 Humanization](./docs/humanization.md)** — Advanced anti-detection patterns
**[📊 Diagnostics](./docs/diagnostics.md)** — Error capture with screenshots/HTML
**[🔗 Webhooks](./docs/conclusionwebhook.md)** — Rich Discord notifications
**[📱 NTFY](./docs/ntfy.md)** — Push notifications to your phone
---
## 📚 Documentation & Support
<div align="center">
**📖 [Complete Documentation Index](./docs/index.md)**
</div>
### Essential Guides
- **[Getting Started](./docs/getting-started.md)** — Zero to running in minutes
- **[Accounts Setup](./docs/accounts.md)** — Microsoft accounts + 2FA configuration
- **[Docker Guide](./docs/docker.md)** — Container deployment
- **[Scheduling](./docs/schedule.md)** — Automated daily runs
- **[Troubleshooting](./docs/diagnostics.md)** — Fix common issues
### Advanced Topics
- **[Humanization](./docs/humanization.md)** — Anti-detection features
- **[Notifications](./docs/ntfy.md)** — Push alerts & Discord webhooks
- **[Proxy Setup](./docs/proxy.md)** — Network configuration
- **[Buy Mode](./docs/buy-mode.md)** — Manual redemption tracking
---
## 🤝 Community
<div align="center">
[![Discord](https://img.shields.io/badge/💬_Join_Discord-7289DA?style=for-the-badge&logo=discord)](https://discord.gg/KRBFxxsU)
[![GitHub](https://img.shields.io/badge/⭐_Star_Project-yellow?style=for-the-badge&logo=github)](https://github.com/TheNetsky/Microsoft-Rewards-Script)
**Found a bug?** [Report an issue](https://github.com/TheNetsky/Microsoft-Rewards-Script/issues)
**Have suggestions?** [Start a discussion](https://github.com/TheNetsky/Microsoft-Rewards-Script/discussions)
</div>
---
<div align="center">
## ⚠️ Disclaimer
**Use at your own risk!** Your Microsoft Rewards account may be suspended or banned when using automation scripts.
This project is for educational purposes only. Use at your own risk. Microsoft may suspend accounts that use automation tools. The authors are not responsible for any account actions taken by Microsoft.
This script is provided for educational purposes. The authors are not responsible for any account actions taken by Microsoft.
**🎯 Contributors**
<a href="https://github.com/TheNetsky/Microsoft-Rewards-Script/graphs/contributors">
<img src="https://contrib.rocks/image?repo=TheNetsky/Microsoft-Rewards-Script" alt="Contributors" />
</a>
*Made with ❤️ by the community • Happy automating! 🎉*
</div>
---
## 🤝 Contributing
This project is primarily for personal use but contributions are welcome. Please ensure any changes maintain compatibility with the existing configuration system.
<img width="1536" height="1024" alt="msn-rw" src="https://github.com/user-attachments/assets/4e396ab3-5292-4948-9778-7b385d751e4d" />

85
SECURITY.md Normal file
View File

@@ -0,0 +1,85 @@
# Security & Privacy Policy
Hi there! 👋 Thanks for caring about security and privacy — we do too. This document explains how this project approaches data handling, security practices, and how to report issues responsibly.
## TL;DR
- We do not collect, phone-home, or exfiltrate your data. No hidden telemetry. 🚫📡
- Your credentials stay on your machine (or in your container volumes). 🔒
- Sessions/cookies are stored locally to reduce re-login friction. 🍪
- Use at your own risk. Microsoft may take action on accounts that use automation.
## What this project does (and doesnt)
This is a local automation tool that drives a browser (Playwright) to perform Microsoft Rewards activities. By default:
- It reads configuration from local files (e.g., `src/config.json`, `src/accounts.json`).
- It can save session data (cookies and optional fingerprints) locally under `./src/browser/<sessionPath>/<email>/`.
- It can send optional notifications/webhooks if you enable them and provide a URL.
It does not:
- Send your accounts or secrets to any third-party service by default.
- Embed any “phone-home” or analytics endpoints.
- Include built-in monetization, miners, or adware. 🚫🐛
## Data handling and storage
- Accounts: You control the `accounts.json` file. Keep it safe. Consider environment variables or secrets managers in CI/CD.
- Sessions: Cookies are stored locally to speed up login. You can delete them anytime by removing the session folder.
- Fingerprints: If you enable fingerprint saving, they are saved locally only. Disable this feature if you prefer ephemeral fingerprints.
- Logs/Reports: Diagnostic artifacts and daily summaries are written to the local `reports/` directory.
- Webhooks/Notifications: If enabled, we send only the minimal information necessary (e.g., summary text, embed fields) to the endpoint you configured.
Tip: For Docker, mount a dedicated data volume for sessions and reports so you can manage them easily. 📦
## Credentials and secrets
- Do not commit secrets. Use `src/accounts.json` locally or set `ACCOUNTS_JSON`/`ACCOUNTS_FILE` via environment variables when running in containers.
- Consider using OS keychains or external secret managers where possible.
- TOTP: If you include a Base32 TOTP secret per account, it remains local and is used strictly during login challenge flows.
## Buy Mode safety
Buy Mode opens a monitor tab (read-only points polling) and a separate user tab for your manual actions. The monitor tab doesnt redeem or click on your behalf — it just reads dashboard data to keep totals up to date. 🛍️
## Responsible disclosure
We value coordinated disclosure. If you find a security issue:
1. Please report it privately first via an issue marked “Security” with a note to request contact details, or by contacting the repository owner directly if available.
2. Provide a minimal reproduction and version info.
3. We will acknowledge within a reasonable timeframe and work on a fix. 🙏
Please do not open public issues with sensitive details before we have had a chance to remediate.
## Scope and assumptions
- This project is open-source and runs on your infrastructure (local machine or container). You are responsible for host hardening and network policies.
- Automation can violate terms of service. You assume all responsibility for how you use this tool.
- Browsers and dependencies evolve. Keep the project and your runtime up to date.
## Dependency and update policy
- We pin key dependencies where practical and avoid risky postinstall scripts in production builds.
- Periodic updates are encouraged. The project includes an optional auto-update helper. Review changes before enabling in sensitive environments.
- Use Playwright official images when running in containers to receive timely browser security updates. 🛡️
## Safe use guidelines
- Run with least privileges. In Docker, prefer non-root where feasible and set `no-new-privileges` if supported.
- Limit outbound network access if your threat model requires it.
- Rotate credentials periodically and revoke unused secrets.
- Clean up diagnostics and reports if they contain sensitive metadata.
## Privacy statement
We dont collect personal data. The repository does not embed analytics. Any processing done by this tool happens locally or against the Microsoft endpoints it drives on your behalf.
If you enable third-party notifications (Discord, NTFY, etc.), data sent there is under your control and subject to those services privacy policies.
## Contact
To report a security issue or ask a question, please open an issue with the “Security” label and well follow up with a private channel. You can also reach out to the project owner/maintainers via GitHub if contact details are listed. 💬
— Stay safe and have fun automating! ✨

View File

@@ -6,37 +6,25 @@ services:
# Volume mounts: Specify a location where you want to save the files on your local machine.
volumes:
- ./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
- ./src/accounts.json:/usr/src/microsoft-rewards-script/accounts.json:ro
- ./src/config.json:/usr/src/microsoft-rewards-script/config.json:ro
- ./sessions:/usr/src/microsoft-rewards-script/sessions # Optional, saves your login session
environment:
TZ: "America/Toronto" # Set your timezone for proper scheduling
TZ: "America/Toronto" # Set your timezone for proper scheduling (used by image and scheduler)
NODE_ENV: "production"
CRON_SCHEDULE: "0 7,16,20 * * *" # Customize your schedule, use crontab.guru for formatting
RUN_ON_START: "true" # Runs the script immediately on container startup
# Force headless when running in Docker (uses Chromium Headless Shell only)
FORCE_HEADLESS: "1"
#SCHEDULER_DAILY_JITTER_MINUTES_MIN: "2"
#SCHEDULER_DAILY_JITTER_MINUTES_MAX: "10"
# Watchdog timeout per pass (minutes, default 180)
#SCHEDULER_PASS_TIMEOUT_MINUTES: "180"
# Run pass in child process (default true). Set to "false" to disable for debugging.
#SCHEDULER_FORK_PER_PASS: "true"
# Add scheduled start-time randomization (uncomment to customize or disable, default: enabled)
#MIN_SLEEP_MINUTES: "5"
#MAX_SLEEP_MINUTES: "50"
SKIP_RANDOM_SLEEP: "false"
# Optionally set how long to wait before killing a stuck script run (prevents blocking future runs, default: 8 hours)
#STUCK_PROCESS_TIMEOUT_HOURS: "8"
# Optional resource limits for the container
mem_limit: 4g
cpus: 2
# Health check - monitors if cron daemon is running to ensure scheduled jobs can execute
# Container marked unhealthy if cron process dies
healthcheck:
test: ["CMD", "sh", "-c", "pgrep cron > /dev/null || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
# Security hardening
security_opt:
- no-new-privileges:true
- no-new-privileges:true
# Use the built-in scheduler by default; override with `command:` for one-shot runs
command: ["npm", "run", "start:schedule"]

94
docs/accounts.md Normal file
View File

@@ -0,0 +1,94 @@
# 👤 Accounts & TOTP (2FA)
<div align="center">
**🔐 Secure Microsoft account setup with 2FA support**
*Everything you need to configure authentication*
</div>
---
## 📍 File Location & Options
The bot needs Microsoft account credentials to log in and complete activities. Here's how to provide them:
### **Default Location**
```
src/accounts.json
```
### **Environment Overrides** (Docker/CI)
- **`ACCOUNTS_FILE`** — Path to accounts file (e.g., `/data/accounts.json`)
- **`ACCOUNTS_JSON`** — Inline JSON string (useful for CI/CD)
The loader tries: `ACCOUNTS_JSON``ACCOUNTS_FILE` → default locations in project root.
## Schema
Each account has at least `email` and `password`.
```
{
"accounts": [
{
"email": "email_1",
"password": "password_1",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
}
]
}
```
- `totp` (optional): Base32 secret for Timebased OneTime Passwords (2FA). If set, the bot generates the 6digit code automatically when asked by Microsoft.
- `recoveryEmail` (optional): used to validate masked recovery prompts.
- `proxy` (optional): peraccount proxy config. See the [Proxy guide](./proxy.md).
## How to get your TOTP secret
1) In your Microsoft account security settings, add an authenticator app.
2) When shown the QR code, choose the option to enter the code manually — this reveals the Base32 secret.
3) Copy that secret (only the text after `secret=` if you have an otpauth URL) into the `totp` field.
Security tips:
- Never commit real secrets to Git.
- Prefer `ACCOUNTS_FILE` or `ACCOUNTS_JSON` in production.
## Examples
- Single account, no 2FA:
```
{"accounts":[{"email":"a@b.com","password":"pass","totp":"","recoveryEmail":"","proxy":{"proxyAxios":true,"url":"","port":0,"username":"","password":""}}]}
```
- Single account with TOTP secret:
```
{"accounts":[{"email":"a@b.com","password":"pass","totp":"JBSWY3DPEHPK3PXP","recoveryEmail":"","proxy":{"proxyAxios":true,"url":"","port":0,"username":"","password":""}}]}
```
- Multiple accounts:
```
{"accounts":[
{"email":"a@b.com","password":"pass","totp":"","recoveryEmail":"" ,"proxy":{"proxyAxios":true,"url":"","port":0,"username":"","password":""}},
{"email":"c@d.com","password":"pass","totp":"","recoveryEmail":"" ,"proxy":{"proxyAxios":true,"url":"","port":0,"username":"","password":""}}
]}
```
## Troubleshooting
- “accounts file not found”: ensure the file exists, or set `ACCOUNTS_FILE` to the correct path.
- 2FA prompt not filled: verify `totp` is a valid Base32 secret; time on the host/container should be correct.
- Locked account: the bot will log and skip; resolve manually then reenable.
---
## 🔗 Related Guides
- **[Getting Started](./getting-started.md)** — Initial setup and configuration
- **[Docker](./docker.md)** — Container deployment with accounts
- **[Security](./security.md)** — Account protection and incident response
- **[NTFY Notifications](./ntfy.md)** — Get alerts for login issues

206
docs/buy-mode.md Normal file
View File

@@ -0,0 +1,206 @@
# 💳 Buy Mode
<div align="center">
**🛒 Manual redemption with live point monitoring**
*Track your spending while maintaining full control*
</div>
---
## 🎯 What is Buy Mode?
Buy Mode allows you to **manually redeem rewards** while the script **passively monitors** your point balance. Perfect for safe redemptions without automation interference.
### **Key Features**
- 👀 **Passive monitoring** — No clicks or automation
- 🔄 **Real-time tracking** — Instant spending alerts
- 📱 **Live notifications** — Discord/NTFY integration
- ⏱️ **Configurable duration** — Set your own time limit
- 📊 **Session summary** — Complete spending report
---
## 🚀 How to Use
### **Command Options**
```bash
# Monitor specific account
npm start -- -buy your@email.com
# Monitor first account in accounts.json
npm start -- -buy
# Alternative: Enable in config (see below)
```
### **What Happens Next**
1. **🖥️ Dual Tab System Opens**
- **Monitor Tab** — Background monitoring (auto-refresh)
- **User Tab** — Your control for redemptions/browsing
2. **📊 Passive Point Tracking**
- Reads balance every ~10 seconds
- Detects spending when points decrease
- Zero interference with your browsing
3. **🔔 Real-time Alerts**
- Instant notifications when spending detected
- Shows amount spent + current balance
- Tracks cumulative session spending
---
## ⚙️ Configuration
### **Set Duration in Config**
Add to `src/config.json`:
```json
{
"buyMode": {
"enabled": false,
"maxMinutes": 45
}
}
```
| Setting | Default | Description |
|---------|---------|-------------|
| `enabled` | `false` | Force buy mode without CLI flag |
| `maxMinutes` | `45` | Auto-stop after N minutes |
### **Enable Notifications**
Buy mode works with existing notification settings:
```json
{
"conclusionWebhook": {
"enabled": true,
"url": "https://discord.com/api/webhooks/YOUR_URL"
},
"ntfy": {
"enabled": true,
"url": "https://ntfy.sh",
"topic": "rewards"
}
}
```
---
## 🖥️ Terminal Output
### **Startup**
```
███╗ ███╗███████╗ ██████╗ ██╗ ██╗██╗ ██╗
████╗ ████║██╔════╝ ██╔══██╗██║ ██║╚██╗ ██╔╝
██╔████╔██║███████╗ ██████╔╝██║ ██║ ╚████╔╝
██║╚██╔╝██║╚════██║ ██╔══██╗██║ ██║ ╚██╔╝
██║ ╚═╝ ██║███████║ ██████╔╝╚██████╔╝ ██║
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝
Manual Purchase Mode • Passive Monitoring
[BUY-MODE] Opening dual-tab system for safe redemptions...
[BUY-MODE] Monitor tab: Background point tracking
[BUY-MODE] User tab: Your control for purchases/browsing
```
### **Live Monitoring**
```
[BUY-MODE] Current balance: 15,000 points
[BUY-MODE] 🛒 Spending detected: -500 points (new balance: 14,500)
[BUY-MODE] Session total spent: 500 points
```
---
## 📋 Use Cases
| Scenario | Benefit |
|----------|---------|
| **🎁 Gift Card Redemption** | Track exact point cost while redeeming safely |
| **🛍️ Microsoft Store Purchases** | Monitor spending across multiple items |
| **✅ Account Verification** | Ensure point changes match expected activity |
| **📊 Spending Analysis** | Real-time tracking of reward usage patterns |
| **🔒 Safe Browsing** | Use Microsoft Rewards normally with monitoring |
---
## 🛠️ Troubleshooting
| Problem | Solution |
|---------|----------|
| **Monitor tab closes** | Script auto-reopens in background |
| **No spending alerts** | Check webhook/NTFY config; verify notifications enabled |
| **Session too short** | Increase `maxMinutes` in config |
| **Login failures** | Verify account credentials in `accounts.json` |
| **Points not updating** | Check internet connection; try refresh |
---
## 🔗 Related Guides
- **[Getting Started](./getting-started.md)** — Initial setup and configuration
- **[Accounts & 2FA](./accounts.md)** — Microsoft account setup
- **[NTFY Notifications](./ntfy.md)** — Mobile push alerts
- **[Discord Webhooks](./conclusionwebhook.md)** — Server notifications
## Terminal Output
When you start buy mode, you'll see:
```
███╗ ███╗███████╗ ██████╗ ██╗ ██╗██╗ ██╗
████╗ ████║██╔════╝ ██╔══██╗██║ ██║╚██╗ ██╔╝
██╔████╔██║███████╗ ██████╔╝██║ ██║ ╚████╔╝
██║╚██╔╝██║╚════██║ ██╔══██╗██║ ██║ ╚██╔╝
██║ ╚═╝ ██║███████║ ██████╔╝╚██████╔╝ ██║
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝
Manual Purchase Mode • Passive Monitoring
[BUY-MODE] Buy mode ENABLED for your@email.com. We'll open 2 tabs:
(1) monitor tab (auto-refreshes), (2) your browsing tab
[BUY-MODE] The monitor tab may refresh every ~10s. Use the other tab...
[BUY-MODE] Opened MONITOR tab (auto-refreshes to track points)
[BUY-MODE] Opened USER tab (use this one to redeem/purchase freely)
[BUY-MODE] Logged in as your@email.com. Buy mode is active...
```
During monitoring:
```
[BUY-MODE] Detected spend: -500 points (current: 12,500)
[BUY-MODE] Monitor tab was closed; reopening in background...
```
## Features
-**Non-intrusive**: No clicks or navigation in your browsing tab
-**Real-time alerts**: Instant notifications when points are spent
-**Auto-recovery**: Reopens monitor tab if accidentally closed
-**Webhook support**: Works with Discord and NTFY notifications
-**Configurable duration**: Set your own monitoring time limit
-**Session tracking**: Complete summary of spending activity
## Use Cases
- **Manual redemptions**: Redeem gift cards or rewards while tracking spending
- **Account verification**: Monitor point changes during manual account activities
- **Spending analysis**: Track how points are being used in real-time
- **Safe browsing**: Use Microsoft Rewards normally while monitoring balance
## Notes
- Monitor tab runs in background and may refresh periodically
- Your main browsing tab is completely under your control
- Session data is saved automatically for future script runs
- Buy mode works with existing notification configurations
- No automation or point collection occurs in this mode
## Troubleshooting
- **Monitor tab closed**: Script automatically reopens it in background
- **No notifications**: Check webhook/NTFY configuration in `config.json`
- **Session timeout**: Increase `maxMinutes` if you need longer monitoring
- **Login issues**: Ensure account credentials are correct in `accounts.json`

389
docs/conclusionwebhook.md Normal file
View File

@@ -0,0 +1,389 @@
# 📊 Discord Conclusion Webhook
<div align="center">
**🎯 Comprehensive session summaries via Discord**
*Complete execution reports delivered instantly*
</div>
---
## 🎯 What is the Conclusion Webhook?
The conclusion webhook sends a **detailed summary notification** at the end of each script execution via Discord, providing a complete overview of the session's results across all accounts.
### **Key Features**
- 📊 **Session overview** — Total accounts processed, success/failure counts
- 💎 **Points summary** — Starting points, earned points, final totals
- ⏱️ **Performance metrics** — Execution times, efficiency statistics
-**Error reporting** — Issues encountered during execution
- 💳 **Buy mode detection** — Point spending alerts and tracking
- 🎨 **Rich embeds** — Color-coded, well-formatted Discord messages
---
## ⚙️ Configuration
### **Basic Setup**
```json
{
"notifications": {
"conclusionWebhook": {
"enabled": true,
"url": "https://discord.com/api/webhooks/123456789/abcdef-webhook-token-here"
}
}
}
```
### **Configuration Options**
| Setting | Description | Example |
|---------|-------------|---------|
| `enabled` | Enable conclusion webhook | `true` |
| `url` | Discord webhook URL | Full webhook URL from Discord |
---
## 🚀 Discord Setup
### **Step 1: Create Webhook**
1. **Open Discord** and go to your server
2. **Right-click** on the channel for notifications
3. **Select "Edit Channel"**
4. **Go to "Integrations" tab**
5. **Click "Create Webhook"**
### **Step 2: Configure Webhook**
- **Name** — "MS Rewards Summary"
- **Avatar** — Upload rewards icon (optional)
- **Channel** — Select appropriate channel
- **Copy webhook URL**
### **Step 3: Add to Config**
```json
{
"notifications": {
"conclusionWebhook": {
"enabled": true,
"url": "YOUR_COPIED_WEBHOOK_URL_HERE"
}
}
}
```
---
## 📋 Message Format
### **Rich Embed Summary**
#### **Header Section**
```
🎯 Microsoft Rewards Summary
⏰ Completed at 2025-01-20 14:30:15
📈 Total Runtime: 25m 36s
```
#### **Account Statistics**
```
📊 Accounts: 3 • 0 with issues
```
#### **Points Overview**
```
💎 Points: 15,230 → 16,890 (+1,660)
```
#### **Performance Metrics**
```
⏱️ Average Duration: 8m 32s
📈 Cumulative Runtime: 25m 36s
```
#### **Buy Mode Detection** (if applicable)
```
💳 Buy Mode Activity Detected
Total Spent: 1,200 points across 2 accounts
```
### **Account Breakdown**
#### **Successful Account**
```
👤 user@example.com
Points: 5,420 → 6,140 (+720)
Duration: 7m 23s
Status: ✅ Completed successfully
```
#### **Failed Account**
```
👤 problem@example.com
Points: 3,210 → 3,210 (+0)
Duration: 2m 15s
Status: ❌ Failed - Login timeout
```
#### **Buy Mode Account**
```
💳 spender@example.com
Session Spent: 500 points
Available: 12,500 points
Status: 💳 Purchase activity detected
```
---
## 📊 Message Examples
### **Successful Session**
```discord
🎯 Microsoft Rewards Summary
📊 Accounts: 3 • 0 with issues
💎 Points: 15,230 → 16,890 (+1,660)
⏱️ Average Duration: 8m 32s
📈 Cumulative Runtime: 25m 36s
👤 user1@example.com
Points: 5,420 → 6,140 (+720)
Duration: 7m 23s
Status: ✅ Completed successfully
👤 user2@example.com
Points: 4,810 → 5,750 (+940)
Duration: 9m 41s
Status: ✅ Completed successfully
👤 user3@example.com
Points: 5,000 → 5,000 (+0)
Duration: 8m 32s
Status: ✅ Completed successfully
```
### **Session with Issues**
```discord
🎯 Microsoft Rewards Summary
📊 Accounts: 3 • 1 with issues
💎 Points: 15,230 → 15,950 (+720)
⏱️ Average Duration: 6m 15s
📈 Cumulative Runtime: 18m 45s
👤 user1@example.com
Points: 5,420 → 6,140 (+720)
Duration: 7m 23s
Status: ✅ Completed successfully
👤 user2@example.com
Points: 4,810 → 4,810 (+0)
Duration: 2m 15s
Status: ❌ Failed - Login timeout
👤 user3@example.com
Points: 5,000 → 5,000 (+0)
Duration: 9m 07s
Status: ⚠️ Partially completed - Quiz failed
```
### **Buy Mode Detection**
```discord
🎯 Microsoft Rewards Summary
📊 Accounts: 2 • 0 with issues
💎 Points: 25,500 → 24,220 (-1,280)
💳 Buy Mode Activity Detected
Total Spent: 1,500 points across 1 account
👤 buyer@example.com
Points: 15,000 → 13,500 (-1,500)
Duration: 12m 34s
Status: 💳 Buy mode detected
Activities: Purchase completed, searches skipped
👤 normal@example.com
Points: 10,500 → 10,720 (+220)
Duration: 8m 45s
Status: ✅ Completed successfully
```
---
## 🤝 Integration with Other Notifications
### **Webhook vs Conclusion Webhook**
| Feature | Real-time Webhook | Conclusion Webhook |
|---------|------------------|-------------------|
| **Timing** | During execution | End of session only |
| **Content** | Errors, warnings, progress | Comprehensive summary |
| **Frequency** | Multiple per session | One per session |
| **Purpose** | Immediate alerts | Session overview |
### **Recommended Combined Setup**
```json
{
"notifications": {
"webhook": {
"enabled": true,
"url": "https://discord.com/api/webhooks/.../real-time"
},
"conclusionWebhook": {
"enabled": true,
"url": "https://discord.com/api/webhooks/.../summary"
},
"ntfy": {
"enabled": true,
"url": "https://ntfy.sh",
"topic": "rewards-mobile"
}
}
}
```
### **Benefits of Combined Setup**
-**Real-time webhook** — Immediate error alerts
- 📊 **Conclusion webhook** — Comprehensive session summary
- 📱 **NTFY** — Mobile notifications for critical issues
---
## 🎛️ Advanced Configuration
### **Multiple Webhooks**
```json
{
"notifications": {
"webhook": {
"enabled": true,
"url": "https://discord.com/api/webhooks/.../errors-channel"
},
"conclusionWebhook": {
"enabled": true,
"url": "https://discord.com/api/webhooks/.../summary-channel"
}
}
}
```
### **Channel Organization**
#### **Recommended Discord Structure**
- **#rewards-errors** — Real-time error notifications (webhook)
- **#rewards-summary** — End-of-run summaries (conclusionWebhook)
- **#rewards-logs** — Detailed text logs (manual uploads)
#### **Channel Settings**
- **Notification settings** — Configure per your preference
- **Webhook permissions** — Limit to specific channels
- **Message history** — Enable for tracking trends
---
## 🔒 Security & Privacy
### **Webhook Security Best Practices**
- 🔐 Use **dedicated Discord server** for notifications
- 🎯 **Limit permissions** to specific channels only
- 🔄 **Regenerate URLs** if compromised
- 🚫 **Don't share** webhook URLs publicly
### **Data Transmission**
-**Summary statistics** only
-**Points and email** addresses
-**No passwords** or sensitive tokens
-**No personal information** beyond emails
### **Data Retention**
- 💾 **Discord stores** messages per server settings
- 🗑️ **No local storage** by the script
- ✂️ **Manual deletion** possible anytime
- 📝 **Webhook logs** may be retained by Discord
---
## 🧪 Testing & Debugging
### **Manual Webhook Test**
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"content":"Test message from rewards script"}' \
"YOUR_WEBHOOK_URL_HERE"
```
### **Script Debug Mode**
```powershell
$env:DEBUG_REWARDS_VERBOSE=1; npm start
```
### **Success Indicators**
```
[INFO] Sending conclusion webhook...
[INFO] Conclusion webhook sent successfully
```
### **Error Messages**
```
[ERROR] Failed to send conclusion webhook: Invalid webhook URL
```
---
## 🛠️ Troubleshooting
| Problem | Solution |
|---------|----------|
| **No summary received** | Check webhook URL; verify Discord permissions |
| **Malformed messages** | Validate webhook URL; check Discord server status |
| **Missing information** | Ensure script completed; check for execution errors |
| **Rate limited** | Single webhook per session prevents this |
### **Common Fixes**
-**Webhook URL** — Must be complete Discord webhook URL
-**Channel permissions** — Webhook must have send permissions
-**Server availability** — Discord server must be accessible
-**Script completion** — Summary only sent after full execution
---
## ⚡ Performance Impact
### **Resource Usage**
- 📨 **Single HTTP request** at script end
-**Non-blocking operation** — No execution delays
- 💾 **Payload size** — Typically < 2KB
- 🌐 **Delivery time** Usually < 1 second
### **Benefits**
- **No impact** on account processing
- **Minimal memory** footprint
- **No disk storage** required
- **Negligible bandwidth** usage
---
## 🎨 Customization
### **Embed Features**
- 🎨 **Color-coded** status indicators
- 🎭 **Emoji icons** for visual clarity
- 📊 **Structured fields** for easy reading
- **Timestamps** and duration info
### **Discord Integration**
- 💬 **Thread notifications** support
- 👥 **Role mentions** (configure in webhook)
- 🔍 **Searchable messages** for history
- 📂 **Archive functionality** for records
---
## 🔗 Related Guides
- **[NTFY Notifications](./ntfy.md)** Mobile push notifications
- **[Getting Started](./getting-started.md)** Initial setup and configuration
- **[Buy Mode](./buy-mode.md)** Manual purchasing with monitoring
- **[Security](./security.md)** Privacy and data protection

227
docs/config.md Normal file
View File

@@ -0,0 +1,227 @@
# ⚙️ Configuration Guide
This page documents every field in `config.json`. You can keep the file lean by deleting blocks you do not use missing values fall back to defaults. Comments (`// ...`) are supported in the JSON thanks to a custom parser.
> NOTE: Previous versions had `logging.live` (live streaming webhook); it was removed and replaced by a simple `logging.redactEmails` flag.
---
## Top-Level Fields
### baseURL
Internal Microsoft Rewards base. Leave it unless you know what you are doing.
### sessionPath
Directory where session data (cookies / fingerprints / job-state) is stored.
---
## browser
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| headless | boolean | false | Run browser UI-less. Setting to `false` can improve stability or help visual debugging. |
| globalTimeout | string/number | "30s" | Max time for common Playwright operations. Accepts ms number or time string (e.g. `"45s"`, `"2min"`). |
---
## execution
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| parallel | boolean | false | Run desktop + mobile simultaneously (higher resource usage). |
| runOnZeroPoints | boolean | false | Skip full run early if there are zero points available (saves time). |
| clusters | number | 1 | Number of process clusters (multi-process concurrency). |
| passesPerRun | number | 1 | Advanced: extra full passes per started run. |
---
## buyMode
Manual redeem / purchase assistance.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| enabled (CLI `-buy`) | boolean | false | Enable buy mode (usually via CLI argument). |
| maxMinutes | number | 45 | Max session length for buy mode. |
---
## fingerprinting.saveFingerprint
Persist browser fingerprints per device type for consistency.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| mobile | boolean | false | Save/reuse a consistent mobile fingerprint. |
| desktop | boolean | false | Save/reuse a consistent desktop fingerprint. |
---
## search
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| useLocalQueries | boolean | false | Use locale-specific query sources instead of global ones. |
### search.settings
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| useGeoLocaleQueries | boolean | false | Blend geo / locale into chosen queries. |
| scrollRandomResults | boolean | true | Random scroll during search pages to look natural. |
| clickRandomResults | boolean | true | Occasionally click safe results. |
| retryMobileSearchAmount | number | 2 | Retries if mobile searches didnt yield points. |
| delay.min / delay.max | string/number | 35min | Delay between searches (ms or time string). |
---
## humanization
Humanlike behavior simulation.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| enabled | boolean | true | Global on/off. |
| stopOnBan | boolean | true | Stop processing further accounts if a ban is detected. |
| immediateBanAlert | boolean | true | Fire notification immediately upon ban detection. |
| actionDelay.min/max | number/string | 150450ms | Random micro-delay per action. |
| gestureMoveProb | number | 0.4 | Probability of a small mouse move gesture. |
| gestureScrollProb | number | 0.2 | Probability of a small scroll gesture. |
| allowedWindows | string[] | [] | Local time windows (e.g. `["08:30-11:00","19:00-22:00"]`). Outside windows, run waits. |
---
## vacation
Random contiguous block of days off per month.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| enabled | boolean | false | Activate monthly break behavior. |
| minDays | number | 3 | Minimum skipped days per month. |
| maxDays | number | 5 | Maximum skipped days per month. |
---
## retryPolicy
Generic transient retry/backoff.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| maxAttempts | number | 3 | Max tries for retryable blocks. |
| baseDelay | number | 1000 | Initial delay in ms. |
| maxDelay | number/string | 30s | Max backoff delay. |
| multiplier | number | 2 | Exponential backoff multiplier. |
| jitter | number | 0.2 | Randomization factor (0..1). |
---
## workers
Enable/disable scripted task categories.
| Key | Default | Description |
|-----|---------|-------------|
| doDailySet | true | Daily set activities. |
| doMorePromotions | true | Promotional tasks. |
| doPunchCards | true | Punch card flows. |
| doDesktopSearch | true | Desktop searches. |
| doMobileSearch | true | Mobile searches. |
| doDailyCheckIn | true | Daily check-in. |
| doReadToEarn | true | Reading tasks. |
| bundleDailySetWithSearch | false | Immediately start desktop search bundle after daily set. |
---
## proxy
| Key | Default | Description |
|-----|---------|-------------|
| proxyGoogleTrends | true | Route Google Trends fetch through proxy if set. |
| proxyBingTerms | true | Route Bing query source fetch through proxy if set. |
---
## notifications
Manages notification channels (Discord webhooks, NTFY, etc.).
### notifications.webhook
Primary webhook (can be used for summary or generic messages).
| Key | Default | Description |
|-----|---------|-------------|
| enabled | false | Allow sending webhook-based notifications. |
| url | "" | Webhook endpoint. |
### notifications.conclusionWebhook
Rich end-of-run summary (if enabled separately).
| Key | Default | Description |
|-----|---------|-------------|
| enabled | false | Enable run summary posting. |
| url | "" | Webhook endpoint. |
### notifications.ntfy
Lightweight push notifications.
| Key | Default | Description |
|-----|---------|-------------|
| enabled | false | Enable NTFY push. |
| url | "" | Base NTFY server URL (e.g. https://ntfy.sh). |
| topic | rewards | Topic/channel name. |
| authToken | "" | Bearer token if your server requires auth. |
---
## logging
| Key | Type | Description |
|-----|------|-------------|
| excludeFunc | string[] | Log buckets suppressed in console + any webhook usage. |
| webhookExcludeFunc | string[] | Buckets suppressed specifically for webhook output. |
| redactEmails | boolean | If true, email addresses are partially masked in logs. |
_Removed fields_: `live.enabled`, `live.url`, `live.redactEmails` — replaced by `redactEmails` only.
---
## diagnostics
Capture evidence when something fails.
| Key | Default | Description |
|-----|---------|-------------|
| enabled | true | Master switch for diagnostics. |
| saveScreenshot | true | Save screenshot on failure. |
| saveHtml | true | Save HTML snapshot on failure. |
| maxPerRun | 2 | Cap artifacts per run per failure type. |
| retentionDays | 7 | Old run artifacts pruned after this many days. |
---
## jobState
Checkpoint system to avoid duplicate work.
| Key | Default | Description |
|-----|---------|-------------|
| enabled | true | Enable job state tracking. |
| dir | "" | Custom directory (default: `<sessionPath>/job-state`). |
---
## schedule
Built-in scheduler (avoids external cron inside container or host).
| Key | Default | Description |
|-----|---------|-------------|
| enabled | false | Enable scheduling loop. |
| useAmPm | false | If true, parse `time12`; else use `time24`. |
| time12 | 9:00 AM | 12hour format time (only if useAmPm=true). |
| time24 | 09:00 | 24hour format time (only if useAmPm=false). |
| timeZone | America/New_York | IANA zone string (e.g. Europe/Paris). |
| runImmediatelyOnStart | false | Run one pass instantly in addition to daily schedule. |
_Legacy_: If both `time12` and `time24` are empty, a legacy `time` (HH:mm) may still be read.
---
## update
Auto-update behavior after a run.
| Key | Default | Description |
|-----|---------|-------------|
| git | true | Pull latest git changes after run. |
| docker | false | Recreate container (if running in Docker orchestration). |
| scriptPath | setup/update/update.mjs | Custom script executed for update flow. |
---
## Security / Best Practices
- Keep `redactEmails` true if you share logs publicly.
- Use a private NTFY instance or secure Discord webhooks (do not leak URLs).
- Avoid setting `headless` false on untrusted remote servers.
---
## Minimal Example
```jsonc
{
"browser": { "headless": true },
"execution": { "parallel": false },
"workers": { "doDailySet": true, "doDesktopSearch": true, "doMobileSearch": true },
"logging": { "redactEmails": true }
}
```
## Common Tweaks
| Goal | Change |
|------|--------|
| Faster dev feedback | Set `browser.headless` to false and shorten search delays. |
| Reduce detection risk | Keep humanization enabled, add vacation window. |
| Silent mode | Add more buckets to `excludeFunc`. |
| Skip mobile searches | Set `workers.doMobileSearch=false`. |
| Use daily schedule | Set `schedule.enabled=true` and adjust `time24` + `timeZone`. |
---
## Changelog Notes
- Removed live webhook streaming complexity; now simpler logging.
- Centralized redaction logic under `logging.redactEmails`.
If something feels undocumented or unclear, open a documentation issue or extend this page.

225
docs/diagnostics.md Normal file
View File

@@ -0,0 +1,225 @@
# 🔍 Diagnostics & Error Capture
<div align="center">
**🛠️ Automatic error screenshots and HTML snapshots**
*Debug smarter with visual evidence*
</div>
---
## 🎯 What is Diagnostics?
The diagnostics system **automatically captures** error screenshots and HTML snapshots when issues occur during script execution, providing visual evidence for troubleshooting.
### **Key Features**
- 📸 **Auto-screenshot** — Visual error capture
- 📄 **HTML snapshots** — Complete page source
- 🚦 **Rate limiting** — Prevents storage bloat
- 🗂️ **Auto-cleanup** — Configurable retention
- 🔒 **Privacy-safe** — Local storage only
---
## ⚙️ Configuration
### **Basic Setup**
Add to `src/config.json`:
```json
{
"diagnostics": {
"enabled": true,
"saveScreenshot": true,
"saveHtml": true,
"maxPerRun": 2,
"retentionDays": 7
}
}
```
### **Configuration Options**
| Setting | Default | Description |
|---------|---------|-------------|
| `enabled` | `true` | Master toggle for diagnostics capture |
| `saveScreenshot` | `true` | Capture PNG screenshots on errors |
| `saveHtml` | `true` | Save page HTML content on errors |
| `maxPerRun` | `2` | Maximum captures per script run |
| `retentionDays` | `7` | Auto-delete reports older than N days |
---
## 🚀 How It Works
### **Automatic Triggers**
The system captures when these errors occur:
- ⏱️ **Page navigation timeouts**
- 🎯 **Element selector failures**
- 🔐 **Authentication errors**
- 🌐 **Network request failures**
-**JavaScript execution errors**
### **Capture Process**
1. **Error Detection** — Script encounters unhandled error
2. **Visual Capture** — Screenshot + HTML snapshot
3. **Safe Storage** — Local `reports/` folder
4. **Continue Execution** — No blocking or interruption
---
## 📁 File Structure
### **Storage Organization**
```
reports/
├── 2025-01-20/
│ ├── error_abc123_001.png
│ ├── error_abc123_001.html
│ ├── error_def456_002.png
│ └── error_def456_002.html
└── 2025-01-21/
└── ...
```
### **File Naming Convention**
```
error_[runId]_[sequence].[ext]
```
- **RunId** — Unique identifier for each script execution
- **Sequence** — Incremental counter (001, 002, etc.)
- **Extension** — `.png` for screenshots, `.html` for source
---
## 🧹 Retention Management
### **Automatic Cleanup**
- Runs after each script completion
- Deletes entire date folders older than `retentionDays`
- Prevents unlimited disk usage growth
### **Manual Cleanup**
```powershell
# Remove all diagnostic reports
Remove-Item -Recurse -Force reports/
# Remove reports older than 3 days
Get-ChildItem reports/ | Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-3)} | Remove-Item -Recurse -Force
```
---
## 📊 Use Cases
| Scenario | Benefit |
|----------|---------|
| **🐛 Development & Debugging** | Visual confirmation of page state during errors |
| **🔍 Element Detection Issues** | HTML source analysis for selector problems |
| **📈 Production Monitoring** | Evidence collection for account issues |
| **⚡ Performance Analysis** | Timeline reconstruction of automation failures |
---
## ⚡ Performance Impact
### **Resource Usage**
- **Screenshots** — ~100-500KB each
- **HTML files** — ~50-200KB each
- **CPU overhead** — Minimal (only during errors)
- **Memory impact** — Asynchronous, non-blocking
### **Storage Optimization**
- Daily cleanup prevents accumulation
- Rate limiting via `maxPerRun`
- Configurable retention period
---
## 🎛️ Environment Settings
### **Development Mode**
```json
{
"diagnostics": {
"enabled": true,
"maxPerRun": 5,
"retentionDays": 14
}
}
```
### **Production Mode**
```json
{
"diagnostics": {
"enabled": true,
"maxPerRun": 2,
"retentionDays": 3
}
}
```
### **Debug Verbose Logging**
```powershell
$env:DEBUG_REWARDS_VERBOSE=1; npm start
```
---
## 🛠️ Troubleshooting
| Problem | Solution |
|---------|----------|
| **No captures despite errors** | Check `enabled: true`; verify `reports/` write permissions |
| **Excessive storage usage** | Reduce `maxPerRun`; decrease `retentionDays` |
| **Missing screenshots** | Verify browser screenshot API; check memory availability |
| **Cleanup not working** | Ensure script completes successfully for auto-cleanup |
### **Common Capture Locations**
- **Login issues** — Authentication page screenshots
- **Activity failures** — Element detection errors
- **Network problems** — Timeout and connection errors
- **Navigation issues** — Page load failures
---
## 🔗 Integration
### **With Notifications**
Diagnostics complement [Discord Webhooks](./conclusionwebhook.md) and [NTFY](./ntfy.md):
- **Webhooks** — Immediate error alerts
- **Diagnostics** — Visual evidence for investigation
- **Combined** — Complete error visibility
### **With Development Workflow**
```bash
# 1. Run script with diagnostics
npm start
# 2. Check for captures after errors
ls reports/$(date +%Y-%m-%d)/
# 3. Analyze screenshots and HTML
# Open .png files for visual state
# Review .html files for DOM structure
```
---
## 🔒 Privacy & Security
- **Local Only** — All captures stored locally
- **No Uploads** — Zero external data transmission
- **Account Info** — May contain sensitive data
- **Secure Storage** — Use appropriate folder permissions
- **Regular Cleanup** — Recommended for sensitive environments
---
## 🔗 Related Guides
- **[Getting Started](./getting-started.md)** — Initial setup and configuration
- **[Discord Webhooks](./conclusionwebhook.md)** — Error notification alerts
- **[NTFY Notifications](./ntfy.md)** — Mobile push notifications
- **[Security](./security.md)** — Privacy and data protection

85
docs/docker.md Normal file
View File

@@ -0,0 +1,85 @@
# 🐳 Docker Guide
<div align="center">
**⚡ Lightweight containerized deployment**
*Automated Microsoft Rewards with minimal Docker footprint*
</div>
---
## 🚀 Quick Start
### **Prerequisites**
-`src/accounts.json` configured with your Microsoft accounts
-`src/config.json` exists (uses defaults if not customized)
- ✅ Docker & Docker Compose installed
### **Launch**
```bash
# Build and start the container
docker compose up -d
# Monitor the automation
docker logs -f microsoft-rewards-script
# Stop when needed
docker compose down
```
**That's it!** The container runs the built-in scheduler automatically.uide
This project ships with a Docker setup tailored for headless runs. It uses Playwrights Chromium Headless Shell to keep the image small.
## Quick Start
- Ensure you have `src/accounts.json` and `src/config.json` in the repo
- Build and start:
- `docker compose up -d`
- Follow logs:
- `docker logs -f microsoft-rewards-script`
## Volumes & Files
The compose file mounts:
- `./src/accounts.json``/usr/src/microsoft-rewards-script/accounts.json` (readonly)
- `./src/config.json``/usr/src/microsoft-rewards-script/config.json` (readonly)
- `./sessions``/usr/src/microsoft-rewards-script/sessions` (persist login sessions)
You can also use env overrides supported by the app loader:
- `ACCOUNTS_FILE=/path/to/accounts.json`
- `ACCOUNTS_JSON='[ {"email":"...","password":"..."} ]'`
## Environment
Useful variables:
- `TZ` — container time zone (e.g., `Europe/Paris`)
- `NODE_ENV=production`
- `FORCE_HEADLESS=1` — ensures headless mode inside the container
- Scheduler knobs (optional):
- `SCHEDULER_DAILY_JITTER_MINUTES_MIN` / `SCHEDULER_DAILY_JITTER_MINUTES_MAX`
- `SCHEDULER_PASS_TIMEOUT_MINUTES`
- `SCHEDULER_FORK_PER_PASS`
## Headless Browsers
The Docker image installs only Chromium Headless Shell via:
- `npx playwright install --with-deps --only-shell`
This dramatically reduces image size vs. installing all Playwright browsers.
## Oneshot vs. Scheduler
- Default command runs the builtin scheduler: `npm run start:schedule`
- For oneshot run, override the command:
- `docker run --rm ... node ./dist/index.js`
## Tips
- If you see 2FA prompts, add your TOTP Base32 secret to `accounts.json` so the bot can autofill codes.
- Use a persistent `sessions` volume to avoid relogging every run.
- For proxies per account, fill the `proxy` block in your `accounts.json` (see [Proxy](./proxy.md)).
---
## 🔗 Related Guides
- **[Getting Started](./getting-started.md)** — Initial setup before containerization
- **[Accounts & 2FA](./accounts.md)** — Configure accounts for Docker
- **[Scheduler](./schedule.md)** — Alternative to Docker cron automation
- **[Proxy Configuration](./proxy.md)** — Network routing in containers

136
docs/getting-started.md Normal file
View File

@@ -0,0 +1,136 @@
# 🚀 Getting Started
<div align="center">
**🎯 From zero to earning Microsoft Rewards points in minutes**
*Complete setup guide for beginners*
</div>
---
## ✅ Requirements
- **Node.js 18+** (22 recommended) — [Download here](https://nodejs.org/)
- **Microsoft accounts** with email + password
- **Optional:** Docker for containerized deployment
---
## ⚡ Quick Setup (Recommended)
<div align="center">
### **🎬 One Command, Total Automation**
</div>
```bash
# 🪟 Windows
setup/setup.bat
# 🐧 Linux/macOS/WSL
bash setup/setup.sh
# 🌍 Any platform
npm run setup
```
**That's it!** The wizard will:
- ✅ Help you create `src/accounts.json` with your Microsoft credentials
- ✅ Install all dependencies automatically
- ✅ Build the TypeScript project
- ✅ Start earning points immediately
---
## 🛠️ Manual Setup
<details>
<summary><strong>📖 Prefer step-by-step? Click here</strong></summary>
### 1⃣ **Configure Your Accounts**
```bash
cp src/accounts.example.json src/accounts.json
# Edit accounts.json with your Microsoft credentials
```
### 2⃣ **Install Dependencies & Build**
```bash
npm install
npm run build
```
### 3⃣ **Choose Your Mode**
```bash
# Single run (test it works)
npm start
# Automated daily scheduler (set and forget)
npm run start:schedule
```
</details>
---
## 🎯 What Happens Next?
The script will automatically:
- 🔍 **Search Bing** for points (desktop + mobile)
- 📅 **Complete daily sets** (quizzes, polls, activities)
- 🎁 **Grab promotions** and bonus opportunities
- 🃏 **Work on punch cards** (multi-day challenges)
-**Daily check-ins** for easy points
- 📚 **Read articles** for additional rewards
**All while looking completely natural to Microsoft!** 🤖
---
## 🐳 Docker Alternative
If you prefer containers:
```bash
# Ensure accounts.json and config.json exist
docker compose up -d
# Follow logs
docker logs -f microsoft-rewards-script
```
**[Full Docker Guide →](./docker.md)**
---
## 🔧 Next Steps
Once running, explore these guides:
| Priority | Guide | Why Important |
|----------|-------|---------------|
| **High** | **[Accounts & 2FA](./accounts.md)** | Set up TOTP for secure automation |
| **High** | **[Scheduling](./schedule.md)** | Configure automated daily runs |
| **Medium** | **[Notifications](./ntfy.md)** | Get alerts on your phone |
| **Low** | **[Humanization](./humanization.md)** | Advanced anti-detection |
---
## 🆘 Need Help?
**Script not starting?** → [Troubleshooting Guide](./diagnostics.md)
**Login issues?** → [Accounts & 2FA Setup](./accounts.md)
**Want Docker?** → [Container Guide](./docker.md)
**Found a bug?** [Report it here](https://github.com/TheNetsky/Microsoft-Rewards-Script/issues)
**Need support?** [Join our Discord](https://discord.gg/KRBFxxsU)
---
## 🔗 Related Guides
- **[Accounts & 2FA](./accounts.md)** — Add Microsoft accounts with TOTP
- **[Docker](./docker.md)** — Deploy with containers
- **[Scheduler](./schedule.md)** — Automate daily execution
- **[Discord Webhooks](./conclusionwebhook.md)** — Get run summaries

277
docs/humanization.md Normal file
View File

@@ -0,0 +1,277 @@
# 🤖 Humanization (Human Mode)
<div align="center">
**🎭 Natural automation that mimics human behavior**
*Subtle gestures for safer operation*
</div>
---
## 🎯 What is Humanization?
Human Mode adds **subtle human-like behavior** to make your automation look and feel more natural. It's designed to be **safe by design** with minimal, realistic gestures.
### **Key Features**
- 🎲 **Random delays** — Natural pause variation
- 🖱️ **Micro movements** — Subtle mouse gestures
- 📜 **Tiny scrolls** — Minor page adjustments
-**Time windows** — Run during specific hours
- 📅 **Random off days** — Skip days naturally
- 🔒 **Safe by design** — Never clicks random elements
---
## ⚙️ Configuration
### **Simple Setup (Recommended)**
```json
{
"humanization": {
"enabled": true
}
}
```
### **Advanced Configuration**
```json
{
"humanization": {
"enabled": true,
"actionDelay": { "min": 150, "max": 450 },
"gestureMoveProb": 0.4,
"gestureScrollProb": 0.2,
"allowedWindows": ["08:00-10:30", "20:00-22:30"],
"randomOffDaysPerWeek": 1
}
}
```
### **Configuration Options**
| Setting | Default | Description |
|---------|---------|-------------|
| `enabled` | `true` | Master toggle for all humanization |
| `actionDelay` | `{min: 150, max: 450}` | Random pause between actions (ms) |
| `gestureMoveProb` | `0.4` | Probability (0-1) for tiny mouse moves |
| `gestureScrollProb` | `0.2` | Probability (0-1) for minor scrolls |
| `allowedWindows` | `[]` | Time windows for script execution |
| `randomOffDaysPerWeek` | `1` | Skip N random days per week |
---
## 🎭 How It Works
### **Action Delays**
- **Random pauses** between automation steps
- **Natural variation** mimics human decision time
- **Configurable range** allows fine-tuning
### **Gesture Simulation**
- **Micro mouse moves** — Tiny cursor adjustments (safe zones only)
- **Minor scrolls** — Small page movements (non-interactive areas)
- **Probability-based** — Not every action includes gestures
### **Temporal Patterns**
- **Time windows** — Only run during specified hours
- **Random off days** — Skip days to avoid rigid patterns
- **Natural scheduling** — Mimics human usage patterns
---
## 🎯 Usage Examples
### **Default Setup (Recommended)**
```json
{
"humanization": { "enabled": true }
}
```
**Best for most users** — Balanced safety and naturalness
### **Minimal Humanization**
```json
{
"humanization": {
"enabled": true,
"gestureMoveProb": 0.1,
"gestureScrollProb": 0.1,
"actionDelay": { "min": 100, "max": 200 }
}
}
```
**Faster execution** with minimal gestures
### **Maximum Natural Behavior**
```json
{
"humanization": {
"enabled": true,
"actionDelay": { "min": 300, "max": 800 },
"gestureMoveProb": 0.6,
"gestureScrollProb": 0.4,
"allowedWindows": ["08:30-11:00", "19:00-22:00"],
"randomOffDaysPerWeek": 2
}
}
```
🎭 **Most human-like** but slower execution
### **Disabled Humanization**
```json
{
"humanization": { "enabled": false }
}
```
🚀 **Fastest execution** — automation optimized
---
## ⏰ Time Windows
### **Setup**
```json
{
"humanization": {
"enabled": true,
"allowedWindows": ["08:00-10:30", "20:00-22:30"]
}
}
```
### **Behavior**
- Script **waits** until next allowed window
- Uses **local time** for scheduling
- **Multiple windows** supported per day
- **Empty array** `[]` = no time restrictions
### **Examples**
```json
// Morning and evening windows
"allowedWindows": ["08:00-10:30", "20:00-22:30"]
// Lunch break only
"allowedWindows": ["12:00-13:00"]
// Extended evening window
"allowedWindows": ["18:00-23:00"]
// No restrictions
"allowedWindows": []
```
---
## 📅 Random Off Days
### **Purpose**
Mimics natural human behavior by skipping random days per week.
### **Configuration**
```json
{
"humanization": {
"randomOffDaysPerWeek": 1 // Skip 1 random day per week
}
}
```
### **Options**
- `0` — Never skip days
- `1` — Skip 1 random day per week (default)
- `2` — Skip 2 random days per week
- `3+` — Higher values for more irregular patterns
---
## 🔒 Safety Features
### **Safe by Design**
-**Never clicks** arbitrary elements
-**Gestures only** in safe zones
-**Minor movements** — pixel-level adjustments
-**Probability-based** — Natural randomness
-**Non-interactive areas** — Avoids clickable elements
### **Buy Mode Compatibility**
- **Passive monitoring** remains unaffected
- **No interference** with manual actions
- **Background tasks** only for monitoring
---
## 📊 Performance Impact
| Setting | Speed Impact | Natural Feel | Recommendation |
|---------|--------------|--------------|----------------|
| **Disabled** | Fastest | Robotic | Development only |
| **Default** | Moderate | Balanced | **Recommended** |
| **High probability** | Slower | Very natural | Conservative users |
| **Time windows** | Delayed start | Realistic | Scheduled execution |
---
## 🛠️ Troubleshooting
| Problem | Solution |
|---------|----------|
| **Script too slow** | Reduce `actionDelay` values; lower probabilities |
| **Too robotic** | Increase probabilities; add time windows |
| **Runs outside hours** | Check `allowedWindows` format (24-hour time) |
| **Skipping too many days** | Reduce `randomOffDaysPerWeek` |
| **Gestures interfering** | Lower probabilities or disable specific gestures |
### **Debug Humanization**
```powershell
$env:DEBUG_HUMANIZATION=1; npm start
```
---
## 🎛️ Presets
### **Conservative**
```json
{
"humanization": {
"enabled": true,
"actionDelay": { "min": 200, "max": 600 },
"gestureMoveProb": 0.6,
"gestureScrollProb": 0.4,
"allowedWindows": ["08:00-10:00", "20:00-22:00"],
"randomOffDaysPerWeek": 2
}
}
```
### **Balanced (Default)**
```json
{
"humanization": {
"enabled": true
}
}
```
### **Performance**
```json
{
"humanization": {
"enabled": true,
"actionDelay": { "min": 100, "max": 250 },
"gestureMoveProb": 0.2,
"gestureScrollProb": 0.1,
"randomOffDaysPerWeek": 0
}
}
```
---
## 🔗 Related Guides
- **[Getting Started](./getting-started.md)** — Initial setup and configuration
- **[Scheduler](./schedule.md)** — Automated timing and execution
- **[Security](./security.md)** — Privacy and detection avoidance
- **[Buy Mode](./buy-mode.md)** — Manual purchasing with monitoring

96
docs/index.md Normal file
View File

@@ -0,0 +1,96 @@
# 📚 Microsoft Rewards Script V2 Documentation
<div align="center">
**🎯 Your complete guide to automating Microsoft Rewards**
*Everything you need to get started and master the script*
</div>
---
## 🚀 Quick Navigation
### **Essential Setup**
| Guide | Description |
|-------|-------------|
| **[🎬 Getting Started](./getting-started.md)** | Zero to running — complete setup guide |
| **[👤 Accounts & 2FA](./accounts.md)** | Microsoft accounts + TOTP authentication |
| **[🐳 Docker](./docker.md)** | Container deployment with headless browsers |
### **Operations & Advanced**
| Guide | Description |
|-------|-------------|
| **[⏰ Scheduling](./schedule.md)** | Automated daily runs and timing |
| **[🛠️ Diagnostics](./diagnostics.md)** | Troubleshooting and error capture |
| **[🧠 Humanization](./humanization.md)** | Anti-detection and natural behavior |
| **[🌐 Proxy Setup](./proxy.md)** | Network routing and IP management |
| **[⚙️ Configuration Reference](./config.md)** | Full `config.json` field documentation |
### **Notifications & Monitoring**
| Guide | Description |
|-------|-------------|
| **[📱 NTFY Push](./ntfy.md)** | Mobile push notifications |
| **[🔗 Discord Webhooks](./conclusionwebhook.md)** | Rich server notifications |
### **Special Modes**
| Guide | Description |
|-------|-------------|
| **[💳 Buy Mode](./buy-mode.md)** | Manual redemption with live monitoring |
---
## 🎯 Recommended Reading Path
**New Users:** Getting Started → Accounts & 2FA → Choose Docker OR Scheduling
**Advanced Users:** Humanization → Diagnostics → Notifications
**Docker Users:** Getting Started → Accounts & 2FA → Docker → NTFY/Webhookstion Index
Welcome to the Microsoft Rewards Script V2 docs. Start here:
- Getting Started: highlevel setup from zero to running — [Getting Started](./getting-started.md)
- Accounts & Authentication — [Accounts & TOTP (2FA)](./accounts.md)
- Runtime & Operations — [Docker Guide](./docker.md), [Scheduling](./schedule.md), [Diagnostics](./diagnostics.md), [Humanization](./humanization.md), [Job State](./jobstate.md), [Auto Update](./update.md), [Security](./security.md)
- Notifications — [NTFY Push](./ntfy.md), [Conclusion Webhook (Discord)](./conclusionwebhook.md)
- Modes & Activities — [Buy Mode](./buy-mode.md)
Recommended reading order if youre new: Getting Started → Accounts & TOTP → Docker or Scheduler.# Documentation Index
Welcome to the Microsoft Rewards Script V2 documentation. Start here to set up your environment, add your Microsoft accounts, and understand how the bot operates.
- Getting Started: Highlevel setup from zero to running
- [Getting Started](./getting-started.md)
- Accounts & Authentication
- [Accounts & TOTP (2FA)](./accounts.md)
- [Proxy Setup](./proxy.md)
- Runtime & Operations
- [Docker Guide](./docker.md)
- [Scheduling](./schedule.md)
- [Diagnostics](./diagnostics.md)
- [Humanization](./humanization.md)
- [Job State](./jobstate.md)
- [Auto Update](./update.md)
- [Security Notes](./security.md)
- Notifications
- [NTFY Push](./ntfy.md)
- [Conclusion Webhook (Discord)](./conclusionwebhook.md)
- Modes & Activities
- [Buy Mode](./buy-mode.md)
If you are new, read Getting Started first, then Accounts & TOTP.
---
## 🚀 Quick Start Path
**New users should follow this sequence:**
1. **[Getting Started](./getting-started.md)** — Install and basic configuration
2. **[Accounts & 2FA](./accounts.md)** — Add your Microsoft accounts
3. **[Docker](./docker.md)** OR **[Scheduler](./schedule.md)** — Choose deployment method
4. **[NTFY](./ntfy.md)** OR **[Discord Webhooks](./conclusionwebhook.md)** — Set up notifications
**Advanced users may also need:**
- **[Proxy](./proxy.md)** — For privacy and geographic routing
- **[Security](./security.md)** — Account protection and incident response
- **[Humanization](./humanization.md)** — Natural behavior simulation

339
docs/jobstate.md Normal file
View File

@@ -0,0 +1,339 @@
# 💾 Job State Persistence
<div align="center">
**🔄 Resume interrupted tasks and track progress across runs**
*Never lose your progress again*
</div>
---
## 🎯 What is Job State Persistence?
Job state persistence allows the script to **resume interrupted tasks** and **track progress** across multiple runs, ensuring no work is lost when the script is stopped or crashes.
### **Key Features**
- 🔄 **Resumable tasks** — Pick up exactly where you left off
- 📅 **Daily tracking** — Date-specific progress monitoring
- 👤 **Per-account isolation** — Independent progress for each account
- 🛡️ **Corruption protection** — Atomic writes prevent data loss
- 🚀 **Performance optimized** — Minimal overhead
---
## ⚙️ Configuration
### **Basic Setup**
```json
{
"jobState": {
"enabled": true,
"dir": ""
}
}
```
### **Configuration Options**
| Setting | Description | Default |
|---------|-------------|---------|
| `enabled` | Enable job state persistence | `true` |
| `dir` | Custom directory for state files | `""` (uses `sessions/job-state`) |
---
## 🏗️ How It Works
### **State Tracking**
- 📋 **Monitors completion** status of individual activities
- 🔍 **Tracks progress** for daily sets, searches, and promotional tasks
-**Prevents duplicates** when script restarts
### **Storage Structure**
```
sessions/job-state/
├── account1@email.com/
│ ├── daily-set-2025-01-20.json
│ ├── desktop-search-2025-01-20.json
│ └── mobile-search-2025-01-20.json
└── account2@email.com/
├── daily-set-2025-01-20.json
└── promotional-tasks-2025-01-20.json
```
### **State File Format**
```json
{
"date": "2025-01-20",
"account": "user@email.com",
"type": "daily-set",
"completed": [
"daily-check-in",
"quiz-1",
"poll-1"
],
"remaining": [
"quiz-2",
"search-desktop"
],
"lastUpdate": "2025-01-20T10:30:00.000Z"
}
```
---
## 🚀 Key Benefits
### **Resumable Tasks**
-**Script restarts** pick up where they left off
-**Individual completion** is remembered
-**Avoid re-doing** completed activities
### **Daily Reset**
- 📅 **Date-specific** state files
- 🌅 **New day** automatically starts fresh tracking
- 📚 **History preserved** for analysis
### **Account Isolation**
- 👤 **Separate state** per account
-**Parallel processing** doesn't interfere
- 📊 **Independent progress** tracking
---
## 📋 Use Cases
### **Interrupted Executions**
| Scenario | Benefit |
|----------|---------|
| **Network issues** | Resume when connection restored |
| **System reboots** | Continue after restart |
| **Manual termination** | Pick up from last checkpoint |
| **Resource exhaustion** | Recover without losing progress |
### **Selective Reruns**
| Feature | Description |
|---------|-------------|
| **Skip completed sets** | Avoid redoing finished daily activities |
| **Resume searches** | Continue partial search sessions |
| **Retry failed tasks** | Target only problematic activities |
| **Account targeting** | Process specific accounts only |
### **Progress Monitoring**
- 📊 **Track completion rates** across accounts
- 🔍 **Identify problematic** activities
- ⏱️ **Monitor task duration** trends
- 🐛 **Debug stuck** or slow tasks
---
## 🛠️ Technical Implementation
### **Checkpoint Strategy**
- 💾 **State saved** after each completed activity
- ⚛️ **Atomic writes** prevent corruption
- 🔒 **Lock-free design** for concurrent access
### **Performance Optimization**
-**Minimal I/O overhead** — Fast state updates
- 🧠 **In-memory caching** — Reduce disk access
- 📥 **Lazy loading** — Load state files on demand
### **Error Handling**
- 🔧 **Corrupted files** are rebuilt automatically
- 📁 **Missing directories** created as needed
- 🎯 **Graceful degradation** when disabled
---
## 🗂️ File Management
### **Automatic Behavior**
- 📅 **Date-specific files** — New files for each day
- 💾 **Preserved history** — Old files remain for reference
- 🚀 **No auto-deletion** — Manual cleanup recommended
### **Manual Maintenance**
```powershell
# Clean state files older than 7 days
Get-ChildItem sessions/job-state -Recurse -Filter "*.json" | Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-7)} | Remove-Item
# Reset all job state (start fresh)
Remove-Item -Recurse -Force sessions/job-state/
# Reset specific account state
Remove-Item -Recurse -Force sessions/job-state/user@email.com/
```
---
## 📊 Example Workflows
### **Interrupted Daily Run**
```
Day 1 - 10:30 AM:
✅ Account A: Daily set completed
🔄 Account B: 3/5 daily tasks done
❌ Script crashes
Day 1 - 2:00 PM:
🚀 Script restarts
✅ Account A: Skipped (already complete)
🔄 Account B: Resumes with 2 remaining tasks
```
### **Multi-Day Tracking**
```
Monday:
📅 daily-set-2025-01-20.json created
✅ All tasks completed
Tuesday:
📅 daily-set-2025-01-21.json created
🔄 Fresh start for new day
📚 Monday's progress preserved
```
---
## 🔍 Debugging Job State
### **State Inspection**
```powershell
# View current state for account
Get-Content sessions/job-state/user@email.com/daily-set-2025-01-20.json | ConvertFrom-Json
# List all state files
Get-ChildItem sessions/job-state -Recurse -Filter "*.json"
```
### **Debug Output**
Enable verbose logging to see state operations:
```powershell
$env:DEBUG_REWARDS_VERBOSE=1; npm start
```
Sample output:
```
[INFO] Loading job state for user@email.com (daily-set)
[INFO] Found 3 completed tasks, 2 remaining
[INFO] Skipping completed task: daily-check-in
[INFO] Starting task: quiz-2
```
---
## 🛠️ Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| **Tasks not resuming** | Missing/corrupt state files | Check file permissions; verify directory exists |
| **Duplicate execution** | Clock sync issues | Ensure system time is accurate |
| **Excessive files** | No cleanup schedule | Implement regular state file cleanup |
| **Permission errors** | Write access denied | Verify sessions/ directory is writable |
### **Common Issues**
#### **Tasks Not Resuming**
```
[ERROR] Failed to load job state: Permission denied
```
**Solutions:**
- ✅ Check file/directory permissions
- ✅ Verify state directory exists
- ✅ Ensure write access to sessions/
#### **Duplicate Task Execution**
```
[WARN] Task appears to be running twice
```
**Solutions:**
- ✅ Check for corrupt state files
- ✅ Verify system clock synchronization
- ✅ Clear state for affected account
#### **Storage Growth**
```
[INFO] Job state directory: 2.3GB (1,247 files)
```
**Solutions:**
- ✅ Implement regular cleanup schedule
- ✅ Remove old state files (7+ days)
- ✅ Monitor disk space usage
---
## 🤝 Integration Features
### **Session Persistence**
- 🍪 **Works alongside** browser session storage
- 🔐 **Complements** cookie and fingerprint persistence
- 🌐 **Independent of** proxy and authentication state
### **Clustering**
-**Isolated state** per cluster worker
- 🚫 **No shared state** between parallel processes
- 📁 **Worker-specific** directories
### **Scheduling**
-**Persists across** scheduled runs
- 🌅 **Daily reset** at midnight automatically
- 🔄 **Long-running continuity** maintained
---
## ⚙️ Advanced Configuration
### **Custom State Directory**
```json
{
"jobState": {
"enabled": true,
"dir": "/custom/path/to/state"
}
}
```
### **Disabling Job State**
```json
{
"jobState": {
"enabled": false
}
}
```
**Effects when disabled:**
-**Tasks restart** from beginning each run
-**No progress tracking** between sessions
-**Potential duplicate work** on interruptions
-**Slightly faster startup** (no state loading)
---
## 📊 Best Practices
### **Development**
-**Enable for testing** — Consistent behavior
- 🧹 **Clear between changes** — Fresh state for major updates
- 🔍 **Monitor for debugging** — State files reveal execution flow
### **Production**
-**Always enabled** — Reliability is critical
- 💾 **Regular backups** — State directory backups
- 📊 **Monitor disk usage** — Prevent storage growth
### **Maintenance**
- 🗓️ **Weekly cleanup** — Remove old state files
- 🔍 **Health checks** — Verify state integrity
- 📝 **Usage monitoring** — Track storage trends
---
## 🔗 Related Guides
- **[Getting Started](./getting-started.md)** — Initial setup and configuration
- **[Scheduler](./schedule.md)** — Automated timing and execution
- **[Diagnostics](./diagnostics.md)** — Error capture and debugging
- **[Security](./security.md)** — Privacy and data protection

407
docs/ntfy.md Normal file
View File

@@ -0,0 +1,407 @@
# 📱 NTFY Push Notifications
<div align="center">
**🔔 Real-time push notifications to your devices**
*Stay informed wherever you are*
</div>
---
## 🎯 What is NTFY?
NTFY is a **simple HTTP-based pub-sub notification service** that sends push notifications to your phone, desktop, or web browser. Perfect for real-time alerts about script events and errors.
### **Key Features**
- 📱 **Mobile & Desktop** — Push to any device
- 🆓 **Free & Open Source** — No vendor lock-in
- 🏠 **Self-hostable** — Complete privacy control
-**Real-time delivery** — Instant notifications
- 🔒 **Authentication support** — Secure topics
### **Official Links**
- **Website** — [ntfy.sh](https://ntfy.sh)
- **Documentation** — [docs.ntfy.sh](https://docs.ntfy.sh)
- **GitHub** — [binwiederhier/ntfy](https://github.com/binwiederhier/ntfy)
---
## ⚙️ Configuration
### **Basic Setup**
```json
{
"notifications": {
"ntfy": {
"enabled": true,
"url": "https://ntfy.sh",
"topic": "rewards-script",
"authToken": ""
}
}
}
```
### **Configuration Options**
| Setting | Description | Example |
|---------|-------------|---------|
| `enabled` | Enable NTFY notifications | `true` |
| `url` | NTFY server URL | `"https://ntfy.sh"` |
| `topic` | Notification topic name | `"rewards-script"` |
| `authToken` | Authentication token (optional) | `"tk_abc123..."` |
---
## 🚀 Setup Options
### **Option 1: Public Service (Easiest)**
```json
{
"notifications": {
"ntfy": {
"enabled": true,
"url": "https://ntfy.sh",
"topic": "your-unique-topic-name"
}
}
}
```
**Pros:**
- ✅ No server setup required
- ✅ Always available
- ✅ Free to use
**Cons:**
- ❌ Public server (less privacy)
- ❌ Rate limits apply
- ❌ Dependent on external service
### **Option 2: Self-Hosted (Recommended)**
```json
{
"notifications": {
"ntfy": {
"enabled": true,
"url": "https://ntfy.yourdomain.com",
"topic": "rewards",
"authToken": "tk_your_token_here"
}
}
}
```
**Self-Hosted Setup:**
```yaml
# docker-compose.yml
version: '3.8'
services:
ntfy:
image: binwiederhier/ntfy
container_name: ntfy
ports:
- "80:80"
volumes:
- ./data:/var/lib/ntfy
command: serve
```
---
## 🔒 Authentication
### **When You Need Auth**
Authentication tokens are **optional** but required for:
- 🔐 **Private topics** with username/password
- 🏠 **Private NTFY servers** with authentication
- 🛡️ **Preventing spam** on your topic
### **Getting an Auth Token**
#### **Method 1: Command Line**
```bash
ntfy token
```
#### **Method 2: Web Interface**
1. Visit your NTFY server (e.g., `https://ntfy.sh`)
2. Go to **Account** section
3. Generate **new access token**
#### **Method 3: API**
```bash
curl -X POST -d '{"label":"rewards-script"}' \
-H "Authorization: Bearer YOUR_LOGIN_TOKEN" \
https://ntfy.sh/v1/account/tokens
```
### **Token Format**
- Tokens start with `tk_` (e.g., `tk_abc123def456...`)
- Use Bearer authentication format
- Tokens are permanent until revoked
---
## 📲 Receiving Notifications
### **Mobile Apps**
- **Android** — [NTFY on Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy)
- **iOS** — [NTFY on App Store](https://apps.apple.com/app/ntfy/id1625396347)
- **F-Droid** — Available for Android
### **Desktop Options**
- **Web Interface** — Visit your NTFY server URL
- **Desktop Apps** — Available for Linux, macOS, Windows
- **Browser Extension** — Chrome/Firefox extensions
### **Setup Steps**
1. **Install** NTFY app on your device
2. **Add subscription** to your topic name
3. **Enter server URL** (if self-hosted)
4. **Test** with a manual message
---
## 🔔 Notification Types
### **Error Notifications**
**Priority:** Max 🚨 | **Trigger:** Script errors and failures
```
[ERROR] DESKTOP [LOGIN] Failed to login: Invalid credentials
```
### **Warning Notifications**
**Priority:** High ⚠️ | **Trigger:** Important warnings
```
[WARN] MOBILE [SEARCH] Didn't gain expected points from search
```
### **Info Notifications**
**Priority:** Default 🏆 | **Trigger:** Important milestones
```
[INFO] MAIN [TASK] Started tasks for account user@email.com
```
### **Buy Mode Notifications**
**Priority:** High 💳 | **Trigger:** Point spending detected
```
💳 Spend detected (Buy Mode)
Account: user@email.com
Spent: -500 points
Current: 12,500 points
Session spent: 1,200 points
```
### **Conclusion Summary**
**End-of-run summary with rich formatting:**
```
🎯 Microsoft Rewards Summary
Accounts: 3 • 0 with issues
Total: 15,230 -> 16,890 (+1,660)
Average Duration: 8m 32s
Cumulative Runtime: 25m 36s
```
---
## 🤝 Integration with Discord
### **Complementary Setup**
Use **both** NTFY and Discord for comprehensive monitoring:
```json
{
"notifications": {
"webhook": {
"enabled": true,
"url": "https://discord.com/api/webhooks/..."
},
"conclusionWebhook": {
"enabled": true,
"url": "https://discord.com/api/webhooks/..."
},
"ntfy": {
"enabled": true,
"url": "https://ntfy.sh",
"topic": "rewards-script"
}
}
}
```
### **Coverage Comparison**
| Feature | NTFY | Discord |
|---------|------|---------|
| **Mobile push** | ✅ Instant | ❌ App required |
| **Rich formatting** | ❌ Text only | ✅ Embeds + colors |
| **Desktop alerts** | ✅ Native | ✅ App notifications |
| **Offline delivery** | ✅ Queued | ❌ Real-time only |
| **Self-hosted** | ✅ Easy | ❌ Complex |
---
## 🎛️ Advanced Configuration
### **Custom Topic Names**
Use descriptive, unique topic names:
```json
{
"topic": "rewards-production-server1"
}
{
"topic": "msn-rewards-home-pc"
}
{
"topic": "rewards-dev-testing"
}
```
### **Environment-Specific**
```json
{
"notifications": {
"ntfy": {
"enabled": true,
"url": "https://ntfy.internal.lan",
"topic": "homelab-rewards",
"authToken": "tk_homelab_token"
}
}
}
```
---
## 🧪 Testing & Debugging
### **Manual Test Message**
```bash
# Public server (no auth)
curl -d "Test message from rewards script" https://ntfy.sh/your-topic
# With authentication
curl -H "Authorization: Bearer tk_your_token" \
-d "Authenticated test message" \
https://ntfy.sh/your-topic
```
### **Script Debug Mode**
```powershell
$env:DEBUG_REWARDS_VERBOSE=1; npm start
```
### **Server Health Check**
```bash
# Check NTFY server status
curl -s https://ntfy.sh/v1/health
# List your topics (with auth)
curl -H "Authorization: Bearer tk_your_token" \
https://ntfy.sh/v1/account/topics
```
---
## 🛠️ Troubleshooting
| Problem | Solution |
|---------|----------|
| **No notifications** | Check topic spelling; verify app subscription |
| **Auth failures** | Verify token format (`tk_`); check token validity |
| **Wrong server** | Test server URL in browser; check HTTPS/HTTP |
| **Rate limits** | Switch to self-hosted; reduce notification frequency |
### **Common Fixes**
-**Topic name** — Must match exactly between config and app
-**Server URL** — Include `https://` and check accessibility
-**Token format** — Must start with `tk_` for authentication
-**Network** — Verify firewall/proxy settings
---
## 🏠 Homelab Integration
### **Official Support**
NTFY is included in:
- **Debian Trixie** (testing)
- **Ubuntu** (latest versions)
### **Popular Integrations**
- **Sonarr/Radarr** — Download completion notifications
- **Prometheus** — Alert manager integration
- **Home Assistant** — Automation notifications
- **Portainer** — Container status alerts
### **Docker Stack Example**
```yaml
version: '3.8'
services:
ntfy:
image: binwiederhier/ntfy
container_name: ntfy
ports:
- "80:80"
volumes:
- ./ntfy-data:/var/lib/ntfy
environment:
- NTFY_BASE_URL=https://ntfy.yourdomain.com
command: serve
rewards:
build: .
depends_on:
- ntfy
environment:
- NTFY_URL=http://ntfy:80
```
---
## 🔒 Privacy & Security
### **Public Server (ntfy.sh)**
- ⚠️ Messages pass through public infrastructure
- ⚠️ Topic names visible in logs
- ✅ Suitable for non-sensitive notifications
### **Self-Hosted Server**
- ✅ Complete control over data
- ✅ Private network deployment possible
- ✅ Recommended for sensitive information
### **Best Practices**
- 🔐 Use **unique, non-guessable** topic names
- 🔑 Enable **authentication** for sensitive notifications
- 🏠 Use **self-hosted server** for maximum privacy
- 🔄 **Regularly rotate** authentication tokens
### **Data Retention**
- 📨 Messages are **not permanently stored**
- ⏱️ Delivery attempts **retried** for short periods
- 🗑️ **No long-term** message history
---
## ⚡ Performance Impact
### **Script Performance**
-**Minimal overhead** — Fire-and-forget notifications
-**Non-blocking** — Failed notifications don't affect script
-**Asynchronous** — No execution delays
### **Network Usage**
- 📊 **Low bandwidth** — Text-only messages
-**HTTP POST** — Simple, efficient protocol
- 🔄 **Retry logic** — Automatic failure recovery
---
## 🔗 Related Guides
- **[Discord Webhooks](./conclusionwebhook.md)** — Rich notification embeds
- **[Getting Started](./getting-started.md)** — Initial setup and configuration
- **[Buy Mode](./buy-mode.md)** — Manual purchasing notifications
- **[Security](./security.md)** — Privacy and data protection

611
docs/proxy.md Normal file
View File

@@ -0,0 +1,611 @@
# 🌐 Proxy Configuration
<div align="center">
**🔒 Route traffic through proxy servers for privacy and flexibility**
*Enhanced anonymity and geographic control*
</div>
---
## 🎯 What Are Proxies?
Proxies act as **intermediaries** between your script and Microsoft's servers, providing enhanced privacy, geographic flexibility, and network management capabilities.
### **Key Benefits**
- 🎭 **IP masking** — Hide your real IP address
- 🌍 **Geographic flexibility** — Appear to browse from different locations
-**Rate limiting** — Distribute requests across multiple IPs
- 🔧 **Network control** — Route traffic through specific servers
- 🔒 **Privacy enhancement** — Add layer of anonymity
---
## ⚙️ Configuration
### **Basic Setup**
```json
{
"browser": {
"proxy": {
"enabled": false,
"server": "proxy.example.com:8080",
"username": "",
"password": "",
"bypass": []
}
}
}
```
### **Configuration Options**
| Setting | Description | Example |
|---------|-------------|---------|
| `enabled` | Enable proxy usage | `true` |
| `server` | Proxy server address and port | `"proxy.example.com:8080"` |
| `username` | Proxy authentication username | `"proxyuser"` |
| `password` | Proxy authentication password | `"proxypass123"` |
| `bypass` | Domains to bypass proxy | `["localhost", "*.internal.com"]` |
---
## 🔌 Supported Proxy Types
### **HTTP Proxies**
**Most common type for web traffic**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "http://proxy.example.com:8080",
"username": "user",
"password": "pass"
}
}
}
```
### **HTTPS Proxies**
**Encrypted proxy connections**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "https://secure-proxy.example.com:8080",
"username": "user",
"password": "pass"
}
}
}
```
### **SOCKS Proxies**
**Support for SOCKS4 and SOCKS5**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "socks5://socks-proxy.example.com:1080",
"username": "user",
"password": "pass"
}
}
}
```
---
## 🏢 Popular Proxy Providers
### **Residential Proxies (Recommended)**
**High-quality IPs from real devices**
#### **Top Providers**
- **Bright Data** (formerly Luminati) — Premium quality
- **Smartproxy** — User-friendly dashboard
- **Oxylabs** — Enterprise-grade
- **ProxyMesh** — Developer-focused
#### **Configuration Example**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "rotating-residential.brightdata.com:22225",
"username": "customer-username-session-random",
"password": "your-password"
}
}
}
```
### **Datacenter Proxies**
**Fast and affordable server-based IPs**
#### **Popular Providers**
- **SquidProxies** — Reliable performance
- **MyPrivateProxy** — Dedicated IPs
- **ProxyRack** — Budget-friendly
- **Storm Proxies** — Rotating options
#### **Configuration Example**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "datacenter.squidproxies.com:8080",
"username": "username",
"password": "password"
}
}
}
```
### **Free Proxies**
**⚠️ Not recommended for production use**
#### **Risks**
- ❌ Unreliable connections
- ❌ Potential security issues
- ❌ Often blocked by services
- ❌ Poor performance
---
## 🔐 Authentication Methods
### **Username/Password (Most Common)**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "proxy.example.com:8080",
"username": "your-username",
"password": "your-password"
}
}
}
```
### **IP Whitelisting**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "proxy.example.com:8080",
"username": "",
"password": ""
}
}
}
```
**Setup Steps:**
1. Contact proxy provider
2. Provide your server's IP address
3. Configure whitelist in provider dashboard
4. Remove credentials from config
### **Session-Based Authentication**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "session-proxy.example.com:8080",
"username": "customer-session-sticky123",
"password": "your-password"
}
}
}
```
---
## 🚫 Bypass Configuration
### **Local Development**
**Bypass proxy for local services**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "proxy.example.com:8080",
"bypass": [
"localhost",
"127.0.0.1",
"*.local",
"*.internal"
]
}
}
}
```
### **Specific Domains**
**Route certain domains directly**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "proxy.example.com:8080",
"bypass": [
"*.microsoft.com",
"login.live.com",
"account.microsoft.com"
]
}
}
}
```
### **Advanced Patterns**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "proxy.example.com:8080",
"bypass": [
"*.intranet.*",
"192.168.*.*",
"10.*.*.*",
"<local>"
]
}
}
}
```
---
## 🎛️ Advanced Configurations
### **Per-Account Proxies**
**Different proxies for different accounts**
```json
{
"accounts": [
{
"email": "user1@example.com",
"password": "password1",
"proxy": {
"enabled": true,
"server": "proxy1.example.com:8080"
}
},
{
"email": "user2@example.com",
"password": "password2",
"proxy": {
"enabled": true,
"server": "proxy2.example.com:8080"
}
}
]
}
```
### **Failover Configuration**
**Multiple proxy servers for redundancy**
```json
{
"browser": {
"proxy": {
"enabled": true,
"servers": [
"primary-proxy.example.com:8080",
"backup-proxy.example.com:8080",
"emergency-proxy.example.com:8080"
],
"username": "user",
"password": "pass"
}
}
}
```
### **Geographic Routing**
**Location-specific proxy selection**
```json
{
"browser": {
"proxy": {
"enabled": true,
"regions": {
"us": "us-proxy.example.com:8080",
"eu": "eu-proxy.example.com:8080",
"asia": "asia-proxy.example.com:8080"
},
"defaultRegion": "us"
}
}
}
```
---
## 🔒 Security & Environment Variables
### **Credential Protection**
**Secure proxy authentication**
**Environment Variables:**
```powershell
# Set in environment
$env:PROXY_USERNAME="your-username"
$env:PROXY_PASSWORD="your-password"
```
**Configuration:**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "proxy.example.com:8080",
"username": "${PROXY_USERNAME}",
"password": "${PROXY_PASSWORD}"
}
}
}
```
### **HTTPS Verification**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "proxy.example.com:8080",
"verifySSL": true,
"rejectUnauthorized": true
}
}
}
```
### **Connection Encryption**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "https://encrypted-proxy.example.com:8080",
"tls": {
"enabled": true,
"version": "TLSv1.3"
}
}
}
}
```
---
## 🧪 Testing & Debugging
### **Manual Tests**
```bash
# Test proxy connection
curl --proxy proxy.example.com:8080 http://httpbin.org/ip
# Test with authentication
curl --proxy user:pass@proxy.example.com:8080 http://httpbin.org/ip
# Test geolocation
curl --proxy proxy.example.com:8080 http://ipinfo.io/json
```
### **Script Debug Mode**
```powershell
$env:DEBUG_PROXY=1; npm start
```
### **Health Check Script**
```bash
#!/bin/bash
PROXY="proxy.example.com:8080"
curl --proxy $PROXY --connect-timeout 10 http://httpbin.org/status/200
echo "Proxy health: $?"
```
---
## 🛠️ Troubleshooting
| Problem | Error | Solution |
|---------|-------|----------|
| **Connection Failed** | `ECONNREFUSED` | Verify server address/port; check firewall |
| **Auth Failed** | `407 Proxy Authentication Required` | Verify username/password; check IP whitelist |
| **Timeout** | `Request timeout` | Increase timeout values; try different server |
| **SSL Error** | `certificate verify failed` | Disable SSL verification; update certificates |
### **Common Error Messages**
#### **Connection Issues**
```
[ERROR] Proxy connection failed: ECONNREFUSED
```
**Solutions:**
- ✅ Verify proxy server address and port
- ✅ Check proxy server is running
- ✅ Confirm firewall allows connections
- ✅ Test with different proxy server
#### **Authentication Issues**
```
[ERROR] Proxy authentication failed: 407 Proxy Authentication Required
```
**Solutions:**
- ✅ Verify username and password
- ✅ Check account is active with provider
- ✅ Confirm IP is whitelisted (if applicable)
- ✅ Try different authentication method
#### **Performance Issues**
```
[ERROR] Proxy timeout: Request timeout
```
**Solutions:**
- ✅ Increase timeout values
- ✅ Check proxy server performance
- ✅ Try different proxy server
- ✅ Reduce concurrent connections
---
## ⚡ Performance Optimization
### **Connection Settings**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "proxy.example.com:8080",
"timeouts": {
"connect": 30000,
"request": 60000,
"idle": 120000
},
"connectionPooling": true,
"maxConnections": 10
}
}
}
```
### **Compression Settings**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "proxy.example.com:8080",
"compression": true,
"gzip": true
}
}
}
```
### **Monitoring Metrics**
- **Connection Success Rate** — % of successful proxy connections
- **Response Time** — Average request latency through proxy
- **Bandwidth Usage** — Data transferred through proxy
- **Error Rate** — % of failed requests via proxy
---
## 🐳 Container Integration
### **Docker Environment**
```dockerfile
# Dockerfile
ENV PROXY_ENABLED=true
ENV PROXY_SERVER=proxy.example.com:8080
ENV PROXY_USERNAME=user
ENV PROXY_PASSWORD=pass
```
### **Kubernetes ConfigMap**
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: rewards-proxy-config
data:
proxy.json: |
{
"enabled": true,
"server": "proxy.example.com:8080",
"username": "user",
"password": "pass"
}
```
### **Environment-Specific**
```json
{
"development": {
"proxy": { "enabled": false }
},
"staging": {
"proxy": {
"enabled": true,
"server": "staging-proxy.example.com:8080"
}
},
"production": {
"proxy": {
"enabled": true,
"server": "prod-proxy.example.com:8080"
}
}
}
```
---
## 📊 Best Practices
### **Proxy Selection**
- 🏆 **Residential > Datacenter** — Better for avoiding detection
- 💰 **Paid > Free** — Reliability and security
- 🔄 **Multiple providers** — Redundancy and failover
- 🌍 **Geographic diversity** — Flexibility and compliance
### **Configuration Management**
- 🔑 **Environment variables** — Secure credential storage
- 🧪 **Test before deploy** — Verify configuration works
- 📊 **Monitor performance** — Track availability and speed
- 🔄 **Backup configs** — Ready failover options
### **Security Guidelines**
- 🔒 **HTTPS proxies** — Encrypted connections when possible
- 🛡️ **SSL verification** — Verify certificates
- 🔄 **Rotate credentials** — Regular password updates
- 👁️ **Monitor access** — Watch for unauthorized usage
---
## ⚖️ Legal & Compliance
### **Terms of Service**
- 📋 Review Microsoft's Terms of Service
- 📄 Understand proxy provider's acceptable use policy
- 🌍 Ensure compliance with local regulations
- 🗺️ Consider geographic restrictions
### **Data Privacy**
- 🔍 Understand data flow through proxy
- 📝 Review proxy provider's data retention policies
- 🔐 Implement additional encryption if needed
- 📊 Monitor proxy logs and access
### **Rate Limiting**
- ⏱️ Respect Microsoft's rate limits
- ⏸️ Implement proper delays between requests
- 🚦 Monitor for IP blocking or throttling
- 🔄 Use proxy rotation to distribute load
---
## 🔗 Related Guides
- **[Getting Started](./getting-started.md)** — Initial setup and configuration
- **[Security](./security.md)** — Privacy and data protection
- **[Docker](./docker.md)** — Container deployment with proxies
- **[Humanization](./humanization.md)** — Natural behavior patterns

648
docs/schedule.md Normal file
View File

@@ -0,0 +1,648 @@
# ⏰ Scheduler & Automation
<div align="center">
**🚀 Built-in scheduler for automated daily execution**
*Set it and forget it*
</div>
---
## 🎯 What is the Scheduler?
The built-in scheduler provides **automated script execution** at specified times without requiring external cron jobs or task schedulers.
### **Key Features**
- 📅 **Daily automation** — Run at the same time every day
- 🌍 **Timezone aware** — Handles DST automatically
- 🔄 **Multiple passes** — Execute script multiple times per run
- 🏖️ **Vacation mode** — Skip random days monthly
- 🎲 **Jitter support** — Randomize execution times
-**Immediate start** — Option to run on startup
---
## ⚙️ Configuration
### **Basic Setup**
```json
{
"schedule": {
"enabled": true,
"time": "09:00",
"timeZone": "America/New_York",
"runImmediatelyOnStart": true
},
"passesPerRun": 2
}
```
### **Advanced Setup with Vacation Mode**
```json
{
"schedule": {
"enabled": true,
"time": "10:00",
"timeZone": "Europe/Paris",
"runImmediatelyOnStart": false
},
"passesPerRun": 3,
"vacation": {
"enabled": true,
"minDays": 3,
"maxDays": 5
}
}
```
### **Configuration Options**
| Setting | Default | Description |
|---------|---------|-------------|
| `enabled` | `false` | Enable built-in scheduler |
| `time` | `"09:00"` | Daily execution time (24-hour format) |
| `timeZone` | `"UTC"` | IANA timezone identifier |
| `runImmediatelyOnStart` | `true` | Execute once on process startup |
| `passesPerRun` | `1` | Number of complete runs per execution |
| `vacation.enabled` | `false` | Skip random monthly off-block |
| `vacation.minDays` | `3` | Minimum vacation days |
| `vacation.maxDays` | `5` | Maximum vacation days |
---
## 🚀 How It Works
### **Daily Scheduling**
1. **Calculate next run** — Timezone-aware scheduling
2. **Wait until time** — Minimal resource usage
3. **Execute passes** — Run script specified number of times
4. **Schedule next day** — Automatic DST adjustment
### **Startup Behavior**
#### **Immediate Start Enabled (`true`)**
- **Before scheduled time** → Run immediately + wait for next scheduled time
- **After scheduled time** → Run immediately + wait for tomorrow's time
#### **Immediate Start Disabled (`false`)**
- **Any time** → Always wait for next scheduled time
### **Multiple Passes**
- Each pass processes **all accounts** through **all tasks**
- Useful for **maximum point collection**
- Higher passes = **more points** but **increased detection risk**
---
## 🏖️ Vacation Mode
### **Monthly Off-Blocks**
Vacation mode randomly selects a **contiguous block of days** each month to skip execution.
### **Configuration**
```json
{
"vacation": {
"enabled": true,
"minDays": 3,
"maxDays": 5
}
}
```
### **How It Works**
- **Random selection** — Different days each month
- **Contiguous block** — Skip consecutive days, not scattered
- **Independent** — Works with weekly random off-days
- **Logged** — Shows selected vacation period
### **Example Output**
```
[SCHEDULE] Selected vacation block this month: 2025-01-15 → 2025-01-18
[SCHEDULE] Skipping run - vacation mode (3 days remaining)
```
---
## 🌍 Supported Timezones
### **North America**
- `America/New_York` — Eastern Time
- `America/Chicago` — Central Time
- `America/Denver` — Mountain Time
- `America/Los_Angeles` — Pacific Time
- `America/Phoenix` — Arizona (no DST)
### **Europe**
- `Europe/London` — GMT/BST
- `Europe/Paris` — CET/CEST
- `Europe/Berlin` — CET/CEST
- `Europe/Rome` — CET/CEST
- `Europe/Moscow` — MSK
### **Asia Pacific**
- `Asia/Tokyo` — JST
- `Asia/Shanghai` — CST
- `Asia/Kolkata` — IST
- `Australia/Sydney` — AEST/AEDT
- `Pacific/Auckland` — NZST/NZDT
---
## 🎲 Randomization & Watchdog
### **Environment Variables**
```powershell
# Add random delay before first run (5-20 minutes)
$env:SCHEDULER_INITIAL_JITTER_MINUTES_MIN=5
$env:SCHEDULER_INITIAL_JITTER_MINUTES_MAX=20
# Add daily jitter to scheduled time (2-10 minutes)
$env:SCHEDULER_DAILY_JITTER_MINUTES_MIN=2
$env:SCHEDULER_DAILY_JITTER_MINUTES_MAX=10
# Kill stuck passes after N minutes
$env:SCHEDULER_PASS_TIMEOUT_MINUTES=180
# Run each pass in separate process (recommended)
$env:SCHEDULER_FORK_PER_PASS=true
```
### **Benefits**
- **Avoid patterns** — Prevents exact-time repetition
- **Protection** — Kills stuck processes
- **Isolation** — Process separation for stability
---
## 🖥️ Running the Scheduler
### **Development Mode**
```powershell
npm run ts-schedule
```
### **Production Mode**
```powershell
npm run build
npm run start:schedule
```
### **Background Execution**
```powershell
# Windows Background (PowerShell)
Start-Process -NoNewWindow -FilePath "npm" -ArgumentList "run", "start:schedule"
# Alternative: Windows Task Scheduler (recommended)
# Create scheduled task via GUI or schtasks command
```
---
## 📊 Usage Examples
### **Basic Daily Automation**
```json
{
"schedule": {
"enabled": true,
"time": "08:00",
"timeZone": "America/New_York"
}
}
```
**Perfect for morning routine** — Catch daily resets
### **Multiple Daily Passes**
```json
{
"schedule": {
"enabled": true,
"time": "10:00",
"timeZone": "Europe/London",
"runImmediatelyOnStart": false
},
"passesPerRun": 3
}
```
🔄 **Maximum points** with higher detection risk
### **Conservative with Vacation**
```json
{
"schedule": {
"enabled": true,
"time": "20:00",
"timeZone": "America/Los_Angeles"
},
"passesPerRun": 1,
"vacation": {
"enabled": true,
"minDays": 4,
"maxDays": 6
}
}
```
🏖️ **Natural patterns** with monthly breaks
---
## 🐳 Docker Integration
### **Built-in Scheduler (Recommended)**
```yaml
services:
microsoft-rewards-script:
build: .
environment:
TZ: Europe/Paris
command: ["npm", "run", "start:schedule"]
```
- Uses `passesPerRun` from config
- Single long-running process
- No external cron needed
### **External Cron (Project Default)**
```yaml
services:
microsoft-rewards-script:
build: .
environment:
CRON_SCHEDULE: "0 7,16,20 * * *"
RUN_ON_START: "true"
```
- Uses `run_daily.sh` with random delays
- Multiple cron executions
- Lockfile prevents overlaps
---
## 📋 Logging Output
### **Scheduler Initialization**
```
[SCHEDULE] Scheduler initialized for daily 09:00 America/New_York
[SCHEDULE] Next run scheduled for 2025-01-21 09:00:00 EST
```
### **Daily Execution**
```
[SCHEDULE] Starting scheduled run (pass 1 of 2)
[SCHEDULE] Completed scheduled run in 12m 34s
[SCHEDULE] Next run scheduled for 2025-01-22 09:00:00 EST
```
### **Time Calculations**
```
[SCHEDULE] Current time: 2025-01-20 15:30:00 EDT
[SCHEDULE] Target time: 2025-01-21 09:00:00 EDT
[SCHEDULE] Waiting 17h 30m until next run
```
---
## 🛠️ Troubleshooting
| Problem | Solution |
|---------|----------|
| **Scheduler not running** | Check `enabled: true`; verify timezone format |
| **Wrong execution time** | Verify system clock; check DST effects |
| **Memory growth** | Restart process weekly; monitor logs |
| **Missed executions** | Check system sleep/hibernation; verify process |
### **Debug Commands**
```powershell
# Test timezone calculation
node -e "console.log(new Date().toLocaleString('en-US', {timeZone: 'America/New_York'}))"
# Verify config syntax
node -e "console.log(JSON.parse((Get-Content 'src/config.json' | Out-String)))"
# Check running processes
Get-Process | Where-Object {$_.ProcessName -eq "node"}
```
---
## ⚡ Performance & Best Practices
### **Optimal Timing**
- **🌅 Morning (7-10 AM)** — Catch daily resets
- **🌆 Evening (7-10 PM)** — Complete remaining tasks
- **❌ Avoid peak hours** — Reduce detection during high traffic
### **Pass Recommendations**
- **1 pass** — Safest, good for most users
- **2-3 passes** — Balance of points vs. risk
- **4+ passes** — Higher risk, development only
### **Monitoring**
- ✅ Check logs regularly for errors
- ✅ Monitor point collection trends
- ✅ Verify scheduler status weekly
---
## 🔗 Alternative Solutions
### **Windows Task Scheduler**
```powershell
# Create scheduled task
schtasks /create /tn "MS-Rewards" /tr "npm start" /sc daily /st 09:00 /sd 01/01/2025
```
### **PowerShell Scheduled Job**
```powershell
# Register scheduled job
Register-ScheduledJob -Name "MSRewards" -ScriptBlock {cd "C:\path\to\project"; npm start} -Trigger (New-JobTrigger -Daily -At 9am)
```
---
## 🔗 Related Guides
- **[Getting Started](./getting-started.md)** — Initial setup and configuration
- **[Humanization](./humanization.md)** — Natural behavior patterns
- **[Docker](./docker.md)** — Container deployment
- **[Job State](./jobstate.md)** — Execution state management
## Usage Examples
### Basic Daily Run
```json
{
"schedule": {
"enabled": true,
"time": "08:00",
"timeZone": "America/New_York"
}
}
```
### Multiple Daily Passes
```json
{
"schedule": {
"enabled": true,
"time": "10:00",
"timeZone": "Europe/London",
"runImmediatelyOnStart": false
},
"passesPerRun": 3
}
```
### Development Testing
```json
{
"schedule": {
"enabled": true,
"time": "00:01",
"timeZone": "UTC",
"runImmediatelyOnStart": true
}
}
```
## Supported Timezones
Common IANA timezone identifiers:
### North America
- `America/New_York` (Eastern Time)
- `America/Chicago` (Central Time)
- `America/Denver` (Mountain Time)
- `America/Los_Angeles` (Pacific Time)
- `America/Phoenix` (Arizona - no DST)
### Europe
- `Europe/London` (GMT/BST)
- `Europe/Paris` (CET/CEST)
- `Europe/Berlin` (CET/CEST)
- `Europe/Rome` (CET/CEST)
- `Europe/Moscow` (MSK)
### Asia Pacific
- `Asia/Tokyo` (JST)
- `Asia/Shanghai` (CST)
- `Asia/Kolkata` (IST)
- `Australia/Sydney` (AEST/AEDT)
- `Pacific/Auckland` (NZST/NZDT)
### UTC Variants
- `UTC` (Coordinated Universal Time)
- `GMT` (Greenwich Mean Time)
## Running the Scheduler
### Development Mode
```bash
npm run ts-schedule
```
### Production Mode
```bash
npm run build
npm run start:schedule
```
### Optional Randomization and Watchdog
You can introduce slight randomness to the start times and protect against stuck runs:
- `SCHEDULER_INITIAL_JITTER_MINUTES_MIN` / `SCHEDULER_INITIAL_JITTER_MINUTES_MAX`
- Adds a onetime random delay before the very first run after the scheduler starts.
- Example: `SCHEDULER_INITIAL_JITTER_MINUTES_MIN=5` and `SCHEDULER_INITIAL_JITTER_MINUTES_MAX=20` delays the first run by 520 minutes.
- `SCHEDULER_DAILY_JITTER_MINUTES_MIN` / `SCHEDULER_DAILY_JITTER_MINUTES_MAX`
- Adds an extra random delay to each daily scheduled execution.
- Example: 210 minutes of daily jitter to avoid exact same second each day.
- `SCHEDULER_PASS_TIMEOUT_MINUTES`
- Kills a stuck pass after N minutes (default 180). Useful if the underlying browser gets stuck.
- `SCHEDULER_FORK_PER_PASS`
- Defaults to `true`. When `true`, each pass runs in a child Node process so a stuck pass can be terminated without killing the scheduler. Set to `false` to run passes inprocess (not recommended).
### Background Execution
```bash
# Linux/macOS (background process)
nohup npm run start:schedule > schedule.log 2>&1 &
# Windows (background service - requires additional setup)
# Recommend using Task Scheduler or Windows Service wrapper
```
## Process Management
### Long-Running Process
- Scheduler runs continuously
- Automatically handles timezone changes
- Graceful handling of system clock adjustments
### Memory Management
- Minimal memory footprint between runs
- Garbage collection after each execution
- No memory leaks in long-running processes
### Error Recovery
- Failed runs don't affect future scheduling
- Automatic retry on next scheduled time
- Error logging for troubleshooting
## Logging Output
### Scheduler Events
```
[SCHEDULE] Scheduler initialized for daily 09:00 America/New_York
[SCHEDULE] Next run scheduled for 2025-09-21 09:00:00 EST
[SCHEDULE] Starting scheduled run (pass 1 of 2)
[SCHEDULE] Completed scheduled run in 12m 34s
[SCHEDULE] Next run scheduled for 2025-09-22 09:00:00 EST
```
### Time Calculations
```
[SCHEDULE] Current time: 2025-09-20 15:30:00 EDT
[SCHEDULE] Target time: 2025-09-21 09:00:00 EDT
[SCHEDULE] Waiting 17h 30m until next run
```
## Integration with Other Features
### Docker Compatibility
- Scheduler works in Docker containers
- Alternative to external cron jobs
- Timezone handling in containerized environments
### Buy Mode Exclusion
- Scheduler only runs automation mode
- Buy mode (`-buy`) ignores scheduler settings
- Manual executions bypass scheduler
### Clustering
- Scheduler runs only in single-process mode
- Clustering disabled when scheduler is active
- Use scheduler OR clustering, not both
## Best Practices
### Optimal Timing
- **Morning runs**: Catch daily resets and new activities
- **Evening runs**: Complete remaining tasks before midnight
- **Avoid peak hours**: Reduce detection risk during high traffic
### Timezone Selection
- Use your local timezone for easier monitoring
- Consider Microsoft Rewards server timezone
- Account for daylight saving time changes
### Multiple Passes
- **2-3 passes**: Good balance of points vs. detection risk
- **More passes**: Higher detection risk
- **Single pass**: Safest but may miss some points
### Monitoring
- Check logs regularly for errors
- Monitor point collection trends
- Verify scheduler is running as expected
## Troubleshooting
### Common Issues
**Scheduler not running:**
- Check `enabled: true` in config
- Verify timezone format is correct
- Ensure no syntax errors in config.json
**Wrong execution time:**
- Verify system clock is accurate
- Check timezone identifier spelling
- Consider daylight saving time effects
**Memory growth over time:**
- Restart scheduler process weekly
- Monitor system resource usage
- Check for memory leaks in logs
**Missed executions:**
- System was sleeping/hibernating
- Process was killed or crashed
- Clock was adjusted significantly
### Debug Commands
```bash
# Test timezone calculation
node -e "console.log(new Date().toLocaleString('en-US', {timeZone: 'America/New_York'}))"
# Verify config syntax
node -e "console.log(JSON.parse(require('fs').readFileSync('src/config.json')))"
# Check process status
ps aux | grep "start:schedule"
```
## Alternative Solutions
### External Cron (Linux/macOS)
```bash
# Crontab entry for 9 AM daily
0 9 * * * cd /path/to/MSN-V2 && npm start
# Multiple times per day
0 9,15,21 * * * cd /path/to/MSN-V2 && npm start
```
### Windows Task Scheduler
- Create scheduled task via Task Scheduler
- Set trigger for daily execution
- Configure action to run `npm start` in project directory
### Docker Cron
```dockerfile
# Add to Dockerfile
RUN apt-get update && apt-get install -y cron
COPY crontab /etc/cron.d/rewards-cron
RUN crontab /etc/cron.d/rewards-cron
```
### Docker + Built-in Scheduler
Au lieu d'utiliser cron, vous pouvez lancer le scheduler intégré dans le conteneur (un seul process longvivant) :
```yaml
services:
microsoft-rewards-script:
build: .
environment:
TZ: Europe/Paris
command: ["npm", "run", "start:schedule"]
```
Dans ce mode :
- `passesPerRun` fonctionne (exécutera plusieurs passes à chaque horaire interne défini par `src/config.json`).
- Vous n'avez plus besoin de `CRON_SCHEDULE` ni de `run_daily.sh`.
### Docker + External Cron (par défaut du projet)
Si vous préférez la planification par cron système dans le conteneur (valeur par défaut du projet) :
- Utilisez `CRON_SCHEDULE` (ex.: `0 7,16,20 * * *`).
- `run_daily.sh` introduit un délai aléatoire (par défaut 550 min) et un lockfile pour éviter les chevauchements.
- `RUN_ON_START=true` déclenche une exécution immédiate au démarrage du conteneur (sans délai aléatoire).
## Performance Considerations
### System Resources
- Minimal CPU usage between runs
- Low memory footprint when idle
- No network activity during waiting periods
### Startup Time
- Fast initialization (< 1 second)
- Quick timezone calculations
- Immediate scheduling of next run
### Reliability
- Robust error handling
- Automatic recovery from failures
- Consistent execution timing

296
docs/security.md Normal file
View File

@@ -0,0 +1,296 @@
# 🔒 Security & Privacy Guide
<div align="center">
**🛡️ Comprehensive security measures and incident response**
*Protect your accounts and maintain privacy*
</div>
---
## 🎯 Security Overview
This guide explains how the script **detects security-related issues**, what it does automatically, and how you can **resolve incidents** safely.
### **Security Features**
- 🚨 **Automated detection** — Recognizes account compromise attempts
- 🛑 **Emergency halting** — Stops all automation during incidents
- 🔔 **Strong alerts** — Immediate notifications via Discord/NTFY
- 📋 **Recovery guidance** — Step-by-step incident resolution
- 🔒 **Privacy protection** — Local-only operation by default
---
## 🚨 Security Incidents & Resolutions
### **Recovery Email Mismatch**
#### **Symptoms**
During Microsoft login, the page shows a masked recovery email like `ko*****@hacker.net` that **doesn't match** your expected recovery email pattern.
#### **What the Script Does**
- 🛑 **Halts automation** for the current account (leaves page open for manual action)
- 🚨 **Sends strong alerts** to all channels and engages global standby
- ⏸️ **Stops processing** — No further accounts are processed
- 🔔 **Repeats reminders** every 5 minutes until intervention
#### **Likely Causes**
- ⚠️ **Account takeover** — Recovery email changed by someone else
- 🔄 **Recent change** — You changed recovery email but forgot to update config
#### **How to Fix**
1. **🔍 Verify account security** in Microsoft Account settings
2. **📝 Update config** if you changed recovery email yourself:
```json
{
"email": "your@email.com",
"recoveryEmail": "ko*****@hacker.net"
}
```
3. **🔐 Change password** and review sign-in activity if compromise suspected
4. **🚀 Restart script** to resume normal operation
#### **Prevention**
- ✅ Keep `recoveryEmail` in `accounts.json` up to date
- ✅ Use strong unique passwords and MFA
- ✅ Regular security reviews
---
### **"We Can't Sign You In" (Blocked)**
#### **Symptoms**
Microsoft presents a page titled **"We can't sign you in"** during login attempts.
#### **What the Script Does**
- 🛑 **Stops automation** and leaves page open for manual recovery
- 🚨 **Sends strong alert** with high priority notifications
- ⏸️ **Engages global standby** to avoid processing other accounts
#### **Likely Causes**
- ⏱️ **Temporary lock** — Rate limiting or security check from Microsoft
- 🚫 **Account restrictions** — Ban related to unusual activity
- 🔒 **Verification required** — SMS code, authenticator, or other challenges
#### **How to Fix**
1. **✅ Complete verification** challenges (SMS, authenticator, etc.)
2. **⏸️ Pause activity** for 24-48h if blocked repeatedly
3. **🔧 Reduce concurrency** and increase delays between actions
4. **🌐 Check proxies** — Ensure consistent IP/country
5. **📞 Appeal if needed** — Contact Microsoft if ban is suspected
#### **Prevention**
- ✅ **Respect rate limits** — Use humanization settings
- ✅ **Avoid patterns** — Don't run too many accounts from same IP
- ✅ **Geographic consistency** — Use proxies from your actual region
- ✅ **Human-like timing** — Avoid frequent credential retries
---
## 🔐 Privacy & Data Protection
### **Local-First Architecture**
- 💾 **All data local** — Credentials, sessions, logs stored locally only
- 🚫 **No telemetry** — Zero data collection or external reporting
- 🔒 **No cloud storage** — Everything remains on your machine
### **Credential Security**
```json
{
"accounts": [
{
"email": "user@example.com",
"password": "secure-password-here",
"totpSecret": "optional-2fa-secret"
}
]
}
```
**Best Practices:**
- 🔐 **Strong passwords** — Unique, complex passwords per account
- 🔑 **2FA enabled** — Time-based one-time passwords when possible
- 📁 **File permissions** — Restrict access to `accounts.json`
- 🔄 **Regular rotation** — Change passwords periodically
### **Session Management**
- 🍪 **Persistent cookies** — Stored locally in `sessions/` directory
- 🔒 **Encrypted storage** — Session data protected at rest
- ⏰ **Automatic expiry** — Old sessions cleaned up automatically
- 🗂️ **Per-account isolation** — No session data mixing
---
## 🌐 Network Security
### **Proxy Configuration**
```json
{
"browser": {
"proxy": {
"enabled": true,
"server": "proxy.example.com:8080",
"username": "user",
"password": "pass"
}
}
}
```
**Security Benefits:**
- 🎭 **IP masking** — Hide your real IP address
- 🌍 **Geographic flexibility** — Appear from different locations
- 🔒 **Traffic encryption** — HTTPS proxy connections
- 🛡️ **Detection avoidance** — Rotate IPs to avoid patterns
### **Traffic Analysis Protection**
- 🔐 **HTTPS only** — All Microsoft communications encrypted
- 🚫 **No plaintext passwords** — Credentials protected in transit
- 🛡️ **Certificate validation** — SSL/TLS verification enabled
- 🔍 **Deep packet inspection** resistant
---
## 🛡️ Anti-Detection Measures
### **Humanization**
```json
{
"humanization": {
"enabled": true,
"actionDelay": { "min": 150, "max": 450 },
"gestureMoveProb": 0.4,
"gestureScrollProb": 0.2
}
}
```
**Natural Behavior Simulation:**
- ⏱️ **Random delays** — Variable timing between actions
- 🖱️ **Mouse movements** — Subtle cursor adjustments
- 📜 **Scrolling gestures** — Natural page interactions
- 🎲 **Randomized patterns** — Avoid predictable automation
### **Browser Fingerprinting**
- 🌐 **Real user agents** — Authentic browser identification
- 📱 **Platform consistency** — Mobile/desktop specific headers
- 🔧 **Plugin simulation** — Realistic browser capabilities
- 🖥️ **Screen resolution** — Appropriate viewport dimensions
---
## 📊 Monitoring & Alerting
### **Real-Time Monitoring**
```json
{
"notifications": {
"webhook": {
"enabled": true,
"url": "https://discord.com/api/webhooks/..."
},
"ntfy": {
"enabled": true,
"url": "https://ntfy.sh",
"topic": "rewards-security"
}
}
}
```
**Alert Types:**
- 🚨 **Security incidents** — Account compromise attempts
- ⚠️ **Login failures** — Authentication issues
- 🔒 **Account blocks** — Access restrictions detected
- 📊 **Performance anomalies** — Unusual execution patterns
### **Log Analysis**
- 📝 **Detailed logging** — All actions recorded locally
- 🔍 **Error tracking** — Failed operations highlighted
- 📊 **Performance metrics** — Timing and success rates
- 🛡️ **Security events** — Incident timeline reconstruction
---
## 🧪 Security Testing
### **Penetration Testing**
```powershell
# Test credential handling
$env:DEBUG_SECURITY=1; npm start
# Test session persistence
$env:DEBUG_SESSIONS=1; npm start
# Test proxy configuration
$env:DEBUG_PROXY=1; npm start
```
### **Vulnerability Assessment**
- 🔍 **Regular audits** — Check for security issues
- 📦 **Dependency scanning** — Monitor npm packages
- 🔒 **Code review** — Manual security analysis
- 🛡️ **Threat modeling** — Identify attack vectors
---
## 📋 Security Checklist
### **Initial Setup**
-**Strong passwords** for all accounts
-**2FA enabled** where possible
-**File permissions** restricted to user only
-**Proxy configured** if desired
-**Notifications set up** for alerts
### **Regular Maintenance**
-**Password rotation** every 90 days
-**Session cleanup** weekly
-**Log review** for anomalies
-**Security updates** for dependencies
-**Backup verification** of configurations
### **Incident Response**
-**Alert investigation** within 15 minutes
-**Account verification** when suspicious
-**Password changes** if compromise suspected
-**Activity review** in Microsoft account settings
-**Documentation** of incidents and resolutions
---
## 🚨 Emergency Procedures
### **Account Compromise Response**
1. **🛑 Immediate shutdown** — Stop all script activity
2. **🔒 Change passwords** — Update all affected accounts
3. **📞 Contact Microsoft** — Report unauthorized access
4. **🔍 Audit activity** — Review recent sign-ins and changes
5. **🛡️ Enable additional security** — Add 2FA, recovery options
6. **📋 Document incident** — Record timeline and actions taken
### **Detection Evasion**
1. **⏸️ Temporary suspension** — Pause automation for 24-48h
2. **🔧 Reduce intensity** — Lower pass counts and frequencies
3. **🌐 Change IPs** — Rotate proxies or VPN endpoints
4. **⏰ Adjust timing** — Modify scheduling patterns
5. **🎭 Increase humanization** — More natural behavior simulation
---
## 🔗 Quick Reference Links
When the script detects a security incident, it opens this guide directly to the relevant section:
- **[Recovery Email Mismatch](#recovery-email-mismatch)** — Email change detection
- **[Account Blocked](#we-cant-sign-you-in-blocked)** — Login restriction handling
---
## 🔗 Related Guides
- **[Getting Started](./getting-started.md)** — Initial setup and configuration
- **[Accounts & 2FA](./accounts.md)** — Microsoft account setup
- **[Proxy Configuration](./proxy.md)** — Network privacy and routing
- **[Humanization](./humanization.md)** — Natural behavior patterns

395
docs/update.md Normal file
View File

@@ -0,0 +1,395 @@
# 🔄 Auto-Update System
<div align="center">
**🚀 Automatic updates to keep your installation current**
*Set it and forget it*
</div>
---
## 🎯 What is Auto-Update?
The automatic update system runs **after script completion** to keep your installation current with the latest features, bug fixes, and security patches.
### **Key Features**
- 🔄 **Automatic updates** — Runs after each script completion
- 🛡️ **Safe by design** — Fast-forward only Git updates
- 🐳 **Docker support** — Container image updates
- 🛠️ **Custom scripts** — Extensible update process
- 🔒 **Error resilient** — Failed updates don't break main script
---
## ⚙️ Configuration
### **Basic Setup**
```json
{
"update": {
"git": true,
"docker": false,
"scriptPath": "setup/update/update.mjs"
}
}
```
### **Configuration Options**
| Setting | Description | Default |
|---------|-------------|---------|
| `git` | Enable Git-based updates | `true` |
| `docker` | Enable Docker container updates | `false` |
| `scriptPath` | Path to custom update script | `"setup/update/update.mjs"` |
---
## 🚀 Update Methods
### **Git Updates (`git: true`)**
#### **What It Does**
- 📥 **Fetches latest changes** from remote repository
-**Fast-forward only pulls** (safe updates)
- 📦 **Reinstalls dependencies** (`npm ci`)
- 🔨 **Rebuilds the project** (`npm run build`)
#### **Requirements**
- ✅ Git installed and available in PATH
- ✅ Repository is a Git clone (not downloaded ZIP)
- ✅ No uncommitted local changes
- ✅ Internet connectivity
#### **Process**
```bash
git fetch --all --prune
git pull --ff-only
npm ci
npm run build
```
### **Docker Updates (`docker: true`)**
#### **What It Does**
- 📥 **Pulls latest container images**
- 🔄 **Restarts services** with new images
- 💾 **Preserves configurations** and mounted volumes
#### **Requirements**
- ✅ Docker and Docker Compose installed
-`docker-compose.yml` file present
- ✅ Proper container registry access
#### **Process**
```bash
docker compose pull
docker compose up -d
```
---
## 🛠️ Custom Update Scripts
### **Default Script**
- **Path** — `setup/update/update.mjs`
- **Format** — ES modules
- **Arguments** — Command line flags
### **Script Arguments**
- `--git` — Enable Git update process
- `--docker` — Enable Docker update process
- Both flags can be combined
### **Custom Script Example**
```javascript
// custom-update.mjs
import { execSync } from 'child_process'
const args = process.argv.slice(2)
if (args.includes('--git')) {
console.log('🔄 Running custom Git update...')
execSync('git pull && npm install', { stdio: 'inherit' })
}
if (args.includes('--docker')) {
console.log('🐳 Running custom Docker update...')
execSync('docker-compose pull && docker-compose up -d', { stdio: 'inherit' })
}
```
---
## ⏰ Execution Timing
### **When Updates Run**
| Scenario | Update Runs |
|----------|-------------|
| **Normal completion** | ✅ All accounts processed successfully |
| **Error completion** | ✅ Script finished with errors but completed |
| **Interruption** | ❌ Script killed or crashed mid-execution |
### **Update Sequence**
1. **🏁 Main script completion** — All accounts processed
2. **📊 Conclusion webhook** sent (if enabled)
3. **🚀 Update process begins**
4. **📥 Git updates** (if enabled)
5. **🐳 Docker updates** (if enabled)
6. **🔚 Process exits**
---
## 🛡️ Safety Features
### **Git Safety**
-**Fast-forward only** — Prevents overwriting local changes
- 📦 **Dependency verification** — Ensures `npm ci` succeeds
- 🔨 **Build validation** — Confirms TypeScript compilation works
### **Error Handling**
-**Update failures** don't break main script
- 🔇 **Silent failures** — Errors logged but don't crash process
- 🔄 **Rollback protection** — Failed updates don't affect current installation
### **Concurrent Execution**
- 🔒 **Single update process** — Multiple instances don't conflict
- 🚫 **Lock-free design** — No file locking needed
- 🎯 **Independent updates** — Each script copy updates separately
---
## 📊 Monitoring Updates
### **Log Output**
```
[UPDATE] Starting post-run update process
[UPDATE] Git update enabled, Docker update disabled
[UPDATE] Running: git fetch --all --prune
[UPDATE] Running: git pull --ff-only
[UPDATE] Running: npm ci
[UPDATE] Running: npm run build
[UPDATE] Update completed successfully
```
### **Update Verification**
```powershell
# Check if updates are pending
git status
# View recent commits
git log --oneline -5
# Verify build status
npm run build
```
---
## 📋 Use Cases
### **Development Environment**
| Benefit | Description |
|---------|-------------|
| **Synchronized** | Keep local installation current with repository |
| **Automated** | Automatic dependency updates |
| **Seamless** | Integration of bug fixes and features |
### **Production Deployment**
| Benefit | Description |
|---------|-------------|
| **Security** | Automated security patches |
| **Features** | Updates without manual intervention |
| **Consistent** | Same update process across servers |
### **Docker Environments**
| Benefit | Description |
|---------|-------------|
| **Images** | Container image updates |
| **Security** | Patches in base images |
| **Automated** | Service restarts |
---
## 📋 Best Practices
### **Git Configuration**
- 🧹 **Clean working directory** — Commit or stash local changes
- 🌿 **Stable branch** — Use `main` or `stable` for auto-updates
- 📝 **Regular commits** — Keep repository history clean
- 💾 **Backup data** — Sessions and accounts before updates
### **Docker Configuration**
- 🏷️ **Image tagging** — Use specific tags, not `latest` for production
- 💾 **Volume persistence** — Ensure data volumes are mounted
- 🔗 **Service dependencies** — Configure proper startup order
- 🎯 **Resource limits** — Set appropriate memory and CPU limits
### **Monitoring**
- 📝 **Check logs regularly** — Monitor update success/failure
- 🧪 **Test after updates** — Verify script functionality
- 💾 **Backup configurations** — Preserve working setups
- 📊 **Version tracking** — Record successful versions
---
## 🛠️ Troubleshooting
### **Git Issues**
| Error | Solution |
|-------|----------|
| **"Not a git repository"** | Clone repository instead of downloading ZIP |
| **"Local changes would be overwritten"** | Commit or stash local changes |
| **"Fast-forward not possible"** | Repository diverged - reset to remote state |
#### **Git Reset Command**
```powershell
# Reset to remote state (⚠️ loses local changes)
git fetch origin
git reset --hard origin/main
```
### **Docker Issues**
| Error | Solution |
|-------|----------|
| **"Docker not found"** | Install Docker and Docker Compose |
| **"Permission denied"** | Add user to docker group |
| **"No docker-compose.yml"** | Create compose file or use custom script |
#### **Docker Permission Fix**
```powershell
# Windows: Ensure Docker Desktop is running
# Linux: Add user to docker group
sudo usermod -aG docker $USER
```
### **Network Issues**
| Error | Solution |
|-------|----------|
| **"Could not resolve host"** | Check internet connectivity |
| **"Connection timeout"** | Check firewall and proxy settings |
---
## 🔧 Manual Updates
### **Git Manual Update**
```powershell
git fetch --all --prune
git pull --ff-only
npm ci
npm run build
```
### **Docker Manual Update**
```powershell
docker compose pull
docker compose up -d
```
### **Dependencies Only**
```powershell
npm ci
npm run build
```
---
## ⚙️ Update Configuration
### **Complete Disable**
```json
{
"update": {
"git": false,
"docker": false
}
}
```
### **Selective Enable**
```json
{
"update": {
"git": true, // Keep Git updates
"docker": false // Disable Docker updates
}
}
```
### **Custom Script Path**
```json
{
"update": {
"git": true,
"docker": false,
"scriptPath": "my-custom-update.mjs"
}
}
```
---
## 🔒 Security Considerations
### **Git Security**
-**Trusted remote** — Updates pull from configured remote only
-**Fast-forward only** — Prevents malicious rewrites
- 📦 **NPM registry** — Dependencies from official registry
### **Docker Security**
- 🏷️ **Verified images** — Container images from configured registries
- ✍️ **Image signatures** — Verify when possible
- 🔍 **Security scanning** — Regular scanning of base images
### **Script Execution**
- 👤 **Same permissions** — Update scripts run with same privileges
- 🚫 **No escalation** — No privilege escalation during updates
- 🔍 **Review scripts** — Custom scripts should be security reviewed
---
## 🎯 Environment Examples
### **Development**
```json
{
"update": {
"git": true,
"docker": false
}
}
```
### **Production**
```json
{
"update": {
"git": false,
"docker": true
}
}
```
### **Hybrid**
```json
{
"update": {
"git": true,
"docker": true,
"scriptPath": "setup/update/production-update.mjs"
}
}
```
---
## 🔗 Related Guides
- **[Getting Started](./getting-started.md)** — Initial setup and configuration
- **[Docker](./docker.md)** — Container deployment and management
- **[Scheduler](./schedule.md)** — Automated timing and execution
- **[Security](./security.md)** — Privacy and data protection

View File

@@ -1,50 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Ensure Playwright uses preinstalled browsers
export PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# 1. Timezone: default to UTC if not provided
: "${TZ:=UTC}"
ln -snf "/usr/share/zoneinfo/$TZ" /etc/localtime
echo "$TZ" > /etc/timezone
dpkg-reconfigure -f noninteractive tzdata
# 2. Validate CRON_SCHEDULE
if [ -z "${CRON_SCHEDULE:-}" ]; then
echo "ERROR: CRON_SCHEDULE environment variable is not set." >&2
echo "Please set CRON_SCHEDULE (e.g., \"0 2 * * *\")." >&2
exit 1
fi
# 3. Initial run without sleep if RUN_ON_START=true
if [ "${RUN_ON_START:-false}" = "true" ]; then
echo "[entrypoint] Starting initial run in background at $(date)"
(
cd /usr/src/microsoft-rewards-script || {
echo "[entrypoint-bg] ERROR: Unable to cd to /usr/src/microsoft-rewards-script" >&2
exit 1
}
# Skip random sleep for initial run, but preserve setting for cron jobs
SKIP_RANDOM_SLEEP=true src/run_daily.sh
echo "[entrypoint-bg] Initial run completed at $(date)"
) &
echo "[entrypoint] Background process started (PID: $!)"
fi
# 4. Template and register cron file with explicit timezone export
if [ ! -f /etc/cron.d/microsoft-rewards-cron.template ]; then
echo "ERROR: Cron template /etc/cron.d/microsoft-rewards-cron.template not found." >&2
exit 1
fi
# Export TZ for envsubst to use
export TZ
envsubst < /etc/cron.d/microsoft-rewards-cron.template > /etc/cron.d/microsoft-rewards-cron
chmod 0644 /etc/cron.d/microsoft-rewards-cron
crontab /etc/cron.d/microsoft-rewards-cron
echo "[entrypoint] Cron configured with schedule: $CRON_SCHEDULE and timezone: $TZ; starting cron at $(date)"
# 5. Start cron in foreground (PID 1)
exec cron -f

View File

@@ -1,17 +1,23 @@
{
"name": "microsoft-rewards-script",
"version": "1.5.3",
"version": "2.0.0",
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
"main": "index.js",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"pre-build": "npm i && rimraf dist && npx playwright install chromium",
"clean": "rimraf dist",
"pre-build": "npm i && npm run clean && node -e \"process.exit(process.env.SKIP_PLAYWRIGHT_INSTALL?0:1)\" || npx playwright install chromium",
"typecheck": "tsc --noEmit",
"build": "tsc",
"start": "node ./dist/index.js",
"ts-start": "ts-node ./src/index.ts",
"start": "node --enable-source-maps ./dist/index.js",
"ts-start": "node --loader ts-node/esm ./src/index.ts",
"dev": "ts-node ./src/index.ts -dev",
"ts-schedule": "ts-node ./src/scheduler.ts",
"start:schedule": "node --enable-source-maps ./dist/scheduler.js",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"prepare": "npm run build",
"setup": "node ./setup/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 ."
@@ -45,6 +51,7 @@
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"ms": "^2.1.3",
"luxon": "^3.5.0",
"playwright": "1.52.0",
"rebrowser-playwright": "1.52.0",
"socks-proxy-agent": "^8.0.5",

0
run.sh Executable file → Normal file
View File

67
setup/update/update.mjs Normal file
View File

@@ -0,0 +1,67 @@
/* eslint-disable linebreak-style */
/**
* Post-run auto-update script
* - If invoked with --git, runs: git fetch --all --prune; git pull --ff-only; npm ci; npm run build
* - If invoked with --docker, runs: docker compose pull; docker compose up -d
*
* Usage:
* node setup/update/update.mjs --git
* node setup/update/update.mjs --docker
*
* Notes:
* - Commands are safe-by-default: use --ff-only for pull to avoid merge commits.
* - Script is no-op if the relevant tool is not available or commands fail.
*/
import { spawn } from 'node:child_process'
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])
return code === 0
}
async function updateGit() {
const hasGit = await which('git')
if (!hasGit) return 1
await run('git', ['fetch', '--all', '--prune'])
const pullCode = await run('git', ['pull', '--ff-only'])
if (pullCode !== 0) return pullCode
const hasNpm = await which('npm')
if (!hasNpm) return 0
await run('npm', ['ci'])
return run('npm', ['run', 'build'])
}
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()
}
process.exit(code)
}
main().catch(() => process.exit(1))

View File

@@ -1,24 +1,31 @@
[
{
"_note": "Microsoft allows up to 3 new accounts per IP per day; in practice it is recommended not to exceed around 5 active accounts per household IP to avoid looking suspicious; there is no official lifetime cap, but creating too many accounts quickly may trigger verification (phone, OTP, captcha); Microsoft discourages bulk account creation from the same IP; unusual activity can result in temporary blocks or account restrictions.",
"accounts": [
{
"email": "email_1",
"password": "password_1",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
"email": "email_1",
"password": "password_1",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
},
{
"email": "email_2",
"password": "password_2",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
"email": "email_2",
"password": "password_2",
"totp": "",
"recoveryEmail": "your_email@domain.com",
"proxy": {
"proxyAxios": true,
"url": "",
"port": 0,
"username": "",
"password": ""
}
}
]
]
}

View File

@@ -29,16 +29,25 @@ class Browser {
if (process.env.AUTO_INSTALL_BROWSERS === '1') {
try {
// Dynamically import child_process to avoid overhead otherwise
const { execSync } = await import('child_process') as any
const { execSync } = await import('child_process')
execSync('npx playwright install chromium', { stdio: 'ignore' })
} catch { /* silent */ }
}
let browser: any
let browser: import('rebrowser-playwright').Browser
// Support both legacy and new config structures (wider scope for later usage)
const cfgAny = this.bot.config as unknown as Record<string, unknown>
try {
// FORCE_HEADLESS env takes precedence (used in Docker with headless shell only)
const envForceHeadless = process.env.FORCE_HEADLESS === '1'
const headlessValue = envForceHeadless ? true : ((cfgAny['headless'] as boolean | undefined) ?? (cfgAny['browser'] && (cfgAny['browser'] as Record<string, unknown>)['headless'] as boolean | undefined) ?? false)
const headless: boolean = Boolean(headlessValue)
const engineName = 'chromium' // current hard-coded engine
this.bot.log(this.bot.isMobile, 'BROWSER', `Launching ${engineName} (headless=${headless})`) // explicit engine log
browser = await playwright.chromium.launch({
//channel: 'msedge', // Uses Edge instead of chrome
headless: this.bot.config.headless,
headless,
...(proxy.url && { proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` } }),
args: [
'--no-sandbox',
@@ -49,7 +58,7 @@ class Browser {
'--ignore-ssl-errors'
]
})
} catch (e: any) {
} catch (e: unknown) {
const msg = (e instanceof Error ? e.message : String(e))
// Common missing browser executable guidance
if (/Executable doesn't exist/i.test(msg)) {
@@ -60,18 +69,57 @@ class Browser {
throw e
}
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, this.bot.config.saveFingerprint)
// Resolve saveFingerprint from legacy root or new fingerprinting.saveFingerprint
const fpConfig = (cfgAny['saveFingerprint'] as unknown) || ((cfgAny['fingerprinting'] as Record<string, unknown> | undefined)?.['saveFingerprint'] as unknown)
const saveFingerprint: { mobile: boolean; desktop: boolean } = (fpConfig as { mobile: boolean; desktop: boolean }) || { mobile: false, desktop: false }
const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, saveFingerprint)
const fingerprint = sessionData.fingerprint ? sessionData.fingerprint : await this.generateFingerprint()
const context = await newInjectedContext(browser as any, { fingerprint: fingerprint })
const context = await newInjectedContext(browser as unknown as import('playwright').Browser, { fingerprint: fingerprint })
// Set timeout to preferred amount
context.setDefaultTimeout(this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? 30000))
// Set timeout to preferred amount (supports legacy globalTimeout or browser.globalTimeout)
const globalTimeout = (cfgAny['globalTimeout'] as unknown) ?? ((cfgAny['browser'] as Record<string, unknown> | undefined)?.['globalTimeout'] as unknown) ?? 30000
context.setDefaultTimeout(this.bot.utils.stringToMs(globalTimeout as (number | string)))
// Normalize viewport and page rendering so content fits typical screens
try {
const desktopViewport = { width: 1280, height: 800 }
const mobileViewport = { width: 390, height: 844 }
context.on('page', async (page) => {
try {
// Set a reasonable viewport size depending on device type
if (this.bot.isMobile) {
await page.setViewportSize(mobileViewport)
} else {
await page.setViewportSize(desktopViewport)
}
// Inject a tiny CSS to avoid gigantic scaling on some environments
await page.addInitScript(() => {
try {
const style = document.createElement('style')
style.id = '__mrs_fit_style'
style.textContent = `
html, body { overscroll-behavior: contain; }
/* Mild downscale to keep content within window on very large DPI */
@media (min-width: 1000px) {
html { zoom: 0.9 !important; }
}
`
document.documentElement.appendChild(style)
} catch { /* ignore */ }
})
} catch { /* ignore */ }
})
} catch { /* ignore */ }
await context.addCookies(sessionData.cookies)
if (this.bot.config.saveFingerprint) {
// Persist fingerprint when feature is configured
if (fpConfig) {
await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerprint)
}

View File

@@ -40,10 +40,17 @@ export default class BrowserFunc {
await this.bot.utils.wait(3000)
await this.bot.browser.utils.tryDismissAllMessages(page)
// Check if account is suspended
const isSuspended = await page.waitForSelector('#suspendedAccountHeader', { state: 'visible', timeout: 2000 }).then(() => true).catch(() => false)
if (isSuspended) {
this.bot.log(this.bot.isMobile, 'GO-HOME', 'This account is suspended!', 'error')
// Check if account is suspended (multiple heuristics)
const suspendedByHeader = await page.waitForSelector('#suspendedAccountHeader', { state: 'visible', timeout: 1500 }).then(() => true).catch(() => false)
let suspendedByText = false
if (!suspendedByHeader) {
try {
const text = (await page.textContent('body')) || ''
suspendedByText = /account has been suspended|suspended due to unusual activity/i.test(text)
} catch { /* ignore */ }
}
if (suspendedByHeader || suspendedByText) {
this.bot.log(this.bot.isMobile, 'GO-HOME', 'This account appears suspended!', 'error')
throw new Error('Account has been suspended!')
}
@@ -82,21 +89,22 @@ export default class BrowserFunc {
* Fetch user dashboard data
* @returns {DashboardData} Object of user bing rewards dashboard data
*/
async getDashboardData(): Promise<DashboardData> {
async getDashboardData(page?: Page): Promise<DashboardData> {
const target = page ?? this.bot.homePage
const dashboardURL = new URL(this.bot.config.baseURL)
const currentURL = new URL(this.bot.homePage.url())
const currentURL = new URL(target.url())
try {
// Should never happen since tasks are opened in a new tab!
if (currentURL.hostname !== dashboardURL.hostname) {
this.bot.log(this.bot.isMobile, 'DASHBOARD-DATA', 'Provided page did not equal dashboard page, redirecting to dashboard page')
await this.goHome(this.bot.homePage)
await this.goHome(target)
}
let lastError: any = null
let lastError: unknown = null
for (let attempt = 1; attempt <= 2; attempt++) {
try {
// Reload the page to get new data
await this.bot.homePage.reload({ waitUntil: 'domcontentloaded' })
await target.reload({ waitUntil: 'domcontentloaded' })
lastError = null
break
} catch (re) {
@@ -108,7 +116,7 @@ export default class BrowserFunc {
if (attempt === 1) {
this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Page appears closed; trying one navigation fallback', 'warn')
try {
await this.goHome(this.bot.homePage)
await this.goHome(target)
} catch {/* ignore */}
} else {
break
@@ -119,7 +127,7 @@ export default class BrowserFunc {
}
}
const scriptContent = await this.bot.homePage.evaluate(() => {
const scriptContent = await target.evaluate(() => {
const scripts = Array.from(document.querySelectorAll('script'))
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
@@ -131,7 +139,7 @@ export default class BrowserFunc {
}
// Extract the dashboard object from the script content
const dashboardData = await this.bot.homePage.evaluate((scriptContent: string) => {
const dashboardData = await target.evaluate((scriptContent: string) => {
// Extract the dashboard object using regex
const regex = /var dashboard = (\{.*?\});/s
const match = regex.exec(scriptContent)
@@ -232,8 +240,12 @@ export default class BrowserFunc {
]
const data = await this.getDashboardData()
let geoLocale = data.userProfile.attributes.country
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
// Guard against missing profile/attributes and undefined settings
let geoLocale = data?.userProfile?.attributes?.country || 'US'
const useGeo = !!(this.bot?.config?.searchSettings?.useGeoLocaleQueries)
geoLocale = (useGeo && typeof geoLocale === 'string' && geoLocale.length === 2)
? geoLocale.toLowerCase()
: 'us'
const userDataRequest: AxiosRequestConfig = {
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me?channel=SAAndroid&options=613',
@@ -295,9 +307,10 @@ export default class BrowserFunc {
const html = await page.content()
const $ = load(html)
const scriptContent = $('script').filter((index: number, element: any) => {
return $(element).text().includes('_w.rewardsQuizRenderInfo')
}).text()
const scriptContent = $('script')
.toArray()
.map(el => $(el).text())
.find(t => t.includes('_w.rewardsQuizRenderInfo')) || ''
if (scriptContent) {
const regex = /_w\.rewardsQuizRenderInfo\s*=\s*({.*?});/s
@@ -355,7 +368,10 @@ export default class BrowserFunc {
const html = await page.content()
const $ = load(html)
const element = $('.offer-cta').toArray().find((x: any) => x.attribs.href?.includes(activity.offerId))
const element = $('.offer-cta').toArray().find((x: unknown) => {
const el = x as { attribs?: { href?: string } }
return !!el.attribs?.href?.includes(activity.offerId)
})
if (element) {
selector = `a[href*="${element.attribs.href}"]`
}

View File

@@ -12,52 +12,57 @@ export default class BrowserUtil {
}
async tryDismissAllMessages(page: Page): Promise<void> {
const buttons = [
const attempts = 3
const buttonGroups: { selector: string; label: string; isXPath?: boolean }[] = [
{ selector: '#acceptButton', label: 'AcceptButton' },
{ selector: '.ext-secondary.ext-button', label: '"Skip for now" Button' },
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' },
{ selector: '#iShowSkip', label: 'iShowSkip' },
{ selector: '#iNext', label: 'iNext' },
{ selector: '#iLooksGood', label: 'iLooksGood' },
{ selector: '#idSIButton9', label: 'idSIButton9' },
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Button' },
{ selector: '.c-glyph.glyph-cancel', label: 'Mobile Welcome Button' },
{ selector: '.maybe-later', label: 'Mobile Rewards App Banner' },
{ selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Accept Cookie Consent Container', isXPath: true },
{ selector: '#bnp_btn_accept', label: 'Bing Cookie Banner' },
{ selector: '#reward_pivot_earn', label: 'Reward Coupon Accept' }
{ selector: '.optanon-allow-all, .optanon-alert-box-button', label: 'OneTrust Accept' },
{ selector: '.ext-secondary.ext-button', label: 'Skip For Now' },
{ selector: '#iLandingViewAction', label: 'Landing Continue' },
{ selector: '#iShowSkip', label: 'Show Skip' },
{ selector: '#iNext', label: 'Next' },
{ selector: '#iLooksGood', label: 'LooksGood' },
{ selector: '#idSIButton9', label: 'PrimaryLoginButton' },
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Generic' },
{ selector: '.c-glyph.glyph-cancel', label: 'Mobile Welcome Cancel' },
{ selector: '.maybe-later, button[data-automation-id*="maybeLater" i]', label: 'Maybe Later' },
{ selector: '#bnp_btn_reject', label: 'Bing Cookie Reject' },
{ selector: '#bnp_btn_accept', label: 'Bing Cookie Accept' },
{ selector: '#bnp_close_link', label: 'Bing Cookie Close' },
{ selector: '#reward_pivot_earn', label: 'Rewards Pivot Earn' },
{ selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Legacy Cookie Accept', isXPath: true }
]
for (const button of buttons) {
for (let round = 0; round < attempts; round++) {
let dismissedThisRound = 0
for (const btn of buttonGroups) {
try {
const loc = btn.isXPath ? page.locator(`xpath=${btn.selector}`) : page.locator(btn.selector)
if (await loc.first().isVisible({ timeout: 200 }).catch(()=>false)) {
await loc.first().click({ timeout: 500 }).catch(()=>{})
dismissedThisRound++
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`)
await page.waitForTimeout(150)
}
} catch { /* ignore */ }
}
// Special case: blocking overlay with inside buttons
try {
const element = button.isXPath ? page.locator(`xpath=${button.selector}`) : page.locator(button.selector)
await element.first().click({ timeout: 500 })
await page.waitForTimeout(500)
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${button.label}`)
} catch (error) {
// Silent fail
}
}
// Handle blocking Bing privacy overlay intercepting clicks (#bnp_overlay_wrapper)
try {
const overlay = await page.locator('#bnp_overlay_wrapper').first()
if (await overlay.isVisible({ timeout: 500 }).catch(()=>false)) {
// Try common dismiss buttons inside overlay
const rejectBtn = await page.locator('#bnp_btn_reject, button[aria-label*="Reject" i]').first()
const acceptBtn = await page.locator('#bnp_btn_accept').first()
if (await rejectBtn.isVisible().catch(()=>false)) {
await rejectBtn.click({ timeout: 500 }).catch(()=>{})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Bing Overlay Reject')
} else if (await acceptBtn.isVisible().catch(()=>false)) {
await acceptBtn.click({ timeout: 500 }).catch(()=>{})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Bing Overlay Accept (fallback)')
const overlay = page.locator('#bnp_overlay_wrapper')
if (await overlay.isVisible({ timeout: 200 }).catch(()=>false)) {
const reject = overlay.locator('#bnp_btn_reject, button[aria-label*="Reject" i]')
const accept = overlay.locator('#bnp_btn_accept')
if (await reject.first().isVisible().catch(()=>false)) {
await reject.first().click({ timeout: 500 }).catch(()=>{})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Reject')
dismissedThisRound++
} else if (await accept.first().isVisible().catch(()=>false)) {
await accept.first().click({ timeout: 500 }).catch(()=>{})
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Accept')
dismissedThisRound++
}
}
await page.waitForTimeout(300)
}
} catch { /* ignore */ }
} catch { /* ignore */ }
if (dismissedThisRound === 0) break // nothing new dismissed -> stop early
}
}
async getLatestTab(page: Page): Promise<Page> {
@@ -78,40 +83,6 @@ export default class BrowserUtil {
}
}
async getTabs(page: Page) {
try {
const browser = page.context()
const pages = browser.pages()
const homeTab = pages[1]
let homeTabURL: URL
if (!homeTab) {
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Home tab could not be found!', 'error')
} else {
homeTabURL = new URL(homeTab.url())
if (homeTabURL.hostname !== 'rewards.bing.com') {
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Reward page hostname is invalid: ' + homeTabURL.host, 'error')
}
}
const workerTab = pages[2]
if (!workerTab) {
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'Worker tab could not be found!', 'error')
}
return {
homeTab: homeTab,
workerTab: workerTab
}
} catch (error) {
throw this.bot.log(this.bot.isMobile, 'GET-TABS', 'An error occurred:' + error, 'error')
}
}
async reloadBadPage(page: Page): Promise<void> {
try {
const html = await page.content().catch(() => '')
@@ -129,4 +100,80 @@ export default class BrowserUtil {
}
}
/**
* Perform small human-like gestures: short waits, minor mouse moves and occasional scrolls.
* This should be called sparingly between actions to avoid a fixed cadence.
*/
async humanizePage(page: Page): Promise<void> {
try {
const h = this.bot.config?.humanization || {}
if (h.enabled === false) return
const moveProb = typeof h.gestureMoveProb === 'number' ? h.gestureMoveProb : 0.4
const scrollProb = typeof h.gestureScrollProb === 'number' ? h.gestureScrollProb : 0.2
// minor mouse move
if (Math.random() < moveProb) {
const x = Math.floor(Math.random() * 30) + 5
const y = Math.floor(Math.random() * 20) + 3
await page.mouse.move(x, y, { steps: 2 }).catch(() => { })
}
// tiny scroll
if (Math.random() < scrollProb) {
const dy = (Math.random() < 0.5 ? 1 : -1) * (Math.floor(Math.random() * 150) + 50)
await page.mouse.wheel(0, dy).catch(() => { })
}
// Random short wait; override via humanization.actionDelay
const range = h.actionDelay
if (range && typeof range.min !== 'undefined' && typeof range.max !== 'undefined') {
try {
const ms = (await import('ms')).default
const min = typeof range.min === 'number' ? range.min : ms(String(range.min))
const max = typeof range.max === 'number' ? range.max : ms(String(range.max))
if (typeof min === 'number' && typeof max === 'number' && max >= min) {
await this.bot.utils.wait(this.bot.utils.randomNumber(Math.max(0, min), Math.min(max, 5000)))
} else {
await this.bot.utils.wait(this.bot.utils.randomNumber(150, 450))
}
} catch {
await this.bot.utils.wait(this.bot.utils.randomNumber(150, 450))
}
} else {
await this.bot.utils.wait(this.bot.utils.randomNumber(150, 450))
}
} catch { /* swallow */ }
}
/**
* Capture minimal diagnostics for a page: screenshot + HTML content.
* Files are written under ./reports/<date>/ with a safe label.
*/
async captureDiagnostics(page: Page, label: string): Promise<void> {
try {
const cfg = this.bot.config?.diagnostics || {}
if (cfg.enabled === false) return
const maxPerRun = typeof cfg.maxPerRun === 'number' ? cfg.maxPerRun : 8
if (!this.bot.tryReserveDiagSlot(maxPerRun)) return
const safe = label.replace(/[^a-z0-9-_]/gi, '_').slice(0, 64)
const now = new Date()
const day = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`
const baseDir = `${process.cwd()}/reports/${day}`
const fs = await import('fs')
const path = await import('path')
if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true })
const ts = `${String(now.getHours()).padStart(2,'0')}${String(now.getMinutes()).padStart(2,'0')}${String(now.getSeconds()).padStart(2,'0')}`
const shot = path.join(baseDir, `${ts}_${safe}.png`)
const htmlPath = path.join(baseDir, `${ts}_${safe}.html`)
if (cfg.saveScreenshot !== false) {
await page.screenshot({ path: shot }).catch(()=>{})
}
if (cfg.saveHtml !== false) {
const html = await page.content().catch(()=> '<html></html>')
fs.writeFileSync(htmlPath, html)
}
this.bot.log(this.bot.isMobile, 'DIAG', `Saved diagnostics to ${shot} and ${htmlPath}`)
} catch (e) {
this.bot.log(this.bot.isMobile, 'DIAG', `Failed to capture diagnostics: ${e instanceof Error ? e.message : e}`, 'warn')
}
}
}

View File

@@ -1,51 +1,197 @@
{
// Base URL for Rewards dashboard and APIs (do not change unless you know what you're doing)
"baseURL": "https://rewards.bing.com",
// Where to store sessions (cookies, fingerprints)
"sessionPath": "sessions",
"headless": false,
"parallel": false,
"runOnZeroPoints": false,
"clusters": 1,
"saveFingerprint": {
"mobile": false,
"desktop": false
"browser": {
// Run browser without UI (true=headless, false=visible). Visible can help with stability.
"headless": false,
// Max time to wait for common operations (supports ms/s/min: e.g. 30000, "30s", "2min")
"globalTimeout": "30s"
},
"execution": {
// Run desktop+mobile in parallel (needs more resources). If false, runs sequentially.
"parallel": false,
// If false and there are 0 points available, the run is skipped early to save time.
"runOnZeroPoints": false,
// Number of account clusters (processes) to run concurrently.
"clusters": 1,
// Number of passes per invocation (advanced; usually 1).
"passesPerRun": 1
},
"buyMode": {
// Manual purchase/redeem mode. Use CLI -buy to enable, or set buyMode.enabled in config.
// Session duration cap in minutes.
"maxMinutes": 45
},
"fingerprinting": {
// Persist browser fingerprints per device type to improve consistency across runs
"saveFingerprint": {
"mobile": false,
"desktop": false
}
},
"search": {
// Use locale-specific query sources
"useLocalQueries": false,
"settings": {
// Add geo/locale signal into query selection
"useGeoLocaleQueries": false,
// Randomly scroll search result pages to look more natural
"scrollRandomResults": true,
// Occasionally click a result (safe targets only)
"clickRandomResults": true,
// Number of times to retry mobile searches if points didnt progress
"retryMobileSearchAmount": 2,
// Delay between searches (supports numbers in ms or time strings)
"delay": {
"min": "3min",
"max": "5min"
}
}
},
"humanization": {
// Global Human Mode switch. true=adds subtle micro-gestures & pauses. false=classic behavior.
"enabled": true,
// If true, as soon as a ban is detected on any account, stop processing remaining accounts
// (ban detection is based on centralized heuristics and error signals)
"stopOnBan": true,
// If true, immediately send an alert (webhook/NTFY) when a ban is detected
"immediateBanAlert": true,
// Extra random pause between actions (ms or time string e.g., "300ms", "1s")
"actionDelay": {
"min": 150,
"max": 450
},
// Probability (0..1) to move mouse a tiny bit in between actions
"gestureMoveProb": 0.4,
// Probability (0..1) to perform a very small scroll
"gestureScrollProb": 0.2,
// Optional local-time windows for execution (e.g., ["08:30-11:00", "19:00-22:00"]).
// If provided, runs will wait until inside a window before starting.
"allowedWindows": []
},
// Optional monthly "vacation" block: skip a contiguous range of days to look more human.
// This is independent of weekly random off-days. When enabled, each month a random
// block between minDays and maxDays is selected (e.g., 35 days) and all runs within
// that date range are skipped. The chosen block is logged at the start of the month.
"vacation": {
"enabled": false,
"minDays": 3,
"maxDays": 5
},
"retryPolicy": {
// Generic retry/backoff for transient failures
"maxAttempts": 3,
"baseDelay": 1000,
"maxDelay": "30s",
"multiplier": 2,
"jitter": 0.2
},
"workers": {
// Select what the bot should complete on desktop/mobile
"doDailySet": true,
"doMorePromotions": true,
"doPunchCards": true,
"doDesktopSearch": true,
"doMobileSearch": true,
"doDailyCheckIn": true,
"doReadToEarn": true
"doReadToEarn": true,
// If true, run a desktop search bundle right after Daily Set
"bundleDailySetWithSearch": false
},
"searchOnBingLocalQueries": false,
"globalTimeout": "30s",
"searchSettings": {
"useGeoLocaleQueries": false,
"scrollRandomResults": true,
"clickRandomResults": true,
"searchDelay": {
"min": "3min",
"max": "5min"
},
"retryMobileSearchAmount": 2
},
"logExcludeFunc": [
"SEARCH-CLOSE-TABS"
],
"webhookLogExcludeFunc": [
"SEARCH-CLOSE-TABS"
],
"proxy": {
// Control which outbound calls go through your proxy
"proxyGoogleTrends": true,
"proxyBingTerms": true
},
"webhook": {
"enabled": false,
"url": ""
"notifications": {
// Live logs (Discord or similar). URL is your webhook endpoint.
"webhook": {
"enabled": false,
"url": ""
},
// Rich end-of-run summary (Discord or similar)
"conclusionWebhook": {
"enabled": false,
"url": ""
},
// NTFY push notifications (plain text)
"ntfy": {
"enabled": false,
"url": "",
"topic": "rewards",
"authToken": ""
}
},
"conclusionWebhook": {
"logging": {
// Logging controls (see docs/config.md). Remove redactEmails or set false to show full emails.
// Filter out noisy log buckets locally and for any webhook summaries
"excludeFunc": [
"SEARCH-CLOSE-TABS",
"LOGIN-NO-PROMPT",
"FLOW"
],
"webhookExcludeFunc": [
"SEARCH-CLOSE-TABS",
"LOGIN-NO-PROMPT",
"FLOW"
],
// Email redaction toggle (previously logging.live.redactEmails)
"redactEmails": true
},
"diagnostics": {
// Capture minimal evidence on failures (screenshots/HTML) and prune old runs
"enabled": true,
"saveScreenshot": true,
"saveHtml": true,
"maxPerRun": 2,
"retentionDays": 7
},
"jobState": {
// Checkpoint to avoid duplicate work across restarts
"enabled": true,
// Custom state directory (defaults to sessionPath/job-state if empty)
"dir": ""
},
"schedule": {
// Built-in scheduler (no cron needed in container). Uses the IANA time zone below.
"enabled": false,
"url": ""
// Choose YOUR preferred time format:
// - US style with AM/PM → set useAmPm: true and edit time12 (e.g., "9:00 AM")
// - 24-hour style → set useAmPm: false and edit time24 (e.g., "09:00")
// Back-compat: if both time12/time24 are empty, the legacy "time" (HH:mm) will be used if present.
"useAmPm": false,
"time12": "9:00 AM",
"time24": "09:00",
// IANA timezone for scheduling (set to your region), e.g. "Europe/Paris" or "America/New_York"
"timeZone": "America/New_York",
// If true, run one pass immediately when the process starts
"runImmediatelyOnStart": false
},
"update": {
// Optional post-run auto-update
"git": true,
"docker": false,
// Custom updater script path (relative to repo root)
"scriptPath": "setup/update/update.mjs"
}
}

View File

@@ -1,2 +0,0 @@
# Run automation according to CRON_SCHEDULE; redirect both stdout & stderr to Docker logs
${CRON_SCHEDULE} TZ=${TZ} /bin/bash /usr/src/microsoft-rewards-script/src/run_daily.sh >> /proc/1/fd/1 2>&1

View File

@@ -13,15 +13,109 @@ import { ReadToEarn } from './activities/ReadToEarn'
import { DailyCheckIn } from './activities/DailyCheckIn'
import { DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData'
import type { ActivityHandler } from '../interface/ActivityHandler'
type ActivityKind =
| { type: 'poll' }
| { type: 'abc' }
| { type: 'thisOrThat' }
| { type: 'quiz' }
| { type: 'urlReward' }
| { type: 'searchOnBing' }
| { type: 'unsupported' }
export default class Activities {
private bot: MicrosoftRewardsBot
private handlers: ActivityHandler[] = []
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
// Register external/custom handlers (optional extension point)
registerHandler(handler: ActivityHandler) {
this.handlers.push(handler)
}
// Centralized dispatcher for activities from dashboard/punchcards
async run(page: Page, activity: MorePromotion | PromotionalItem): Promise<void> {
// First, try custom handlers (if any)
for (const h of this.handlers) {
try {
if (h.canHandle(activity)) {
await h.run(page, activity)
return
}
} catch (e) {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Custom handler ${(h.id || 'unknown')} failed: ${e instanceof Error ? e.message : e}`, 'error')
}
}
const kind = this.classifyActivity(activity)
try {
switch (kind.type) {
case 'poll':
await this.doPoll(page)
break
case 'abc':
await this.doABC(page)
break
case 'thisOrThat':
await this.doThisOrThat(page)
break
case 'quiz':
await this.doQuiz(page)
break
case 'searchOnBing':
await this.doSearchOnBing(page, activity)
break
case 'urlReward':
await this.doUrlReward(page)
break
default:
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${String((activity as { promotionType?: string }).promotionType)}"!`, 'warn')
break
}
} catch (e) {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Dispatcher error for "${activity.title}": ${e instanceof Error ? e.message : e}`, 'error')
}
}
public getTypeLabel(activity: MorePromotion | PromotionalItem): string {
const k = this.classifyActivity(activity)
switch (k.type) {
case 'poll': return 'Poll'
case 'abc': return 'ABC'
case 'thisOrThat': return 'ThisOrThat'
case 'quiz': return 'Quiz'
case 'searchOnBing': return 'SearchOnBing'
case 'urlReward': return 'UrlReward'
default: return 'Unsupported'
}
}
private classifyActivity(activity: MorePromotion | PromotionalItem): ActivityKind {
const type = (activity.promotionType || '').toLowerCase()
if (type === 'quiz') {
// Distinguish Poll/ABC/ThisOrThat vs general quiz using current heuristics
const max = activity.pointProgressMax
const url = (activity.destinationUrl || '').toLowerCase()
if (max === 10) {
if (url.includes('pollscenarioid')) return { type: 'poll' }
return { type: 'abc' }
}
if (max === 50) return { type: 'thisOrThat' }
return { type: 'quiz' }
}
if (type === 'urlreward') {
const name = (activity.name || '').toLowerCase()
if (name.includes('exploreonbing')) return { type: 'searchOnBing' }
return { type: 'urlReward' }
}
return { type: 'unsupported' }
}
doSearch = async (page: Page, data: DashboardData): Promise<void> => {
const search = new Search(this.bot)
await search.doSearch(page, data)

File diff suppressed because it is too large Load Diff

View File

@@ -3,19 +3,30 @@ import { Page } from 'rebrowser-playwright'
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
import { MicrosoftRewardsBot } from '../index'
import JobState from '../util/JobState'
import Retry from '../util/Retry'
import { AdaptiveThrottler } from '../util/AdaptiveThrottler'
export class Workers {
public bot: MicrosoftRewardsBot
private jobState: JobState
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
this.jobState = new JobState(this.bot.config)
}
// Daily Set
async doDailySet(page: Page, data: DashboardData) {
const todayData = data.dailySetPromotions[this.bot.utils.getFormattedDate()]
const activitiesUncompleted = todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? []
const today = this.bot.utils.getFormattedDate()
const activitiesUncompleted = (todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [])
.filter(x => {
if (this.bot.config.jobState?.enabled === false) return true
const email = this.bot.currentAccountEmail || 'unknown'
return !this.jobState.isDone(email, today, x.offerId)
})
if (!activitiesUncompleted.length) {
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All Daily Set" items have already been completed')
@@ -27,12 +38,30 @@ export class Workers {
await this.solveActivities(page, activitiesUncompleted)
// Mark as done to prevent duplicate work if checkpoints enabled
if (this.bot.config.jobState?.enabled !== false) {
const email = this.bot.currentAccountEmail || 'unknown'
for (const a of activitiesUncompleted) {
this.jobState.markDone(email, today, a.offerId)
}
}
page = await this.bot.browser.utils.getLatestTab(page)
// Always return to the homepage if not already
await this.bot.browser.func.goHome(page)
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All "Daily Set" items have been completed')
// Optional: immediately run desktop search bundle
if (!this.bot.isMobile && this.bot.config.workers.bundleDailySetWithSearch && this.bot.config.workers.doDesktopSearch) {
try {
await this.bot.utils.waitRandom(1200, 2600)
await this.bot.activities.doSearch(page, data)
} catch (e) {
this.bot.log(this.bot.isMobile, 'DAILY-SET', `Post-DailySet search failed: ${e instanceof Error ? e.message : e}`, 'warn')
}
}
}
// Punch Card
@@ -120,7 +149,9 @@ export class Workers {
private async solveActivities(activityPage: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
const activityInitial = activityPage.url() // Homepage for Daily/More and Index for promotions
for (const activity of activities) {
const retry = new Retry(this.bot.config.retryPolicy)
const throttle = new AdaptiveThrottler()
for (const activity of activities) {
try {
// Reselect the worker page
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
@@ -132,7 +163,11 @@ export class Workers {
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
}
await this.bot.utils.wait(1000)
await this.bot.browser.utils.humanizePage(activityPage)
{
const m = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(800*m), Math.floor(1400*m))
}
if (activityPage.url() !== activityInitial) {
await activityPage.goto(activityInitial)
@@ -154,74 +189,50 @@ export class Workers {
if it didn't then it gave enough time for the page to load.
*/
await activityPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { })
await this.bot.utils.wait(2000)
switch (activity.promotionType) {
// Quiz (Poll, Quiz or ABC)
case 'quiz':
switch (activity.pointProgressMax) {
// Poll or ABC (Usually 10 points)
case 10:
// Normal poll
if (activity.destinationUrl.toLowerCase().includes('pollscenarioid')) {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "Poll" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doPoll(activityPage)
} else { // ABC
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "ABC" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doABC(activityPage)
}
break
// This Or That Quiz (Usually 50 points)
case 50:
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "ThisOrThat" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doThisOrThat(activityPage)
break
// Quizzes are usually 30-40 points
default:
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "Quiz" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doQuiz(activityPage)
break
}
break
// UrlReward (Visit)
case 'urlreward':
// Search on Bing are subtypes of "urlreward"
if (activity.name.toLowerCase().includes('exploreonbing')) {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "SearchOnBing" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doSearchOnBing(activityPage, activity)
} else {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "UrlReward" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
await this.bot.activities.doUrlReward(activityPage)
}
break
// Unsupported types
default:
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${activity.promotionType}"!`, 'warn')
break
// Small human-like jitter before executing
await this.bot.browser.utils.humanizePage(activityPage)
{
const m = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(1200*m), Math.floor(2600*m))
}
// Cooldown
await this.bot.utils.wait(2000)
// Log the detected type using the same heuristics as before
const typeLabel = this.bot.activities.getTypeLabel(activity)
if (typeLabel !== 'Unsupported') {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${typeLabel}" title: "${activity.title}"`)
await activityPage.click(selector)
activityPage = await this.bot.browser.utils.getLatestTab(activityPage)
// Watchdog: abort if the activity hangs too long
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
const runWithTimeout = (p: Promise<void>) => Promise.race([
p,
new Promise<void>((_, rej) => setTimeout(() => rej(new Error('activity-timeout')), timeoutMs))
])
await retry.run(async () => {
try {
await runWithTimeout(this.bot.activities.run(activityPage, activity))
throttle.record(true)
} catch (e) {
await this.bot.browser.utils.captureDiagnostics(activityPage, `activity_timeout_${activity.title || activity.offerId}`)
throttle.record(false)
throw e
}
}, () => true)
} else {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${activity.promotionType}"!`, 'warn')
}
// Cooldown with jitter
await this.bot.browser.utils.humanizePage(activityPage)
{
const m = throttle.getDelayMultiplier()
await this.bot.utils.waitRandom(Math.floor(1200*m), Math.floor(2600*m))
}
} catch (error) {
await this.bot.browser.utils.captureDiagnostics(activityPage, `activity_error_${activity.title || activity.offerId}`)
this.bot.log(this.bot.isMobile, 'ACTIVITY', 'An error occurred:' + error, 'error')
throttle.record(false)
}
}

View File

@@ -30,7 +30,7 @@ export class Quiz extends Workers {
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const answerAttribute = await answerSelector?.evaluate((el: any) => el.getAttribute('iscorrectoption'))
const answerAttribute = await answerSelector?.evaluate((el: Element) => el.getAttribute('iscorrectoption'))
if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
answers.push(`#rqAnswerOption${i}`)
@@ -60,7 +60,7 @@ export class Quiz extends Workers {
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const dataOption = await answerSelector?.evaluate((el: any) => el.getAttribute('data-option'))
const dataOption = await answerSelector?.evaluate((el: Element) => el.getAttribute('data-option'))
if (dataOption === correctOption) {
// Click the answer on page
@@ -84,6 +84,7 @@ export class Quiz extends Workers {
this.bot.log(this.bot.isMobile, 'QUIZ', 'Completed the quiz successfully')
} catch (error) {
await this.bot.browser.utils.captureDiagnostics(page, 'quiz_error')
await page.close()
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred:' + error, 'error')
}

View File

@@ -33,12 +33,32 @@ export class Search extends Workers {
return
}
// Generate search queries
let googleSearchQueries = await this.getGoogleTrends(this.bot.config.searchSettings.useGeoLocaleQueries ? data.userProfile.attributes.country : 'US')
googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries)
// Generate search queries (primary: Google Trends)
const geo = this.bot.config.searchSettings.useGeoLocaleQueries ? data.userProfile.attributes.country : 'US'
let googleSearchQueries = await this.getGoogleTrends(geo)
// Deduplicate the search terms
googleSearchQueries = [...new Set(googleSearchQueries)]
// Fallback: if trends failed or insufficient, sample from local queries file
if (!googleSearchQueries.length || googleSearchQueries.length < 10) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Primary trends source insufficient, falling back to local queries.json', 'warn')
try {
const local = await import('../queries.json')
// Flatten & sample
const sampleSize = Math.max(5, Math.min(this.bot.config.searchSettings.localFallbackCount || 25, local.default.length))
const sampled = this.bot.utils.shuffleArray(local.default).slice(0, sampleSize)
googleSearchQueries = sampled.map((x: { title: string; queries: string[] }) => ({ topic: x.queries[0] || x.title, related: x.queries.slice(1) }))
} catch (e) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Failed loading local queries fallback: ' + (e instanceof Error ? e.message : e), 'error')
}
}
googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries)
// Deduplicate topics
const seen = new Set<string>()
googleSearchQueries = googleSearchQueries.filter(q => {
if (seen.has(q.topic.toLowerCase())) return false
seen.add(q.topic.toLowerCase())
return true
})
// Go to bing
await page.goto(this.searchPageURL ? this.searchPageURL : this.bingHome)
@@ -47,7 +67,7 @@ export class Search extends Workers {
await this.bot.browser.utils.tryDismissAllMessages(page)
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it doesn't continue after 5 more searches with alternative queries, abort search
let stagnation = 0 // consecutive searches without point progress
const queries: string[] = []
// Mobile search doesn't seem to like related queries?
@@ -63,28 +83,26 @@ export class Search extends Workers {
const newMissingPoints = this.calculatePoints(searchCounters)
// If the new point amount is the same as before
if (newMissingPoints == missingPoints) {
maxLoop++ // Add to max loop
} else { // There has been a change in points
maxLoop = 0 // Reset the loop
if (newMissingPoints === missingPoints) {
stagnation++
} else {
stagnation = 0
}
missingPoints = newMissingPoints
if (missingPoints === 0) {
break
}
if (missingPoints === 0) break
// Only for mobile searches
if (maxLoop > 5 && this.bot.isMobile) {
if (stagnation > 5 && this.bot.isMobile) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 5 iterations, likely bad User-Agent', 'warn')
break
}
// If we didn't gain points for 10 iterations, assume it's stuck
if (maxLoop > 10) {
if (stagnation > 10) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn')
maxLoop = 0 // Reset to 0 so we can retry with related searches below
stagnation = 0 // allow fallback loop below
break
}
}
@@ -99,8 +117,11 @@ export class Search extends Workers {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`)
let i = 0
while (missingPoints > 0) {
let fallbackRounds = 0
const extraRetries = this.bot.config.searchSettings.extraFallbackRetries || 1
while (missingPoints > 0 && fallbackRounds <= extraRetries) {
const query = googleSearchQueries[i++] as GoogleSearch
if (!query) break
// Get related search terms to the Google search queries
const relatedTerms = await this.getRelatedTerms(query?.topic)
@@ -113,10 +134,10 @@ export class Search extends Workers {
const newMissingPoints = this.calculatePoints(searchCounters)
// If the new point amount is the same as before
if (newMissingPoints == missingPoints) {
maxLoop++ // Add to max loop
} else { // There has been a change in points
maxLoop = 0 // Reset the loop
if (newMissingPoints === missingPoints) {
stagnation++
} else {
stagnation = 0
}
missingPoints = newMissingPoints
@@ -127,11 +148,12 @@ export class Search extends Workers {
}
// Try 5 more times, then we tried a total of 15 times, fair to say it's stuck
if (maxLoop > 5) {
if (stagnation > 5) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn')
return
}
}
fallbackRounds++
}
}
}
@@ -156,20 +178,38 @@ export class Search extends Workers {
await this.bot.utils.wait(500)
const searchBar = '#sb_form_q'
await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
await searchPage.click(searchBar) // Focus on the textarea
await this.bot.utils.wait(500)
await searchPage.keyboard.down(platformControlKey)
await searchPage.keyboard.press('A')
await searchPage.keyboard.press('Backspace')
await searchPage.keyboard.up(platformControlKey)
await searchPage.keyboard.type(query)
await searchPage.keyboard.press('Enter')
// Prefer attached over visible to avoid strict visibility waits when overlays exist
const box = searchPage.locator(searchBar)
await box.waitFor({ state: 'attached', timeout: 15000 })
// Try dismissing overlays before interacting
await this.bot.browser.utils.tryDismissAllMessages(searchPage)
await this.bot.utils.wait(200)
let navigatedDirectly = false
try {
// Try focusing and filling instead of clicking (more reliable on mobile)
await box.focus({ timeout: 2000 }).catch(() => { /* ignore focus errors */ })
await box.fill('')
await this.bot.utils.wait(200)
await searchPage.keyboard.down(platformControlKey)
await searchPage.keyboard.press('A')
await searchPage.keyboard.press('Backspace')
await searchPage.keyboard.up(platformControlKey)
await box.type(query, { delay: 20 })
await searchPage.keyboard.press('Enter')
} catch (typeErr) {
// As a robust fallback, navigate directly to the search results URL
const q = encodeURIComponent(query)
const url = `https://www.bing.com/search?q=${q}`
await searchPage.goto(url)
navigatedDirectly = true
}
await this.bot.utils.wait(3000)
// Bing.com in Chrome opens a new tab when searching
const resultPage = await this.bot.browser.utils.getLatestTab(searchPage)
// Bing.com in Chrome opens a new tab when searching via Enter; if we navigated directly, stay on current tab
const resultPage = navigatedDirectly ? searchPage : await this.bot.browser.utils.getLatestTab(searchPage)
this.searchPageURL = new URL(resultPage.url()).href // Set the results page
await this.bot.browser.utils.reloadBadPage(resultPage)
@@ -185,7 +225,10 @@ export class Search extends Workers {
}
// Delay between searches
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.min), this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.max))))
const minDelay = this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.min)
const maxDelay = this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.max)
const adaptivePad = Math.min(4000, Math.max(0, Math.floor(Math.random() * 800)))
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(minDelay, maxDelay)) + adaptivePad)
return await this.bot.browser.func.getSearchPoints()

View File

@@ -20,11 +20,20 @@ export class SearchOnBing extends Workers {
const query = await this.getSearchQuery(activity.title)
const searchBar = '#sb_form_q'
await page.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
await this.safeClick(page, searchBar)
await this.bot.utils.wait(500)
await page.keyboard.type(query)
await page.keyboard.press('Enter')
const box = page.locator(searchBar)
await box.waitFor({ state: 'attached', timeout: 15000 })
await this.bot.browser.utils.tryDismissAllMessages(page)
await this.bot.utils.wait(200)
try {
await box.focus({ timeout: 2000 }).catch(() => { /* ignore */ })
await box.fill('')
await this.bot.utils.wait(200)
await page.keyboard.type(query, { delay: 20 })
await page.keyboard.press('Enter')
} catch {
const url = `https://www.bing.com/search?q=${encodeURIComponent(query)}`
await page.goto(url)
}
await this.bot.utils.wait(3000)
await page.close()
@@ -36,22 +45,6 @@ export class SearchOnBing extends Workers {
}
}
private async safeClick(page: Page, selector: string) {
try {
await page.click(selector, { timeout: 5000 })
} catch (e: any) {
const msg = (e?.message || '')
if (/Timeout.*click/i.test(msg) || /intercepts pointer events/i.test(msg)) {
// Try to dismiss overlays then retry once
await this.bot.browser.utils.tryDismissAllMessages(page)
await this.bot.utils.wait(500)
await page.click(selector, { timeout: 5000 })
} else {
throw e
}
}
}
private async getSearchQuery(title: string): Promise<string> {
interface Queries {
title: string;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,10 @@
export interface Account {
email: string;
password: string;
/** Optional TOTP secret in Base32 (e.g., from Microsoft Authenticator setup) */
totp?: string;
/** Optional recovery email used to verify masked address on Microsoft login screens */
recoveryEmail?: string;
proxy: AccountProxy;
}

View File

@@ -0,0 +1,21 @@
import type { MorePromotion, PromotionalItem } from './DashboardData'
import type { Page } from 'playwright'
/**
* Activity handler contract for solving a single dashboard activity.
* Implementations should be stateless (or hold only a reference to the bot)
* and perform all required steps on the provided page.
*/
export interface ActivityHandler {
/** Optional identifier for diagnostics */
id?: string
/**
* Return true if this handler knows how to process the given activity.
*/
canHandle(activity: MorePromotion | PromotionalItem): boolean
/**
* Execute the activity on the provided page. The page is already
* navigated to the activity tab/window by the caller.
*/
run(page: Page, activity: MorePromotion | PromotionalItem): Promise<void>
}

View File

@@ -10,11 +10,23 @@ export interface Config {
searchOnBingLocalQueries: boolean;
globalTimeout: number | string;
searchSettings: ConfigSearchSettings;
humanization?: ConfigHumanization; // Anti-ban humanization controls
retryPolicy?: ConfigRetryPolicy; // Global retry/backoff policy
jobState?: ConfigJobState; // Persistence of per-activity checkpoints
logExcludeFunc: string[];
webhookLogExcludeFunc: string[];
logging?: ConfigLogging; // Preserve original logging object (for live webhook settings)
proxy: ConfigProxy;
webhook: ConfigWebhook;
conclusionWebhook?: ConfigWebhook; // Optional secondary webhook for final summary
ntfy: ConfigNtfy;
diagnostics?: ConfigDiagnostics;
update?: ConfigUpdate;
schedule?: ConfigSchedule;
passesPerRun?: number;
buyMode?: ConfigBuyMode; // Optional manual spending mode
vacation?: ConfigVacation; // Optional monthly contiguous off-days
crashRecovery?: ConfigCrashRecovery; // Automatic restart / graceful shutdown
}
export interface ConfigSaveFingerprint {
@@ -28,6 +40,8 @@ export interface ConfigSearchSettings {
clickRandomResults: boolean;
searchDelay: ConfigSearchDelay;
retryMobileSearchAmount: number;
localFallbackCount?: number; // Number of local fallback queries to sample when trends fail
extraFallbackRetries?: number; // Additional mini-retry loops with fallback terms
}
export interface ConfigSearchDelay {
@@ -38,6 +52,15 @@ export interface ConfigSearchDelay {
export interface ConfigWebhook {
enabled: boolean;
url: string;
username?: string; // Optional override for displayed webhook name
avatarUrl?: string; // Optional avatar image URL
}
export interface ConfigNtfy {
enabled: boolean;
url: string;
topic: string;
authToken?: string; // Optional authentication token
}
export interface ConfigProxy {
@@ -45,6 +68,50 @@ export interface ConfigProxy {
proxyBingTerms: boolean;
}
export interface ConfigDiagnostics {
enabled?: boolean; // master toggle
saveScreenshot?: boolean; // capture .png
saveHtml?: boolean; // capture .html
maxPerRun?: number; // cap number of captures per run
retentionDays?: number; // delete older diagnostic folders
}
export interface ConfigUpdate {
git?: boolean; // if true, run git pull + npm ci + npm run build after completion
docker?: boolean; // if true, run docker update routine (compose pull/up) after completion
scriptPath?: string; // optional custom path to update script relative to repo root
}
export interface ConfigBuyMode {
enabled?: boolean; // if true, force buy mode session
maxMinutes?: number; // session duration cap
}
export interface ConfigSchedule {
enabled?: boolean;
time?: string; // Back-compat: accepts "HH:mm" or "h:mm AM/PM"
// New optional explicit times
time12?: string; // e.g., "9:00 AM"
time24?: string; // e.g., "09:00"
timeZone?: string; // IANA TZ e.g., "America/New_York"
useAmPm?: boolean; // If true, prefer time12 + AM/PM style; if false, prefer time24. If undefined, back-compat behavior.
runImmediatelyOnStart?: boolean; // if true, run once immediately when process starts
}
export interface ConfigVacation {
enabled?: boolean; // default false
minDays?: number; // default 3
maxDays?: number; // default 5
}
export interface ConfigCrashRecovery {
autoRestart?: boolean; // Restart the root process after fatal crash
maxRestarts?: number; // Max restart attempts (default 2)
backoffBaseMs?: number; // Base backoff before restart (default 2000)
restartFailedWorker?: boolean; // (future) attempt to respawn crashed worker
restartFailedWorkerAttempts?: number; // attempts per worker (default 1)
}
export interface ConfigWorkers {
doDailySet: boolean;
doMorePromotions: boolean;
@@ -53,4 +120,60 @@ export interface ConfigWorkers {
doMobileSearch: boolean;
doDailyCheckIn: boolean;
doReadToEarn: boolean;
bundleDailySetWithSearch?: boolean; // If true, run desktop search right after Daily Set
}
// Anti-ban humanization
export interface ConfigHumanization {
// Master toggle for Human Mode. When false, humanization is minimized.
enabled?: boolean;
// If true, stop processing remaining accounts after a ban is detected
stopOnBan?: boolean;
// If true, send an immediate webhook/NTFY alert when a ban is detected
immediateBanAlert?: boolean;
// Additional random waits between actions
actionDelay?: { min: number | string; max: number | string };
// Probability [0..1] to perform micro mouse moves per step
gestureMoveProb?: number;
// Probability [0..1] to perform tiny scrolls per step
gestureScrollProb?: number;
// Allowed execution windows (local time). Each item is "HH:mm-HH:mm".
// If provided, runs outside these windows will be delayed until the next allowed window.
allowedWindows?: string[];
// Randomly skip N days per week to look more human (0-7). Default 1.
randomOffDaysPerWeek?: number;
}
// Retry/backoff policy
export interface ConfigRetryPolicy {
maxAttempts?: number; // default 3
baseDelay?: number | string; // default 1000ms
maxDelay?: number | string; // default 30s
multiplier?: number; // default 2
jitter?: number; // 0..1; default 0.2
}
// Job state persistence
export interface ConfigJobState {
enabled?: boolean; // default true
dir?: string; // base directory; defaults to <sessionPath>/job-state
}
// Live logging configuration
export interface ConfigLoggingLive {
enabled?: boolean; // master switch for live webhook logs
redactEmails?: boolean; // if true, redact emails in outbound logs
}
export interface ConfigLogging {
excludeFunc?: string[];
webhookExcludeFunc?: string[];
live?: ConfigLoggingLive;
liveWebhookUrl?: string; // legacy/dedicated live webhook override
redactEmails?: boolean; // legacy top-level redaction flag
// Optional nested live.url support (already handled dynamically in Logger)
[key: string]: unknown; // forward compatibility
}
// CommunityHelp removed (privacy-first policy)

View File

@@ -1,155 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
export TZ="${TZ:-UTC}"
cd /usr/src/microsoft-rewards-script
LOCKFILE=/tmp/run_daily.lock
# -------------------------------
# Function: Check and fix lockfile integrity
# -------------------------------
self_heal_lockfile() {
# If lockfile exists but is empty → remove it
if [ -f "$LOCKFILE" ]; then
local lock_content
lock_content=$(<"$LOCKFILE" || echo "")
if [[ -z "$lock_content" ]]; then
echo "[$(date)] [run_daily.sh] Found empty lockfile → removing."
rm -f "$LOCKFILE"
return
fi
# If lockfile contains non-numeric PID → remove it
if ! [[ "$lock_content" =~ ^[0-9]+$ ]]; then
echo "[$(date)] [run_daily.sh] Found corrupted lockfile content ('$lock_content') → removing."
rm -f "$LOCKFILE"
return
fi
# If lockfile contains PID but process is dead → remove it
if ! kill -0 "$lock_content" 2>/dev/null; then
echo "[$(date)] [run_daily.sh] Lockfile PID $lock_content is dead → removing stale lock."
rm -f "$LOCKFILE"
return
fi
fi
}
# -------------------------------
# Function: Acquire lock
# -------------------------------
acquire_lock() {
local max_attempts=5
local attempt=0
local timeout_hours=${STUCK_PROCESS_TIMEOUT_HOURS:-8}
local timeout_seconds=$((timeout_hours * 3600))
while [ $attempt -lt $max_attempts ]; do
# Try to create lock with current PID
if (set -C; echo "$$" > "$LOCKFILE") 2>/dev/null; then
echo "[$(date)] [run_daily.sh] Lock acquired successfully (PID: $$)"
return 0
fi
# Lock exists, validate it
if [ -f "$LOCKFILE" ]; then
local existing_pid
existing_pid=$(<"$LOCKFILE" || echo "")
echo "[$(date)] [run_daily.sh] Lock file exists with PID: '$existing_pid'"
# If lockfile content is invalid → delete and retry
if [[ -z "$existing_pid" || ! "$existing_pid" =~ ^[0-9]+$ ]]; then
echo "[$(date)] [run_daily.sh] Removing invalid lockfile → retrying..."
rm -f "$LOCKFILE"
continue
fi
# If process is dead → delete and retry
if ! kill -0 "$existing_pid" 2>/dev/null; then
echo "[$(date)] [run_daily.sh] Removing stale lock (dead PID: $existing_pid)"
rm -f "$LOCKFILE"
continue
fi
# Check process runtime → kill if exceeded timeout
local process_age
if process_age=$(ps -o etimes= -p "$existing_pid" 2>/dev/null | tr -d ' '); then
if [ "$process_age" -gt "$timeout_seconds" ]; then
echo "[$(date)] [run_daily.sh] Killing stuck process $existing_pid (${process_age}s > ${timeout_hours}h)"
kill -TERM "$existing_pid" 2>/dev/null || true
sleep 5
kill -KILL "$existing_pid" 2>/dev/null || true
rm -f "$LOCKFILE"
continue
fi
fi
fi
echo "[$(date)] [run_daily.sh] Lock held by PID $existing_pid, attempt $((attempt + 1))/$max_attempts"
sleep 2
((attempt++))
done
echo "[$(date)] [run_daily.sh] Could not acquire lock after $max_attempts attempts; exiting."
return 1
}
# -------------------------------
# Function: Release lock
# -------------------------------
release_lock() {
if [ -f "$LOCKFILE" ]; then
local lock_pid
lock_pid=$(<"$LOCKFILE")
if [ "$lock_pid" = "$$" ]; then
rm -f "$LOCKFILE"
echo "[$(date)] [run_daily.sh] Lock released (PID: $$)"
fi
fi
}
# Always release lock on exit — but only if we acquired it
trap 'release_lock' EXIT INT TERM
# -------------------------------
# MAIN EXECUTION FLOW
# -------------------------------
echo "[$(date)] [run_daily.sh] Current process PID: $$"
# Self-heal any broken or empty locks before proceeding
self_heal_lockfile
# Attempt to acquire the lock safely
if ! acquire_lock; then
exit 0
fi
# Random sleep between MIN and MAX to spread execution
MINWAIT=${MIN_SLEEP_MINUTES:-5}
MAXWAIT=${MAX_SLEEP_MINUTES:-50}
MINWAIT_SEC=$((MINWAIT*60))
MAXWAIT_SEC=$((MAXWAIT*60))
if [ "${SKIP_RANDOM_SLEEP:-false}" != "true" ]; then
SLEEPTIME=$(( MINWAIT_SEC + RANDOM % (MAXWAIT_SEC - MINWAIT_SEC) ))
echo "[$(date)] [run_daily.sh] Sleeping for $((SLEEPTIME/60)) minutes ($SLEEPTIME seconds)"
sleep "$SLEEPTIME"
else
echo "[$(date)] [run_daily.sh] Skipping random sleep"
fi
# Start the actual script
echo "[$(date)] [run_daily.sh] Starting script..."
if npm start; then
echo "[$(date)] [run_daily.sh] Script completed successfully."
else
echo "[$(date)] [run_daily.sh] ERROR: Script failed!" >&2
fi
echo "[$(date)] [run_daily.sh] Script finished"
# Lock is released automatically via trap

329
src/scheduler.ts Normal file
View File

@@ -0,0 +1,329 @@
import { DateTime, IANAZone } from 'luxon'
import { spawn } from 'child_process'
import fs from 'fs'
import path from 'path'
import { MicrosoftRewardsBot } from './index'
import { loadConfig } from './util/Load'
import { log } from './util/Logger'
import type { Config } from './interface/Config'
function resolveTimeParts(schedule: Config['schedule'] | undefined): { tz: string; hour: number; minute: number } {
const tz = (schedule?.timeZone && IANAZone.isValidZone(schedule.timeZone)) ? schedule.timeZone : 'UTC'
// Determine source string
let src = ''
if (typeof schedule?.useAmPm === 'boolean') {
if (schedule.useAmPm) src = (schedule.time12 || schedule.time || '').trim()
else src = (schedule.time24 || schedule.time || '').trim()
} else {
// Back-compat: prefer time if present; else time24 or time12
src = (schedule?.time || schedule?.time24 || schedule?.time12 || '').trim()
}
// Try to parse 24h first: HH:mm
const m24 = src.match(/^\s*(\d{1,2}):(\d{2})\s*$/i)
if (m24) {
const hh = Math.max(0, Math.min(23, parseInt(m24[1]!, 10)))
const mm = Math.max(0, Math.min(59, parseInt(m24[2]!, 10)))
return { tz, hour: hh, minute: mm }
}
// Parse 12h with AM/PM: h:mm AM or h AM
const m12 = src.match(/^\s*(\d{1,2})(?::(\d{2}))?\s*(AM|PM)\s*$/i)
if (m12) {
let hh = parseInt(m12[1]!, 10)
const mm = m12[2] ? parseInt(m12[2]!, 10) : 0
const ampm = m12[3]!.toUpperCase()
if (hh === 12) hh = 0
if (ampm === 'PM') hh += 12
hh = Math.max(0, Math.min(23, hh))
const m = Math.max(0, Math.min(59, mm))
return { tz, hour: hh, minute: m }
}
// Fallback: default 09:00
return { tz, hour: 9, minute: 0 }
}
function parseTargetToday(now: Date, schedule: Config['schedule'] | undefined) {
const { tz, hour, minute } = resolveTimeParts(schedule)
const dtn = DateTime.fromJSDate(now, { zone: tz })
return dtn.set({ hour, minute, second: 0, millisecond: 0 })
}
async function runOnePass(): Promise<void> {
const bot = new MicrosoftRewardsBot(false)
await bot.initialize()
await bot.run()
}
/**
* Run a single pass either in-process or as a child process (default),
* with a watchdog timeout to kill stuck runs.
*/
async function runOnePassWithWatchdog(): Promise<void> {
// Heartbeat-aware watchdog configuration
// If a child is actively updating its heartbeat file, we allow it to run beyond the legacy timeout.
// Defaults are generous to allow first-day passes to finish searches with delays.
const staleHeartbeatMin = Number(
process.env.SCHEDULER_STALE_HEARTBEAT_MINUTES || process.env.SCHEDULER_PASS_TIMEOUT_MINUTES || 30
)
const graceMin = Number(process.env.SCHEDULER_HEARTBEAT_GRACE_MINUTES || 15)
const hardcapMin = Number(process.env.SCHEDULER_PASS_HARDCAP_MINUTES || 480) // 8 hours
const checkEveryMs = 60_000 // check once per minute
// Fork per pass: safer because we can terminate a stuck child without killing the scheduler
const forkPerPass = String(process.env.SCHEDULER_FORK_PER_PASS || 'true').toLowerCase() !== 'false'
if (!forkPerPass) {
// In-process fallback (cannot forcefully stop if truly stuck)
await log('main', 'SCHEDULER', `Starting pass in-process (grace ${graceMin}m • stale ${staleHeartbeatMin}m • hardcap ${hardcapMin}m). Cannot force-kill if stuck.`)
// No true watchdog possible in-process; just run
await runOnePass()
return
}
// Child process execution
const indexJs = path.join(__dirname, 'index.js')
await log('main', 'SCHEDULER', `Spawning child for pass: ${process.execPath} ${indexJs}`)
// Prepare heartbeat file path and pass to child
const cfg = loadConfig() as Config
const baseDir = path.join(process.cwd(), cfg.sessionPath || 'sessions')
const hbFile = path.join(baseDir, `heartbeat_${Date.now()}.lock`)
try { fs.mkdirSync(baseDir, { recursive: true }) } catch { /* ignore */ }
await new Promise<void>((resolve) => {
const child = spawn(process.execPath, [indexJs], { stdio: 'inherit', env: { ...process.env, SCHEDULER_HEARTBEAT_FILE: hbFile } })
let finished = false
const startedAt = Date.now()
const killChild = async (signal: NodeJS.Signals) => {
try {
await log('main', 'SCHEDULER', `Sending ${signal} to stuck child PID ${child.pid}`,'warn')
child.kill(signal)
} catch { /* ignore */ }
}
const timer = setInterval(() => {
if (finished) return
const now = Date.now()
const runtimeMin = Math.floor((now - startedAt) / 60000)
// Hard cap: always terminate if exceeded
if (runtimeMin >= hardcapMin) {
log('main', 'SCHEDULER', `Pass exceeded hard cap of ${hardcapMin} minutes; terminating...`, 'warn')
void killChild('SIGTERM')
setTimeout(() => { try { child.kill('SIGKILL') } catch { /* ignore */ } }, 10_000)
return
}
// Before grace, don't judge
if (runtimeMin < graceMin) return
// Check heartbeat freshness
try {
const st = fs.statSync(hbFile)
const mtimeMs = st.mtimeMs
const ageMin = Math.floor((now - mtimeMs) / 60000)
if (ageMin >= staleHeartbeatMin) {
log('main', 'SCHEDULER', `Heartbeat stale for ${ageMin}m (>=${staleHeartbeatMin}m). Terminating child...`, 'warn')
void killChild('SIGTERM')
setTimeout(() => { try { child.kill('SIGKILL') } catch { /* ignore */ } }, 10_000)
}
} catch {
// If file missing after grace, consider stale
log('main', 'SCHEDULER', 'Heartbeat file missing after grace. Terminating child...', 'warn')
void killChild('SIGTERM')
setTimeout(() => { try { child.kill('SIGKILL') } catch { /* ignore */ } }, 10_000)
}
}, checkEveryMs)
child.on('exit', async (code, signal) => {
finished = true
clearInterval(timer)
// Cleanup heartbeat file
try { if (fs.existsSync(hbFile)) fs.unlinkSync(hbFile) } catch { /* ignore */ }
if (signal) {
await log('main', 'SCHEDULER', `Child exited due to signal: ${signal}`, 'warn')
} else if (code && code !== 0) {
await log('main', 'SCHEDULER', `Child exited with non-zero code: ${code}`, 'warn')
} else {
await log('main', 'SCHEDULER', 'Child pass completed successfully')
}
resolve()
})
child.on('error', async (err) => {
finished = true
clearInterval(timer)
try { if (fs.existsSync(hbFile)) fs.unlinkSync(hbFile) } catch { /* ignore */ }
await log('main', 'SCHEDULER', `Failed to spawn child: ${err instanceof Error ? err.message : String(err)}`, 'error')
resolve()
})
})
}
async function runPasses(passes: number): Promise<void> {
const n = Math.max(1, Math.floor(passes || 1))
for (let i = 1; i <= n; i++) {
await log('main', 'SCHEDULER', `Starting pass ${i}/${n}`)
const started = Date.now()
await runOnePassWithWatchdog()
const took = Date.now() - started
const sec = Math.max(1, Math.round(took / 1000))
await log('main', 'SCHEDULER', `Completed pass ${i}/${n}`)
await log('main', 'SCHEDULER', `Pass ${i} duration: ${sec}s`)
}
}
async function main() {
const cfg = loadConfig() as Config & { schedule?: { enabled?: boolean; time?: string; timeZone?: string; runImmediatelyOnStart?: boolean } }
const schedule = cfg.schedule || { enabled: false }
const passes = typeof cfg.passesPerRun === 'number' ? cfg.passesPerRun : 1
const offPerWeek = Math.max(0, Math.min(7, Number(cfg.humanization?.randomOffDaysPerWeek ?? 1)))
let offDays: number[] = [] // 1..7 ISO weekday
let offWeek: number | null = null
type VacRange = { start: string; end: string } | null
let vacMonth: string | null = null // 'yyyy-LL'
let vacRange: VacRange = null // ISO dates 'yyyy-LL-dd'
const refreshOffDays = async (now: { weekNumber: number }) => {
if (offPerWeek <= 0) { offDays = []; offWeek = null; return }
const week = now.weekNumber
if (offWeek === week && offDays.length) return
// choose distinct weekdays [1..7]
const pool = [1,2,3,4,5,6,7]
const chosen: number[] = []
for (let i=0;i<Math.min(offPerWeek,7);i++) {
const idx = Math.floor(Math.random()*pool.length)
chosen.push(pool[idx]!)
pool.splice(idx,1)
}
offDays = chosen.sort((a,b)=>a-b)
offWeek = week
await log('main','SCHEDULER',`Selected random off-days this week (ISO): ${offDays.join(', ')}`,'warn')
}
const chooseVacationRange = async (now: typeof DateTime.prototype) => {
// Only when enabled
if (!cfg.vacation?.enabled) { vacRange = null; vacMonth = null; return }
const monthKey = now.toFormat('yyyy-LL')
if (vacMonth === monthKey && vacRange) return
// Determine month days and choose contiguous block
const monthStart = now.startOf('month')
const monthEnd = now.endOf('month')
const totalDays = monthEnd.day
const minD = Math.max(1, Math.min(28, Number(cfg.vacation.minDays ?? 3)))
const maxD = Math.max(minD, Math.min(31, Number(cfg.vacation.maxDays ?? 5)))
const span = (minD === maxD) ? minD : (minD + Math.floor(Math.random() * (maxD - minD + 1)))
const latestStart = Math.max(1, totalDays - span + 1)
const startDay = 1 + Math.floor(Math.random() * latestStart)
const start = monthStart.set({ day: startDay })
const end = start.plus({ days: span - 1 })
vacMonth = monthKey
vacRange = { start: start.toFormat('yyyy-LL-dd'), end: end.toFormat('yyyy-LL-dd') }
await log('main','SCHEDULER',`Selected vacation block this month: ${vacRange.start}${vacRange.end} (${span} day(s))`,'warn')
}
if (!schedule.enabled) {
await log('main', 'SCHEDULER', 'Schedule disabled; running once then exit')
await runPasses(passes)
process.exit(0)
}
const tz = (schedule.timeZone && IANAZone.isValidZone(schedule.timeZone)) ? schedule.timeZone : 'UTC'
// Default to false to avoid unexpected immediate runs
const runImmediate = schedule.runImmediatelyOnStart === true
let running = false
// Optional initial jitter before the first run (to vary start time)
const initJitterMin = Number(process.env.SCHEDULER_INITIAL_JITTER_MINUTES_MIN || process.env.SCHEDULER_INITIAL_JITTER_MIN || 0)
const initJitterMax = Number(process.env.SCHEDULER_INITIAL_JITTER_MINUTES_MAX || process.env.SCHEDULER_INITIAL_JITTER_MAX || 0)
const initialJitterBounds: [number, number] = [isFinite(initJitterMin) ? initJitterMin : 0, isFinite(initJitterMax) ? initJitterMax : 0]
const applyInitialJitter = (initialJitterBounds[0] > 0 || initialJitterBounds[1] > 0)
if (runImmediate && !running) {
running = true
if (applyInitialJitter) {
const min = Math.max(0, Math.min(initialJitterBounds[0], initialJitterBounds[1]))
const max = Math.max(0, Math.max(initialJitterBounds[0], initialJitterBounds[1]))
const jitterSec = (min === max) ? min * 60 : (min * 60 + Math.floor(Math.random() * ((max - min) * 60)))
if (jitterSec > 0) {
await log('main', 'SCHEDULER', `Initial jitter: delaying first run by ${Math.round(jitterSec / 60)} minute(s) (${jitterSec}s)`, 'warn')
await new Promise((r) => setTimeout(r, jitterSec * 1000))
}
}
const nowDT = DateTime.local().setZone(tz)
await chooseVacationRange(nowDT)
await refreshOffDays(nowDT)
const todayIso = nowDT.toFormat('yyyy-LL-dd')
const vr = vacRange as { start: string; end: string } | null
const isVacationToday = !!(vr && todayIso >= vr.start && todayIso <= vr.end)
if (isVacationToday) {
await log('main','SCHEDULER',`Skipping immediate run: vacation day (${todayIso})`,'warn')
} else if (offDays.includes(nowDT.weekday)) {
await log('main','SCHEDULER',`Skipping immediate run: off-day (weekday ${nowDT.weekday})`,'warn')
} else {
await runPasses(passes)
}
running = false
}
for (;;) {
const now = new Date()
const targetToday = parseTargetToday(now, schedule)
let next = targetToday
const nowDT = DateTime.fromJSDate(now, { zone: targetToday.zone })
if (nowDT >= targetToday) {
next = targetToday.plus({ days: 1 })
}
let ms = Math.max(0, next.toMillis() - nowDT.toMillis())
// Optional daily jitter to further randomize the exact start time each day
const dailyJitterMin = Number(process.env.SCHEDULER_DAILY_JITTER_MINUTES_MIN || process.env.SCHEDULER_DAILY_JITTER_MIN || 0)
const dailyJitterMax = Number(process.env.SCHEDULER_DAILY_JITTER_MINUTES_MAX || process.env.SCHEDULER_DAILY_JITTER_MAX || 0)
const djMin = isFinite(dailyJitterMin) ? dailyJitterMin : 0
const djMax = isFinite(dailyJitterMax) ? dailyJitterMax : 0
let extraMs = 0
if (djMin > 0 || djMax > 0) {
const mn = Math.max(0, Math.min(djMin, djMax))
const mx = Math.max(0, Math.max(djMin, djMax))
const jitterSec = (mn === mx) ? mn * 60 : (mn * 60 + Math.floor(Math.random() * ((mx - mn) * 60)))
extraMs = jitterSec * 1000
ms += extraMs
}
const human = next.toFormat('yyyy-LL-dd HH:mm ZZZZ')
const totalSec = Math.round(ms / 1000)
if (extraMs > 0) {
await log('main', 'SCHEDULER', `Next run at ${human} plus daily jitter (+${Math.round(extraMs/60000)}m) → in ${totalSec}s`)
} else {
await log('main', 'SCHEDULER', `Next run at ${human} (in ${totalSec}s)`)
}
await new Promise((resolve) => setTimeout(resolve, ms))
const nowRun = DateTime.local().setZone(tz)
await chooseVacationRange(nowRun)
await refreshOffDays(nowRun)
const todayIso2 = nowRun.toFormat('yyyy-LL-dd')
const vr2 = vacRange as { start: string; end: string } | null
const isVacation = !!(vr2 && todayIso2 >= vr2.start && todayIso2 <= vr2.end)
if (isVacation) {
await log('main','SCHEDULER',`Skipping scheduled run: vacation day (${todayIso2})`,'warn')
continue
}
if (offDays.includes(nowRun.weekday)) {
await log('main','SCHEDULER',`Skipping scheduled run: off-day (weekday ${nowRun.weekday})`,'warn')
continue
}
if (!running) {
running = true
await runPasses(passes)
running = false
} else {
await log('main','SCHEDULER','Skipped scheduled trigger because a pass is already running','warn')
}
}
}
main().catch((e) => {
console.error(e)
process.exit(1)
})

7
src/types/luxon.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/* Minimal ambient declarations to unblock TypeScript when @types/luxon not present. */
declare module 'luxon' {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const DateTime: any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const IANAZone: any
}

View File

@@ -0,0 +1,25 @@
export class AdaptiveThrottler {
private errorCount = 0
private successCount = 0
private window: Array<{ ok: boolean; at: number }> = []
private readonly maxWindow = 50
record(ok: boolean) {
this.window.push({ ok, at: Date.now() })
if (ok) this.successCount++
else this.errorCount++
if (this.window.length > this.maxWindow) {
const removed = this.window.shift()
if (removed) removed.ok ? this.successCount-- : this.errorCount--
}
}
/** Return a multiplier to apply to waits (1 = normal). */
getDelayMultiplier(): number {
const total = Math.max(1, this.successCount + this.errorCount)
const errRatio = this.errorCount / total
// 0% errors -> 1x; 50% errors -> ~1.8x; 80% -> ~2.5x (cap)
const mult = 1 + Math.min(1.5, errRatio * 2)
return Number(mult.toFixed(2))
}
}

View File

@@ -42,7 +42,21 @@ class AxiosClient {
return bypassInstance.request(config)
}
return this.instance.request(config)
try {
return await this.instance.request(config)
} catch (err: unknown) {
// If proxied request fails with common proxy/network errors, retry once without proxy
const e = err as { code?: string; cause?: { code?: string }; message?: string } | undefined
const code = e?.code || e?.cause?.code
const isNetErr = code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'ENOTFOUND'
const msg = String(e?.message || '')
const looksLikeProxyIssue = /proxy|tunnel|socks|agent/i.test(msg)
if (!bypassProxy && (isNetErr || looksLikeProxyIssue)) {
const bypassInstance = axios.create()
return bypassInstance.request(config)
}
throw err
}
}
}

16
src/util/BanDetector.ts Normal file
View File

@@ -0,0 +1,16 @@
export type BanStatus = { status: boolean; reason: string }
const BAN_PATTERNS: Array<{ re: RegExp; reason: string }> = [
{ re: /suspend|suspended|suspension/i, reason: 'account suspended' },
{ re: /locked|lockout|serviceabuse|abuse/i, reason: 'locked or service abuse detected' },
{ re: /unusual.*activity|unusual activity/i, reason: 'unusual activity prompts' },
{ re: /verify.*identity|identity.*verification/i, reason: 'identity verification required' }
]
export function detectBanReason(input: unknown): BanStatus {
const s = input instanceof Error ? (input.message || '') : String(input || '')
for (const p of BAN_PATTERNS) {
if (p.re.test(s)) return { status: true, reason: p.reason }
}
return { status: false, reason: '' }
}

View File

@@ -1,32 +1,109 @@
import axios from 'axios'
import { Config } from '../interface/Config'
import { Ntfy } from './Ntfy'
// Light obfuscation of the avatar URL (base64). Prevents casual editing in config.
const AVATAR_B64 = 'aHR0cHM6Ly9tZWRpYS5kaXNjb3JkYXBwLm5ldC9hdHRhY2htZW50cy8xNDIxMTYzOTUyOTcyMzY5OTMxLzE0MjExNjQxNDU5OTQyNDAxMTAvbXNuLnBuZz93aWR0aD01MTImZWlnaHQ9NTEy'
function getAvatarUrl(): string {
try { return Buffer.from(AVATAR_B64, 'base64').toString('utf-8') } catch { return '' }
}
type WebhookContext = 'summary' | 'ban' | 'security' | 'compromised' | 'spend' | 'error' | 'default'
function pickUsername(ctx: WebhookContext, fallbackColor?: number): string {
switch (ctx) {
case 'summary': return 'Summary'
case 'ban': return 'Ban'
case 'security': return 'Security'
case 'compromised': return 'Pirate'
case 'spend': return 'Spend'
case 'error': return 'Error'
default: return fallbackColor === 0xFF0000 ? 'Error' : 'Rewards'
}
}
interface DiscordField { name: string; value: string; inline?: boolean }
interface DiscordEmbed {
title?: string
description?: string
color?: number
fields?: DiscordField[]
}
interface ConclusionPayload {
content?: string
embeds?: any[]
embeds?: DiscordEmbed[]
context?: WebhookContext
}
/**
* Send a final structured summary to the dedicated conclusion webhook (if enabled),
* otherwise do nothing. Does NOT fallback to the normal logging webhook to avoid spam.
* Send a final structured summary to the configured webhook,
* and optionally mirror a plain-text summary to NTFY.
*
* This preserves existing webhook behavior while adding NTFY
* as a separate, optional channel.
*/
export async function ConclusionWebhook(configData: Config, content: string, embed?: ConclusionPayload) {
const webhook = configData.conclusionWebhook
export async function ConclusionWebhook(config: Config, content: string, payload?: ConclusionPayload) {
// Send to both webhooks when available
const hasConclusion = !!(config.conclusionWebhook?.enabled && config.conclusionWebhook.url)
const hasWebhook = !!(config.webhook?.enabled && config.webhook.url)
const sameTarget = hasConclusion && hasWebhook && config.conclusionWebhook!.url === config.webhook!.url
if (!webhook || !webhook.enabled || webhook.url.length < 10) return
const body: ConclusionPayload & { username?: string; avatar_url?: string } = {}
if (payload?.embeds) body.embeds = payload.embeds
if (content && content.trim()) body.content = content
const firstColor = payload?.embeds && payload.embeds[0]?.color
const ctx: WebhookContext = payload?.context || (firstColor === 0xFF0000 ? 'error' : 'default')
body.username = pickUsername(ctx, firstColor)
body.avatar_url = getAvatarUrl()
const body: ConclusionPayload = embed?.embeds ? { embeds: embed.embeds } : { content }
if (content && !body.content && !body.embeds) body.content = content
const request = {
method: 'POST',
url: webhook.url,
headers: {
'Content-Type': 'application/json'
},
data: body
// Post to conclusion webhook if configured
const postWithRetry = async (url: string, label: string) => {
const max = 2
let lastErr: unknown = null
for (let attempt = 1; attempt <= max; attempt++) {
try {
await axios.post(url, body, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 })
console.log(`[Webhook:${label}] summary sent (attempt ${attempt}).`)
return
} catch (e) {
lastErr = e
if (attempt === max) break
await new Promise(r => setTimeout(r, 1000 * attempt))
}
}
console.error(`[Webhook:${label}] failed after ${max} attempts:`, lastErr)
}
await axios(request).catch(() => { })
if (hasConclusion) {
await postWithRetry(config.conclusionWebhook!.url, sameTarget ? 'conclusion+primary' : 'conclusion')
}
if (hasWebhook && !sameTarget) {
await postWithRetry(config.webhook!.url, 'primary')
}
// NTFY: mirror a plain text summary (optional)
if (config.ntfy?.enabled && config.ntfy.url && config.ntfy.topic) {
let message = content || ''
if (!message && payload?.embeds && payload.embeds.length > 0) {
const e: DiscordEmbed = payload.embeds[0]!
const title = e.title ? `${e.title}\n` : ''
const desc = e.description ? `${e.description}\n` : ''
const totals = e.fields && e.fields[0]?.value ? `\n${e.fields[0].value}\n` : ''
message = `${title}${desc}${totals}`.trim()
}
if (!message) message = 'Microsoft Rewards run complete.'
// Choose NTFY level based on embed color (yellow = warn)
let embedColor: number | undefined
if (payload?.embeds && payload.embeds.length > 0) {
embedColor = payload.embeds[0]!.color
}
const ntfyType = embedColor === 0xFFAA00 ? 'warn' : 'log'
try {
await Ntfy(message, ntfyType)
console.log('Conclusion summary sent to NTFY.')
} catch (err) {
console.error('Failed to send conclusion summary to NTFY:', err)
}
}
}

54
src/util/Humanizer.ts Normal file
View File

@@ -0,0 +1,54 @@
import { Page } from 'rebrowser-playwright'
import Util from './Utils'
import type { ConfigHumanization } from '../interface/Config'
export class Humanizer {
private util: Util
private cfg: ConfigHumanization | undefined
constructor(util: Util, cfg?: ConfigHumanization) {
this.util = util
this.cfg = cfg
}
async microGestures(page: Page): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
const moveProb = this.cfg?.gestureMoveProb ?? 0.4
const scrollProb = this.cfg?.gestureScrollProb ?? 0.2
try {
if (Math.random() < moveProb) {
const x = Math.floor(Math.random() * 40) + 5
const y = Math.floor(Math.random() * 30) + 5
await page.mouse.move(x, y, { steps: 2 }).catch(() => {})
}
if (Math.random() < scrollProb) {
const dy = (Math.random() < 0.5 ? 1 : -1) * (Math.floor(Math.random() * 150) + 50)
await page.mouse.wheel(0, dy).catch(() => {})
}
} catch {/* noop */}
}
async actionPause(): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
const defMin = 150
const defMax = 450
let min = defMin
let max = defMax
if (this.cfg?.actionDelay) {
const parse = (v: number | string) => {
if (typeof v === 'number') return v
try {
const n = this.util.stringToMs(String(v))
return Math.max(0, Math.min(n, 10_000))
} catch { return defMin }
}
min = parse(this.cfg.actionDelay.min)
max = parse(this.cfg.actionDelay.max)
if (min > max) [min, max] = [max, min]
max = Math.min(max, 5_000)
}
await this.util.wait(this.util.randomNumber(min, max))
}
}
export default Humanizer

58
src/util/JobState.ts Normal file
View File

@@ -0,0 +1,58 @@
import fs from 'fs'
import path from 'path'
import type { Config } from '../interface/Config'
type DayState = {
doneOfferIds: string[]
}
type FileState = {
days: Record<string, DayState>
}
export class JobState {
private baseDir: string
constructor(cfg: Config) {
const dir = cfg.jobState?.dir || path.join(process.cwd(), cfg.sessionPath, 'job-state')
this.baseDir = dir
if (!fs.existsSync(this.baseDir)) fs.mkdirSync(this.baseDir, { recursive: true })
}
private fileFor(email: string): string {
const safe = email.replace(/[^a-z0-9._-]/gi, '_')
return path.join(this.baseDir, `${safe}.json`)
}
private load(email: string): FileState {
const file = this.fileFor(email)
if (!fs.existsSync(file)) return { days: {} }
try {
const raw = fs.readFileSync(file, 'utf-8')
const parsed = JSON.parse(raw)
return parsed && typeof parsed === 'object' && parsed.days ? parsed as FileState : { days: {} }
} catch { return { days: {} } }
}
private save(email: string, state: FileState): void {
const file = this.fileFor(email)
fs.writeFileSync(file, JSON.stringify(state, null, 2), 'utf-8')
}
isDone(email: string, day: string, offerId: string): boolean {
const st = this.load(email)
const d = st.days[day]
if (!d) return false
return d.doneOfferIds.includes(offerId)
}
markDone(email: string, day: string, offerId: string): void {
const st = this.load(email)
if (!st.days[day]) st.days[day] = { doneOfferIds: [] }
const d = st.days[day]
if (!d.doneOfferIds.includes(offerId)) d.doneOfferIds.push(offerId)
this.save(email, st)
}
}
export default JobState

View File

@@ -8,38 +8,274 @@ import { Account } from '../interface/Account'
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 {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const n: any = (raw as any) || {}
// Browser / execution
const headless = n.browser?.headless ?? n.headless ?? false
const globalTimeout = n.browser?.globalTimeout ?? n.globalTimeout ?? '30s'
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
const searchSettingsSrc = n.search?.settings ?? n.searchSettings ?? {}
const delaySrc = searchSettingsSrc.delay ?? searchSettingsSrc.searchDelay ?? { min: '3min', max: '5min' }
const searchSettings = {
useGeoLocaleQueries: !!(searchSettingsSrc.useGeoLocaleQueries ?? false),
scrollRandomResults: !!(searchSettingsSrc.scrollRandomResults ?? false),
clickRandomResults: !!(searchSettingsSrc.clickRandomResults ?? false),
retryMobileSearchAmount: Number(searchSettingsSrc.retryMobileSearchAmount ?? 2),
searchDelay: {
min: delaySrc.min ?? '3min',
max: delaySrc.max ?? '5min'
},
localFallbackCount: Number(searchSettingsSrc.localFallbackCount ?? 25),
extraFallbackRetries: Number(searchSettingsSrc.extraFallbackRetries ?? 1)
}
// Workers
const workers = n.workers ?? {
doDailySet: true,
doMorePromotions: true,
doPunchCards: true,
doDesktopSearch: true,
doMobileSearch: true,
doDailyCheckIn: true,
doReadToEarn: true,
bundleDailySetWithSearch: false
}
// Ensure missing flag gets a default
if (typeof workers.bundleDailySetWithSearch !== 'boolean') workers.bundleDailySetWithSearch = false
// Logging
const logging = n.logging ?? {}
const logExcludeFunc = Array.isArray(logging.excludeFunc) ? logging.excludeFunc : (n.logExcludeFunc ?? [])
const webhookLogExcludeFunc = Array.isArray(logging.webhookExcludeFunc) ? logging.webhookExcludeFunc : (n.webhookLogExcludeFunc ?? [])
// Notifications
const notifications = n.notifications ?? {}
const webhook = notifications.webhook ?? n.webhook ?? { enabled: false, url: '' }
const conclusionWebhook = notifications.conclusionWebhook ?? n.conclusionWebhook ?? { enabled: false, url: '' }
const ntfy = notifications.ntfy ?? n.ntfy ?? { enabled: false, url: '', topic: '', authToken: '' }
// Buy Mode
const buyMode = n.buyMode ?? {}
const buyModeEnabled = typeof buyMode.enabled === 'boolean' ? buyMode.enabled : false
const buyModeMax = typeof buyMode.maxMinutes === 'number' ? buyMode.maxMinutes : 45
// Fingerprinting
const saveFingerprint = (n.fingerprinting?.saveFingerprint ?? n.saveFingerprint) ?? { mobile: false, desktop: false }
// Humanization defaults (single on/off)
if (!n.humanization) n.humanization = {}
if (typeof n.humanization.enabled !== 'boolean') n.humanization.enabled = true
if (typeof n.humanization.stopOnBan !== 'boolean') n.humanization.stopOnBan = false
if (typeof n.humanization.immediateBanAlert !== 'boolean') n.humanization.immediateBanAlert = true
if (typeof n.humanization.randomOffDaysPerWeek !== 'number') {
n.humanization.randomOffDaysPerWeek = 1
}
// Strong default gestures when enabled (explicit values still win)
if (typeof n.humanization.gestureMoveProb !== 'number') {
n.humanization.gestureMoveProb = n.humanization.enabled === false ? 0 : 0.5
}
if (typeof n.humanization.gestureScrollProb !== 'number') {
n.humanization.gestureScrollProb = n.humanization.enabled === false ? 0 : 0.25
}
// Vacation mode (monthly contiguous off-days)
if (!n.vacation) n.vacation = {}
if (typeof n.vacation.enabled !== 'boolean') n.vacation.enabled = false
const vMin = Number(n.vacation.minDays)
const vMax = Number(n.vacation.maxDays)
n.vacation.minDays = isFinite(vMin) && vMin > 0 ? Math.floor(vMin) : 3
n.vacation.maxDays = isFinite(vMax) && vMax > 0 ? Math.floor(vMax) : 5
if (n.vacation.maxDays < n.vacation.minDays) {
const t = n.vacation.minDays; n.vacation.minDays = n.vacation.maxDays; n.vacation.maxDays = t
}
const cfg: Config = {
baseURL: n.baseURL ?? 'https://rewards.bing.com',
sessionPath: n.sessionPath ?? 'sessions',
headless,
parallel,
runOnZeroPoints,
clusters,
saveFingerprint,
workers,
searchOnBingLocalQueries: !!useLocalQueries,
globalTimeout,
searchSettings,
humanization: n.humanization,
retryPolicy: n.retryPolicy,
jobState: n.jobState,
logExcludeFunc,
webhookLogExcludeFunc,
logging, // retain full logging object for live webhook usage
proxy: n.proxy ?? { proxyGoogleTrends: true, proxyBingTerms: true },
webhook,
conclusionWebhook,
ntfy,
diagnostics: n.diagnostics,
update: n.update,
schedule: n.schedule,
passesPerRun: passesPerRun,
vacation: n.vacation,
buyMode: { enabled: buyModeEnabled, maxMinutes: buyModeMax },
crashRecovery: n.crashRecovery || {}
}
return cfg
}
export function loadAccounts(): Account[] {
try {
// 1) CLI dev override
let file = 'accounts.json'
// If dev mode, use dev account(s)
if (process.argv.includes('-dev')) {
file = 'accounts.dev.json'
}
const accountDir = path.join(__dirname, '../', file)
const accounts = fs.readFileSync(accountDir, 'utf-8')
// 2) Docker-friendly env overrides
const envJson = process.env.ACCOUNTS_JSON
const envFile = process.env.ACCOUNTS_FILE
return JSON.parse(accounts)
let raw: string | undefined
if (envJson && envJson.trim().startsWith('[')) {
raw = 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')
} else {
// Try multiple locations to support both root mounts and dist mounts
const candidates = [
path.join(__dirname, '../', file), // root/accounts.json (preferred)
path.join(__dirname, '../src', file), // fallback: file kept inside src/
path.join(process.cwd(), file), // cwd override
path.join(process.cwd(), 'src', file), // cwd/src/accounts.json
path.join(__dirname, file) // dist/accounts.json (legacy)
]
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')
}
// Support comments in accounts file (same as config)
const cleaned = stripJsonComments(raw)
const parsedUnknown = JSON.parse(cleaned)
// 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')
// minimal shape validation
for (const a of parsed) {
if (!a || typeof a.email !== 'string' || typeof a.password !== 'string') {
throw new Error('each account must have email and password strings')
}
}
return parsed as Account[]
} catch (error) {
throw new Error(error as string)
}
}
export function getConfigPath(): string { return configSourcePath }
export function loadConfig(): Config {
try {
if (configCache) {
return configCache
}
const configDir = path.join(__dirname, '../', 'config.json')
const config = fs.readFileSync(configDir, 'utf-8')
// Resolve config.json from common locations
const candidates = [
path.join(__dirname, '../', 'config.json'), // root/config.json when compiled (expected primary)
path.join(__dirname, '../src', 'config.json'), // fallback: running compiled dist but file still in src/
path.join(process.cwd(), 'config.json'), // cwd root
path.join(process.cwd(), 'src', 'config.json'), // running from repo root but config left in src/
path.join(__dirname, 'config.json') // last resort: dist/util/config.json
]
let cfgPath: string | null = null
for (const p of candidates) {
try { if (fs.existsSync(p)) { cfgPath = p; break } } catch { /* ignore */ }
}
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 normalized = normalizeConfig(raw)
configCache = normalized // Set as cache
configSourcePath = cfgPath
const configData = JSON.parse(config)
configCache = configData // Set as cache
return configData
return normalized
} catch (error) {
throw new Error(error as string)
}
@@ -56,13 +292,19 @@ export async function loadSessionData(sessionPath: string, email: string, isMobi
cookies = JSON.parse(cookiesData)
}
// Fetch fingerprint file
const fingerprintFile = path.join(__dirname, '../browser/', sessionPath, email, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`)
// Fetch fingerprint file (support both legacy typo "fingerpint" and corrected "fingerprint")
const baseDir = path.join(__dirname, '../browser/', sessionPath, email)
const legacyFile = path.join(baseDir, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`)
const correctFile = path.join(baseDir, `${isMobile ? 'mobile_fingerprint' : 'desktop_fingerprint'}.json`)
let fingerprint!: BrowserFingerprintWithHeaders
if (((saveFingerprint.desktop && !isMobile) || (saveFingerprint.mobile && isMobile)) && fs.existsSync(fingerprintFile)) {
const fingerprintData = await fs.promises.readFile(fingerprintFile, 'utf-8')
fingerprint = JSON.parse(fingerprintData)
const shouldLoad = (saveFingerprint.desktop && !isMobile) || (saveFingerprint.mobile && isMobile)
if (shouldLoad) {
const chosen = fs.existsSync(correctFile) ? correctFile : (fs.existsSync(legacyFile) ? legacyFile : '')
if (chosen) {
const fingerprintData = await fs.promises.readFile(chosen, 'utf-8')
fingerprint = JSON.parse(fingerprintData)
}
}
return {
@@ -96,7 +338,7 @@ export async function saveSessionData(sessionPath: string, browser: BrowserConte
}
}
export async function saveFingerprintData(sessionPath: string, email: string, isMobile: boolean, fingerpint: BrowserFingerprintWithHeaders): Promise<string> {
export async function saveFingerprintData(sessionPath: string, email: string, isMobile: boolean, fingerprint: BrowserFingerprintWithHeaders): Promise<string> {
try {
// Fetch path
const sessionDir = path.join(__dirname, '../browser/', sessionPath, email)
@@ -106,8 +348,12 @@ export async function saveFingerprintData(sessionPath: string, email: string, is
await fs.promises.mkdir(sessionDir, { recursive: true })
}
// Save fingerprint to a file
await fs.promises.writeFile(path.join(sessionDir, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`), JSON.stringify(fingerpint))
// Save fingerprint to files (write both legacy and corrected names for compatibility)
const legacy = path.join(sessionDir, `${isMobile ? 'mobile_fingerpint' : 'desktop_fingerpint'}.json`)
const correct = path.join(sessionDir, `${isMobile ? 'mobile_fingerprint' : 'desktop_fingerprint'}.json`)
const payload = JSON.stringify(fingerprint)
await fs.promises.writeFile(correct, payload)
try { await fs.promises.writeFile(legacy, payload) } catch { /* ignore */ }
return sessionDir
} catch (error) {

View File

@@ -1,45 +1,92 @@
import chalk from 'chalk'
import { Webhook } from './Webhook'
import { Ntfy } from './Ntfy'
import { loadConfig } from './Load'
export function log(isMobile: boolean | 'main', title: string, message: string, type: 'log' | 'warn' | 'error' = 'log', color?: keyof typeof chalk): void {
// Synchronous logger that returns an Error when type === 'error' so callers can `throw log(...)` safely.
export function log(isMobile: boolean | 'main', title: string, message: string, type: 'log' | 'warn' | 'error' = 'log', color?: keyof typeof chalk): Error | void {
const configData = loadConfig()
if (configData.logExcludeFunc.some(x => x.toLowerCase() === title.toLowerCase())) {
// Access logging config with fallback for backward compatibility
const configAny = configData as unknown as Record<string, unknown>
const loggingConfig = configAny.logging || configData
const loggingConfigAny = loggingConfig as unknown as Record<string, unknown>
const logExcludeFunc = Array.isArray(loggingConfigAny.excludeFunc) ? loggingConfigAny.excludeFunc :
Array.isArray(loggingConfigAny.logExcludeFunc) ? loggingConfigAny.logExcludeFunc : []
if (Array.isArray(logExcludeFunc) && logExcludeFunc.some((x: string) => x.toLowerCase() === title.toLowerCase())) {
return
}
const currentTime = new Date().toLocaleString()
const platformText = isMobile === 'main' ? 'MAIN' : isMobile ? 'MOBILE' : 'DESKTOP'
const chalkedPlatform = isMobile === 'main' ? chalk.bgCyan('MAIN') : isMobile ? chalk.bgBlue('MOBILE') : chalk.bgMagenta('DESKTOP')
// Clean string for notifications (no chalk, structured)
type LoggingCfg = { excludeFunc?: string[]; webhookExcludeFunc?: string[]; redactEmails?: boolean }
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||''}`
}) : s
const cleanStr = redact(`[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${platformText} [${title}] ${message}`)
// Clean string for the Webhook (no chalk)
const cleanStr = `[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${platformText} [${title}] ${message}`
// Send the clean string to the Webhook
if (!configData.webhookLogExcludeFunc.some(x => x.toLowerCase() === title.toLowerCase())) {
Webhook(configData, cleanStr)
// Define conditions for sending to NTFY
const ntfyConditions = {
log: [
message.toLowerCase().includes('started tasks for account'),
message.toLowerCase().includes('press the number'),
message.toLowerCase().includes('no points to earn')
],
error: [],
warn: [
message.toLowerCase().includes('aborting'),
message.toLowerCase().includes('didn\'t gain')
]
}
// Formatted string with chalk for terminal logging
const str = `[${currentTime}] [PID: ${process.pid}] [${type.toUpperCase()}] ${chalkedPlatform} [${title}] ${message}`
// Check if the current log type and message meet the NTFY conditions
try {
if (type in ntfyConditions && ntfyConditions[type as keyof typeof ntfyConditions].some(condition => condition)) {
// Fire-and-forget
Promise.resolve(Ntfy(cleanStr, type)).catch(() => { /* ignore ntfy errors */ })
}
} catch { /* ignore */ }
// Console output with better formatting
const typeIndicator = type === 'error' ? '✗' : type === 'warn' ? '⚠' : '●'
const platformColor = isMobile === 'main' ? chalk.cyan : isMobile ? chalk.blue : chalk.magenta
const typeColor = type === 'error' ? chalk.red : type === 'warn' ? chalk.yellow : chalk.green
const formattedStr = [
chalk.gray(`[${currentTime}]`),
chalk.gray(`[${process.pid}]`),
typeColor(`${typeIndicator} ${type.toUpperCase()}`),
platformColor(`[${platformText}]`),
chalk.bold(`[${title}]`),
redact(message)
].join(' ')
const applyChalk = color && typeof chalk[color] === 'function' ? chalk[color] as (msg: string) => string : null
// Log based on the type
switch (type) {
case 'warn':
applyChalk ? console.warn(applyChalk(str)) : console.warn(str)
applyChalk ? console.warn(applyChalk(formattedStr)) : console.warn(formattedStr)
break
case 'error':
applyChalk ? console.error(applyChalk(str)) : console.error(str)
applyChalk ? console.error(applyChalk(formattedStr)) : console.error(formattedStr)
break
default:
applyChalk ? console.log(applyChalk(str)) : console.log(str)
applyChalk ? console.log(applyChalk(formattedStr)) : console.log(formattedStr)
break
}
}
// Return an Error when logging an error so callers can `throw log(...)`
if (type === 'error') {
// CommunityReporter disabled per project policy
return new Error(cleanStr)
}
}

33
src/util/Ntfy.ts Normal file
View File

@@ -0,0 +1,33 @@
import { loadConfig } from './Load'
import axios from 'axios'
const NOTIFICATION_TYPES = {
error: { priority: 'max', tags: 'rotating_light' }, // Customize the ERROR icon here, see: https://docs.ntfy.sh/emojis/
warn: { priority: 'high', tags: 'warning' }, // Customize the WARN icon here, see: https://docs.ntfy.sh/emojis/
log: { priority: 'default', tags: 'medal_sports' } // Customize the LOG icon here, see: https://docs.ntfy.sh/emojis/
}
export async function Ntfy(message: string, type: keyof typeof NOTIFICATION_TYPES = 'log'): Promise<void> {
const config = loadConfig().ntfy
if (!config?.enabled || !config.url || !config.topic) return
try {
const { priority, tags } = NOTIFICATION_TYPES[type]
const headers = {
Title: 'Microsoft Rewards Script',
Priority: priority,
Tags: tags,
...(config.authToken && { Authorization: `Bearer ${config.authToken}` })
}
const response = await axios.post(`${config.url}/${config.topic}`, message, { headers })
if (response.status === 200) {
console.log('NTFY notification successfully sent.')
} else {
console.error(`NTFY notification failed with status ${response.status}`)
}
} catch (error) {
console.error('Failed to send NTFY notification:', error)
}
}

63
src/util/Retry.ts Normal file
View File

@@ -0,0 +1,63 @@
import type { ConfigRetryPolicy } from '../interface/Config'
import Util from './Utils'
type NumericPolicy = {
maxAttempts: number
baseDelay: number
maxDelay: number
multiplier: number
jitter: number
}
export type Retryable<T> = () => Promise<T>
export class Retry {
private policy: NumericPolicy
constructor(policy?: ConfigRetryPolicy) {
const def: NumericPolicy = {
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 30000,
multiplier: 2,
jitter: 0.2
}
const merged: ConfigRetryPolicy = { ...(policy || {}) }
// normalize string durations
const util = new Util()
const parse = (v: number | string) => {
if (typeof v === 'number') return v
try { return util.stringToMs(String(v)) } catch { return def.baseDelay }
}
this.policy = {
maxAttempts: (merged.maxAttempts as number) ?? def.maxAttempts,
baseDelay: parse(merged.baseDelay ?? def.baseDelay),
maxDelay: parse(merged.maxDelay ?? def.maxDelay),
multiplier: (merged.multiplier as number) ?? def.multiplier,
jitter: (merged.jitter as number) ?? def.jitter
}
}
async run<T>(fn: Retryable<T>, isRetryable?: (e: unknown) => boolean): Promise<T> {
let attempt = 0
let delay = this.policy.baseDelay
let lastErr: unknown
while (attempt < this.policy.maxAttempts) {
try {
return await fn()
} catch (e) {
lastErr = e
attempt += 1
const retry = isRetryable ? isRetryable(e) : true
if (!retry || attempt >= this.policy.maxAttempts) break
const jitter = 1 + (Math.random() * 2 - 1) * this.policy.jitter
const sleep = Math.min(this.policy.maxDelay, Math.max(0, Math.floor(delay * jitter)))
await new Promise((r) => setTimeout(r, sleep))
delay = Math.min(this.policy.maxDelay, Math.floor(delay * (this.policy.multiplier || 2)))
}
}
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr))
}
}
export default Retry

84
src/util/Totp.ts Normal file
View File

@@ -0,0 +1,84 @@
import crypto from 'crypto'
/**
* Decode Base32 (RFC 4648) to a Buffer.
* Accepts lowercase/uppercase, optional padding.
*/
function base32Decode(input: string): Buffer {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
const clean = input.toUpperCase().replace(/=+$/g, '').replace(/[^A-Z2-7]/g, '')
let bits = 0
let value = 0
const bytes: number[] = []
for (const char of clean) {
const idx = alphabet.indexOf(char)
if (idx < 0) continue
value = (value << 5) | idx
bits += 5
if (bits >= 8) {
bits -= 8
bytes.push((value >>> bits) & 0xff)
}
}
return Buffer.from(bytes)
}
/**
* Generate an HMAC using Node's crypto and return Buffer.
*/
function hmac(algorithm: string, key: Buffer, data: Buffer): Buffer {
return crypto.createHmac(algorithm, key).update(data).digest()
}
export type TotpOptions = { digits?: number; step?: number; algorithm?: 'SHA1' | 'SHA256' | 'SHA512' }
/**
* Generate TOTP per RFC 6238.
* @param secretBase32 - shared secret in Base32
* @param time - Unix time in seconds (defaults to now)
* @param options - { digits, step, algorithm }
* @returns numeric TOTP as string (zero-padded)
*/
export function generateTOTP(
secretBase32: string,
time: number = Math.floor(Date.now() / 1000),
options?: TotpOptions
): string {
const digits = options?.digits ?? 6
const step = options?.step ?? 30
const alg = (options?.algorithm ?? 'SHA1').toUpperCase()
const key = base32Decode(secretBase32)
const counter = Math.floor(time / step)
// 8-byte big-endian counter
const counterBuffer = Buffer.alloc(8)
counterBuffer.writeBigUInt64BE(BigInt(counter), 0)
let hmacAlg: string
if (alg === 'SHA1') hmacAlg = 'sha1'
else if (alg === 'SHA256') hmacAlg = 'sha256'
else if (alg === 'SHA512') hmacAlg = 'sha512'
else throw new Error('Unsupported algorithm. Use SHA1, SHA256 or SHA512.')
const hash = hmac(hmacAlg, key, counterBuffer)
if (!hash || hash.length < 20) {
// Minimal sanity check; for SHA1 length is 20
throw new Error('Invalid HMAC output for TOTP')
}
// Dynamic truncation
const offset = hash[hash.length - 1]! & 0x0f
if (offset + 3 >= hash.length) {
throw new Error('Invalid dynamic truncation offset')
}
const code =
((hash[offset]! & 0x7f) << 24) |
((hash[offset + 1]! & 0xff) << 16) |
((hash[offset + 2]! & 0xff) << 8) |
(hash[offset + 3]! & 0xff)
const otp = (code % 10 ** digits).toString().padStart(digits, '0')
return otp
}

View File

@@ -8,6 +8,11 @@ export default class Util {
})
}
async waitRandom(minMs: number, maxMs: number): Promise<void> {
const delta = this.randomNumber(minMs, maxMs)
return this.wait(delta)
}
getFormattedDate(ms = Date.now()): string {
const today = new Date(ms)
const month = String(today.getMonth() + 1).padStart(2, '0') // January is 0

View File

@@ -1,23 +0,0 @@
import axios from 'axios'
import { Config } from '../interface/Config'
export async function Webhook(configData: Config, content: string) {
const webhook = configData.webhook
if (!webhook.enabled || webhook.url.length < 10) return
const request = {
method: 'POST',
url: webhook.url,
headers: {
'Content-Type': 'application/json'
},
data: {
'content': content
}
}
await axios(request).catch(() => { })
}

View File

@@ -41,7 +41,6 @@
/* Module Resolution Options */
"moduleResolution":"node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"types": ["node"],
"typeRoots": ["./node_modules/@types"],
// Keep explicit typeRoots to ensure resolution in environments that don't auto-detect before full install.
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
@@ -67,8 +66,6 @@
},
"include": [
"src/**/*.ts",
"src/accounts.json",
"src/config.json",
"src/functions/queries.json"
],
"exclude": [