mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-11 02:46:17 +00:00
V2.1 (#375)
* feat: Implement edge version fetching with retry logic and caching * chore: Update version to 2.1.0 in package.json * fix: Update package version to 2.1.0 and enhance user agent metadata * feat: Enhance 2FA handling with improved TOTP input and submission logic * fix: Refactor getSystemComponents to improve mobile user agent string generation * feat: Add support for cron expressions for advanced scheduling * feat: Improve humanization feature with detailed logging for off-days configuration * feat: Add live log streaming via webhook and enhance logging configuration * fix: Remove unused @types/cron-parser dependency from devDependencies * feat: Add cron-parser dependency and enhance Axios error handling for proxy authentication * feat: Enhance dashboard data retrieval with retry logic and diagnostics capture * feat: Add ready-to-use sample configurations and update configuration settings for better customization * feat: Add buy mode detection and configuration methods for enhanced manual redemption * feat: Migrate configuration from JSON to JSONC format for improved readability and comments support feat: Implement centralized diagnostics capture for better error handling and reporting fix: Update documentation references from config.json to config.jsonc chore: Add .vscode to .gitignore for cleaner project structure refactor: Enhance humanization and diagnostics capture logic in BrowserUtil and Login classes * feat: Reintroduce ambiance declarations for the 'luxon' module to unlock TypeScript * feat: Update search delay settings for improved performance and reliability * feat: Update README and SECURITY documentation for clarity and improved data handling guidelines * Enhance README and SECURITY documentation for Microsoft Rewards Script V2 - Updated README.md to improve structure, add badges, and enhance clarity on features and setup instructions. - Expanded SECURITY.md to provide detailed data handling practices, security guidelines, and best practices for users. - Included sections on data flow, credential management, and responsible use of the automation tool. - Added a security checklist for users to ensure safe practices while using the script. * feat: Réorganiser et enrichir la documentation du README pour une meilleure clarté et accessibilité * feat: Updated and reorganized the README for better presentation and clarity * feat: Revised and simplified the README for better clarity and accessibility * Update README.md
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
sessions/
|
||||
dist/
|
||||
node_modules/
|
||||
.vscode/
|
||||
.github/
|
||||
accounts.json
|
||||
notes
|
||||
|
||||
@@ -235,4 +235,4 @@ This project is for educational purposes only. Use at your own risk. Microsoft m
|
||||
|
||||
---
|
||||
|
||||
<img width="1536" height="1024" alt="msn-rw" src="https://github.com/user-attachments/assets/4e396ab3-5292-4948-9778-7b385d751e4d" />
|
||||

|
||||
|
||||
375
SECURITY.md
375
SECURITY.md
@@ -1,85 +1,368 @@
|
||||
# Security & Privacy Policy
|
||||
# 🔐 Security & Privacy Policy# Security & Privacy Guidelines# 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. 🚫📡
|
||||
<div align="center">
|
||||
|
||||
|
||||
|
||||
**Your data, your control — transparency first**This document describes how the Microsoft Rewards Script V2 handles data, the assumptions we make about your environment, and how to report security concerns. The codebase runs entirely under your control; there is no built-in telemetry or remote service component.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.
|
||||
|
||||
|
||||
|
||||
This document explains how the Microsoft Rewards Script handles your information,
|
||||
|
||||
what we do (and don't do) with it, and how to keep your setup secure.
|
||||
|
||||
---## TL;DR
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Data Flow Summary- We do not collect, phone-home, or exfiltrate your data. No hidden telemetry. 🚫📡
|
||||
|
||||
## 🎯 TL;DR — The Quick Version
|
||||
|
||||
- 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 doesn’t)
|
||||
| Topic | What You Need to Know |
|
||||
|
||||
This is a local automation tool that drives a browser (Playwright) to perform Microsoft Rewards activities. By default:
|
||||
|:------|:----------------------|- Configuration is loaded from local files (`src/config.jsonc`, `src/accounts.json`) or environment variables you supply.- Sessions/cookies are stored locally to reduce re-login friction. 🍪
|
||||
|
||||
- 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.
|
||||
| **📡 Data Collection** | ❌ **None** — No telemetry, no phone-home, no tracking |
|
||||
|
||||
It does not:
|
||||
| **🔒 Credentials** | ✅ Stored **locally only** in `src/accounts.json` or environment variables |- The automation layer drives Playwright browsers to interact with Microsoft Rewards. No other network destinations are contacted unless you configure outbound notifications.- Use at your own risk. Microsoft may take action on accounts that use automation.
|
||||
|
||||
- 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. 🚫🐛
|
||||
| **🍪 Sessions** | ✅ Saved **on your machine** to avoid repeated logins |
|
||||
|
||||
## Data handling and storage
|
||||
| **🔔 Webhooks** | ⚠️ Optional — **You control** what gets sent where |- Diagnosed artifacts (logs, screenshots, HTML snapshots) are written to local folders within the repository.
|
||||
|
||||
- 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.
|
||||
| **🐳 Docker** | ✅ Read-only mounts, no data leaves the container |
|
||||
|
||||
Tip: For Docker, mount a dedicated data volume for sessions and reports so you can manage them easily. 📦
|
||||
| **⚖️ Terms of Service** | ⚠️ Automation **may violate** Microsoft's ToS — use at your own risk |## What this project does (and doesn’t)
|
||||
|
||||
|
||||
|
||||
---We do not collect or transmit your credentials, points totals, or activity history. All state remains on the host executing the script.
|
||||
|
||||
|
||||
|
||||
## 🔍 What This Script DoesThis is a local automation tool that drives a browser (Playwright) to perform Microsoft Rewards activities. By default:
|
||||
|
||||
|
||||
|
||||
This is a **local automation tool** that drives a browser (Playwright) to complete Microsoft Rewards activities on your behalf.---
|
||||
|
||||
|
||||
|
||||
### ✅ What it does:- It reads configuration from local files (e.g., `src/config.jsonc`, `src/accounts.json`).
|
||||
|
||||
- Reads configuration from local files (`src/config.jsonc`, `src/accounts.json`)
|
||||
|
||||
- Stores session data (cookies, fingerprints) locally under `src/browser/`## Stored Data- It can save session data (cookies and optional fingerprints) locally under `./src/browser/<sessionPath>/<email>/`.
|
||||
|
||||
- Optionally sends notifications to endpoints **you configure**
|
||||
|
||||
- Saves diagnostic logs and screenshots to `reports/` when troubleshooting- It can send optional notifications/webhooks if you enable them and provide a URL.
|
||||
|
||||
|
||||
|
||||
### ❌ What it does NOT do:| Item | Location | Notes |
|
||||
|
||||
- Collect or transmit your data to any third-party service by default
|
||||
|
||||
- Include any hidden telemetry or analytics|------|----------|-------|It does not:
|
||||
|
||||
- Store credentials anywhere except where you specify
|
||||
|
||||
- Run background processes you don't know about| Account credentials | `src/accounts.json` or environment variables | Keep this file out of source control. Use `.gitignore` and restrict file permissions. |
|
||||
|
||||
|
||||
|
||||
---| Configuration | `src/config.jsonc` | Comment-friendly JSONC; may contain secrets (webhook URLs, proxies). |- Send your accounts or secrets to any third-party service by default.
|
||||
|
||||
|
||||
|
||||
## 📦 Where Your Data Lives| Sessions & fingerprints | `src/browser/<sessionPath>/` | Contains cookies and optional device fingerprints used for continuity. Safe to delete if you want fresh sessions. |- Embed any “phone-home” or analytics endpoints.
|
||||
|
||||
|
||||
|
||||
| Data Type | Location | Purpose | Security Notes || Diagnostics | `reports/` (when enabled) | Screenshots, HTML, and logs captured for debugging. Review before sharing. |- Include built-in monetization, miners, or adware. 🚫🐛
|
||||
|
||||
|:----------|:---------|:--------|:--------------|
|
||||
|
||||
| **🔑 Credentials** | `src/accounts.json` or env vars | Login automation | **Keep out of Git!** Add to `.gitignore` |
|
||||
|
||||
| **⚙️ Configuration** | `src/config.jsonc` | Runtime settings | May contain webhook URLs — treat as sensitive |
|
||||
|
||||
| **🍪 Sessions** | `src/browser/<sessionPath>/` | Persist logins | Cookies + fingerprints — delete to reset |When running inside Docker, the compose file mounts `./src/accounts.json`, `./src/config.jsonc`, and `./sessions` as read-only or persistent volumes. No data leaves those mounts unless you explicitly configure additional outputs.## Data handling and storage
|
||||
|
||||
| **📊 Reports** | `reports/` folder | Diagnostics | Screenshots/logs — review before sharing |
|
||||
|
||||
| **🐳 Docker volumes** | Container mounts | Same as above | Read-only where possible |
|
||||
|
||||
|
||||
|
||||
------- Accounts: You control the `accounts.json` file. Keep it safe. Consider environment variables or secrets managers in CI/CD.
|
||||
|
||||
|
||||
|
||||
## 🔐 Keeping Your Setup Secure- Sessions: Cookies are stored locally to speed up login. You can delete them anytime by removing the session folder.
|
||||
|
||||
|
||||
|
||||
### 1️⃣ **Protect Your Credentials**## Credentials & Secrets- Fingerprints: If you enable fingerprint saving, they are saved locally only. Disable this feature if you prefer ephemeral fingerprints.
|
||||
|
||||
|
||||
|
||||
```bash- Logs/Reports: Diagnostic artifacts and daily summaries are written to the local `reports/` directory.
|
||||
|
||||
# ✅ DO: Keep accounts.json out of version control
|
||||
|
||||
echo "src/accounts.json" >> .gitignore- Do not commit `src/accounts.json` or any file containing secrets. The sample `.gitignore` already excludes them; verify your local overrides do the same.- Webhooks/Notifications: If enabled, we send only the minimal information necessary (e.g., summary text, embed fields) to the endpoint you configured.
|
||||
|
||||
|
||||
|
||||
# ✅ DO: Use environment variables in CI/CD- If you use TOTP, the Base32 secret remains local and is only used to respond to Microsoft login challenges.
|
||||
|
||||
export ACCOUNTS_JSON='[{"email":"...","password":"..."}]'
|
||||
|
||||
- For CI or scripted deployments, prefer supplying credentials through environment variables (`ACCOUNTS_JSON` or `ACCOUNTS_FILE`).Tip: For Docker, mount a dedicated data volume for sessions and reports so you can manage them easily. 📦
|
||||
|
||||
# ❌ DON'T: Commit secrets to GitHub
|
||||
|
||||
# ❌ DON'T: Share accounts.json publicly- Rotate your Microsoft account passwords and webhook tokens periodically.
|
||||
|
||||
```
|
||||
|
||||
## 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.
|
||||
### 2️⃣ **Secure Your Configuration**
|
||||
|
||||
---
|
||||
|
||||
- **Webhook URLs** in `config.jsonc` are essentially passwords — anyone with the URL can post to your channel
|
||||
|
||||
- **TOTP secrets** stay local and are only used during Microsoft login challenges- Do not commit secrets. Use `src/accounts.json` locally or set `ACCOUNTS_JSON`/`ACCOUNTS_FILE` via environment variables when running in containers.
|
||||
|
||||
- **Proxy credentials** should be treated like passwords
|
||||
|
||||
## Notifications- Consider using OS keychains or external secret managers where possible.
|
||||
|
||||
### 3️⃣ **Session Management**
|
||||
|
||||
- TOTP: If you include a Base32 TOTP secret per account, it remains local and is used strictly during login challenge flows.
|
||||
|
||||
```bash
|
||||
|
||||
# Clear sessions if you suspect compromiseOptional integrations (Discord webhooks, NTFY, others you add) send only the payloads you configure. Review each provider’s privacy policy before enabling. Treat webhook URLs as shared secrets; they allow anyone with the URL to post into the channel.
|
||||
|
||||
rm -rf src/browser/sessions/*
|
||||
|
||||
## Buy Mode safety
|
||||
|
||||
# Or in Docker
|
||||
|
||||
docker compose down -v # Removes volumes---
|
||||
|
||||
```
|
||||
|
||||
Buy Mode opens a monitor tab (read-only points polling) and a separate user tab for your manual actions. The monitor tab doesn’t redeem or click on your behalf — it just reads dashboard data to keep totals up to date. 🛍️
|
||||
|
||||
## Responsible disclosure
|
||||
### 4️⃣ **Docker Best Practices**
|
||||
|
||||
We value coordinated disclosure. If you find a security issue:
|
||||
## Recommended Practices
|
||||
|
||||
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. 🙏
|
||||
The included `compose.yaml` already:
|
||||
|
||||
Please do not open public issues with sensitive details before we have had a chance to remediate.
|
||||
- ✅ Uses read-only mounts for config files## Responsible disclosure
|
||||
|
||||
- ✅ Runs as non-root user where possible
|
||||
|
||||
- ✅ Limits container privileges- Run the script with least privilege. When using Docker, the provided compose file uses non-root execution and read-only mounts where possible.
|
||||
|
||||
|
||||
|
||||
Additional hardening:- Back up `sessions` only if you understand the contents (cookies, fingerprints). Delete the directory if you suspect compromise or want to reset state.We value coordinated disclosure. If you find a security issue:
|
||||
|
||||
```yaml
|
||||
|
||||
security_opt:- Enable the diagnostics bundle (`docs/diagnostics.md`) only when troubleshooting, and scrub artifacts before sharing.
|
||||
|
||||
- no-new-privileges:true
|
||||
|
||||
cap_drop:- Keep dependencies updated (`npm run build` after `npm install`) to receive security patches for Playwright and transitive packages.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.
|
||||
|
||||
- ALL
|
||||
|
||||
```- Review `src/config.jsonc` comments; several fields (e.g., `humanization`, `retryPolicy`) influence how aggressively the automation behaves. Conservative defaults reduce the chance of account flags.2. Provide a minimal reproduction and version info.
|
||||
|
||||
|
||||
|
||||
### 5️⃣ **Regular Maintenance**3. We will acknowledge within a reasonable timeframe and work on a fix. 🙏
|
||||
|
||||
|
||||
|
||||
- 🔄 **Update dependencies:** `npm install && npm run build`---
|
||||
|
||||
- 🔑 **Rotate credentials** periodically
|
||||
|
||||
- 🧹 **Clean diagnostics:** Review and delete `reports/` contentsPlease do not open public issues with sensitive details before we have had a chance to remediate.
|
||||
|
||||
- 🔍 **Monitor logs** for suspicious activity
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
## 🔔 Notifications & Privacy
|
||||
|
||||
## Dependency and update policy
|
||||
If you discover a vulnerability in this project:
|
||||
|
||||
When you enable Discord webhooks or NTFY:
|
||||
|
||||
- The script sends **only the summary data you configure**- This project is open-source and runs on your infrastructure (local machine or container). You are responsible for host hardening and network policies.
|
||||
|
||||
- No credentials or session data is included
|
||||
|
||||
- The receiving service (Discord, NTFY, etc.) has its own privacy policy1. Privately reach out via a GitHub issue tagged “Security” requesting a direct contact channel, or message a maintainer through the listed GitHub profile if available.- Automation can violate terms of service. You assume all responsibility for how you use this tool.
|
||||
|
||||
|
||||
|
||||
**Control what gets sent:**2. Include a minimal reproduction, environment details, and the commit or release you tested.- Browsers and dependencies evolve. Keep the project and your runtime up to date.
|
||||
|
||||
```jsonc
|
||||
|
||||
// In config.jsonc3. Allow a reasonable window for investigation and remediation before publishing details.
|
||||
|
||||
"notifications": {
|
||||
|
||||
"conclusionWebhook": {## Dependency and update policy
|
||||
|
||||
"enabled": false, // Disable to send nothing
|
||||
|
||||
"url": "https://discord.com/api/webhooks/..."We appreciate coordinated disclosure and will credit contributions in the changelog when permitted.
|
||||
|
||||
}
|
||||
|
||||
}- 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.
|
||||
|
||||
---
|
||||
|
||||
- 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
|
||||
## 🛡️ Responsible 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.
|
||||
## Scope & Assumptions
|
||||
|
||||
## Privacy statement
|
||||
### ✅ **Good Practices**
|
||||
|
||||
We don’t 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.
|
||||
- Run with least privileges (avoid root/admin unless needed)## Safe use guidelines
|
||||
|
||||
If you enable third-party notifications (Discord, NTFY, etc.), data sent there is under your control and subject to those services’ privacy policies.
|
||||
- Use environment variables for secrets in production
|
||||
|
||||
## Contact
|
||||
- Keep the repository and dependencies updated- The project runs on infrastructure you control. Host hardening, firewall rules, and secret storage are your responsibility.
|
||||
|
||||
- Review code changes before pulling updates
|
||||
|
||||
- Monitor account health regularly- Automation may violate Microsoft’s terms of service. Use at your own risk; the maintainers are not liable for account actions Microsoft may take.- Run with least privileges. In Docker, prefer non-root where feasible and set `no-new-privileges` if supported.
|
||||
|
||||
|
||||
|
||||
### ⚠️ **Risk Awareness**- Playwright and Chromium evolve quickly. Rebuild after dependency updates and monitor upstream advisories.- Limit outbound network access if your threat model requires it.
|
||||
|
||||
- **Microsoft ToS:** Automation violates their terms — accounts may be suspended
|
||||
|
||||
- **Rate limiting:** Aggressive settings increase ban risk- Rotate credentials periodically and revoke unused secrets.
|
||||
|
||||
- **Shared environments:** Don't run on untrusted machines
|
||||
|
||||
- **Network exposure:** Limit outbound connections if your threat model requires it---- Clean up diagnostics and reports if they contain sensitive metadata.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 🐛 Vulnerability Reporting## Contact## Privacy statement
|
||||
|
||||
|
||||
|
||||
We value security research and coordinated disclosure.
|
||||
|
||||
|
||||
|
||||
### 📧 **How to Report**Open a GitHub issue labeled “Security” or reach out to the repository owner if you require a private communication channel. Provide as much diagnostic context as you can share safely.We don’t 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.
|
||||
|
||||
|
||||
|
||||
1. **Privately open a GitHub issue** labeled "Security"
|
||||
|
||||
2. **Include:**
|
||||
|
||||
- Description of the vulnerabilityStay safe, and automate responsibly.If you enable third-party notifications (Discord, NTFY, etc.), data sent there is under your control and subject to those services’ privacy policies.
|
||||
|
||||
- Steps to reproduce
|
||||
|
||||
- Affected versions/commits
|
||||
|
||||
- Suggested remediation (if any)## Contact
|
||||
|
||||
3. **Give us time** to investigate and patch before public disclosure
|
||||
|
||||
To report a security issue or ask a question, please open an issue with the “Security” label and we’ll 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! ✨
|
||||
### 🏆 **Recognition**
|
||||
|
||||
Security contributors will be credited in the changelog (with permission).— Stay safe and have fun automating! ✨
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 📋 Security Checklist
|
||||
|
||||
<details>
|
||||
<summary><strong>🔒 Click to expand the complete security checklist</strong></summary>
|
||||
|
||||
- [ ] `src/accounts.json` is in `.gitignore`
|
||||
- [ ] File permissions restrict access to sensitive configs
|
||||
- [ ] Using TOTP for 2FA (reduces password-only exposure)
|
||||
- [ ] Webhook URLs treated as secrets
|
||||
- [ ] Sessions folder backed up securely (if at all)
|
||||
- [ ] Running with minimal privileges
|
||||
- [ ] Docker using read-only mounts where possible
|
||||
- [ ] Dependencies updated regularly
|
||||
- [ ] Diagnostic reports reviewed before sharing
|
||||
- [ ] Monitoring for unusual account activity
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 📞 Contact
|
||||
|
||||
- **Security issues:** Open a GitHub issue with "Security" label
|
||||
- **General support:** [Discord community](https://discord.gg/KRBFxxsU)
|
||||
- **Bug reports:** [GitHub Issues](https://github.com/TheNetsky/Microsoft-Rewards-Script/issues)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Stay safe, automate responsibly** ✨
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2025*
|
||||
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ 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/accounts.json:ro
|
||||
- ./src/config.json:/usr/src/microsoft-rewards-script/config.json:ro
|
||||
- ./src/config.jsonc:/usr/src/microsoft-rewards-script/config.json:ro
|
||||
- ./sessions:/usr/src/microsoft-rewards-script/sessions # Optional, saves your login session
|
||||
|
||||
environment:
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
Buy Mode allows you to **manually redeem rewards** while the script **passively monitors** your point balance. Perfect for safe redemptions without automation interference.
|
||||
|
||||
> ℹ️ Buy Mode automatically launches the browser in a visible window (headless=false) so you can interact with captchas and checkout flows. Use `FORCE_HEADLESS=1` only if you understand the limitations.
|
||||
|
||||
### **Key Features**
|
||||
- 👀 **Passive monitoring** — No clicks or automation
|
||||
- 🔄 **Real-time tracking** — Instant spending alerts
|
||||
@@ -55,7 +57,7 @@ npm start -- -buy
|
||||
## ⚙️ Configuration
|
||||
|
||||
### **Set Duration in Config**
|
||||
Add to `src/config.json`:
|
||||
Add to `src/config.jsonc`:
|
||||
```json
|
||||
{
|
||||
"buyMode": {
|
||||
@@ -201,6 +203,6 @@ During monitoring:
|
||||
## Troubleshooting
|
||||
|
||||
- **Monitor tab closed**: Script automatically reopens it in background
|
||||
- **No notifications**: Check webhook/NTFY configuration in `config.json`
|
||||
- **No notifications**: Check webhook/NTFY configuration in `config.jsonc`
|
||||
- **Session timeout**: Increase `maxMinutes` if you need longer monitoring
|
||||
- **Login issues**: Ensure account credentials are correct in `accounts.json`
|
||||
|
||||
147
docs/config-presets/balanced.json
Normal file
147
docs/config-presets/balanced.json
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
// Base URL for Rewards dashboard and APIs
|
||||
"baseURL": "https://rewards.bing.com",
|
||||
// Where to store sessions (cookies, fingerprints)
|
||||
"sessionPath": "sessions",
|
||||
|
||||
"browser": {
|
||||
// Headless mode is more stable on shared servers
|
||||
"headless": true,
|
||||
// Use short notation for readability
|
||||
"globalTimeout": "30s"
|
||||
},
|
||||
|
||||
"execution": {
|
||||
"parallel": false,
|
||||
"runOnZeroPoints": false,
|
||||
"clusters": 1,
|
||||
"passesPerRun": 1
|
||||
},
|
||||
|
||||
"buyMode": {
|
||||
"maxMinutes": 45
|
||||
},
|
||||
|
||||
"fingerprinting": {
|
||||
"saveFingerprint": {
|
||||
"mobile": true,
|
||||
"desktop": true
|
||||
}
|
||||
},
|
||||
|
||||
"search": {
|
||||
"useLocalQueries": true,
|
||||
"settings": {
|
||||
"useGeoLocaleQueries": true,
|
||||
"scrollRandomResults": true,
|
||||
"clickRandomResults": true,
|
||||
"retryMobileSearchAmount": 2,
|
||||
"delay": {
|
||||
"min": "8s",
|
||||
"max": "22s"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"humanization": {
|
||||
"enabled": true,
|
||||
"stopOnBan": true,
|
||||
"immediateBanAlert": true,
|
||||
"actionDelay": {
|
||||
"min": 500,
|
||||
"max": 2200
|
||||
},
|
||||
"gestureMoveProb": 0.65,
|
||||
"gestureScrollProb": 0.4,
|
||||
"allowedWindows": []
|
||||
},
|
||||
|
||||
"vacation": {
|
||||
"enabled": true,
|
||||
"minDays": 2,
|
||||
"maxDays": 4
|
||||
},
|
||||
|
||||
"retryPolicy": {
|
||||
"maxAttempts": 3,
|
||||
"baseDelay": 1000,
|
||||
"maxDelay": "30s",
|
||||
"multiplier": 2,
|
||||
"jitter": 0.2
|
||||
},
|
||||
|
||||
"workers": {
|
||||
"doDailySet": true,
|
||||
"doMorePromotions": true,
|
||||
"doPunchCards": true,
|
||||
"doDesktopSearch": true,
|
||||
"doMobileSearch": true,
|
||||
"doDailyCheckIn": true,
|
||||
"doReadToEarn": true,
|
||||
"bundleDailySetWithSearch": true
|
||||
},
|
||||
|
||||
"proxy": {
|
||||
"proxyGoogleTrends": true,
|
||||
"proxyBingTerms": true
|
||||
},
|
||||
|
||||
"notifications": {
|
||||
"webhook": {
|
||||
"enabled": false,
|
||||
"url": "***"
|
||||
},
|
||||
"conclusionWebhook": {
|
||||
"enabled": false,
|
||||
"url": "***"
|
||||
},
|
||||
"ntfy": {
|
||||
"enabled": false,
|
||||
"url": "",
|
||||
"topic": "rewards",
|
||||
"authToken": ""
|
||||
}
|
||||
},
|
||||
|
||||
"logging": {
|
||||
"excludeFunc": [
|
||||
"SEARCH-CLOSE-TABS",
|
||||
"LOGIN-NO-PROMPT",
|
||||
"FLOW"
|
||||
],
|
||||
"webhookExcludeFunc": [
|
||||
"SEARCH-CLOSE-TABS",
|
||||
"LOGIN-NO-PROMPT",
|
||||
"FLOW"
|
||||
],
|
||||
"redactEmails": true
|
||||
},
|
||||
|
||||
"diagnostics": {
|
||||
"enabled": true,
|
||||
"saveScreenshot": true,
|
||||
"saveHtml": true,
|
||||
"maxPerRun": 2,
|
||||
"retentionDays": 7
|
||||
},
|
||||
|
||||
"jobState": {
|
||||
"enabled": true,
|
||||
"dir": ""
|
||||
},
|
||||
|
||||
"schedule": {
|
||||
"enabled": false,
|
||||
"useAmPm": false,
|
||||
"time12": "9:00 AM",
|
||||
"time24": "09:00",
|
||||
"timeZone": "America/New_York",
|
||||
"runImmediatelyOnStart": false
|
||||
},
|
||||
|
||||
"update": {
|
||||
"git": true,
|
||||
"docker": false,
|
||||
"scriptPath": "setup/update/update.mjs"
|
||||
}
|
||||
}
|
||||
147
docs/config-presets/balanced.jsonc
Normal file
147
docs/config-presets/balanced.jsonc
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
// Base URL for Rewards dashboard and APIs
|
||||
"baseURL": "https://rewards.bing.com",
|
||||
// Where to store sessions (cookies, fingerprints)
|
||||
"sessionPath": "sessions",
|
||||
|
||||
"browser": {
|
||||
// Headless mode is more stable on shared servers
|
||||
"headless": true,
|
||||
// Use short notation for readability
|
||||
"globalTimeout": "30s"
|
||||
},
|
||||
|
||||
"execution": {
|
||||
"parallel": false,
|
||||
"runOnZeroPoints": false,
|
||||
"clusters": 1,
|
||||
"passesPerRun": 1
|
||||
},
|
||||
|
||||
"buyMode": {
|
||||
"maxMinutes": 45
|
||||
},
|
||||
|
||||
"fingerprinting": {
|
||||
"saveFingerprint": {
|
||||
"mobile": true,
|
||||
"desktop": true
|
||||
}
|
||||
},
|
||||
|
||||
"search": {
|
||||
"useLocalQueries": true,
|
||||
"settings": {
|
||||
"useGeoLocaleQueries": true,
|
||||
"scrollRandomResults": true,
|
||||
"clickRandomResults": true,
|
||||
"retryMobileSearchAmount": 2,
|
||||
"delay": {
|
||||
"min": "8s",
|
||||
"max": "22s"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"humanization": {
|
||||
"enabled": true,
|
||||
"stopOnBan": true,
|
||||
"immediateBanAlert": true,
|
||||
"actionDelay": {
|
||||
"min": 500,
|
||||
"max": 2200
|
||||
},
|
||||
"gestureMoveProb": 0.65,
|
||||
"gestureScrollProb": 0.4,
|
||||
"allowedWindows": []
|
||||
},
|
||||
|
||||
"vacation": {
|
||||
"enabled": true,
|
||||
"minDays": 2,
|
||||
"maxDays": 4
|
||||
},
|
||||
|
||||
"retryPolicy": {
|
||||
"maxAttempts": 3,
|
||||
"baseDelay": 1000,
|
||||
"maxDelay": "30s",
|
||||
"multiplier": 2,
|
||||
"jitter": 0.2
|
||||
},
|
||||
|
||||
"workers": {
|
||||
"doDailySet": true,
|
||||
"doMorePromotions": true,
|
||||
"doPunchCards": true,
|
||||
"doDesktopSearch": true,
|
||||
"doMobileSearch": true,
|
||||
"doDailyCheckIn": true,
|
||||
"doReadToEarn": true,
|
||||
"bundleDailySetWithSearch": true
|
||||
},
|
||||
|
||||
"proxy": {
|
||||
"proxyGoogleTrends": true,
|
||||
"proxyBingTerms": true
|
||||
},
|
||||
|
||||
"notifications": {
|
||||
"webhook": {
|
||||
"enabled": false,
|
||||
"url": "***"
|
||||
},
|
||||
"conclusionWebhook": {
|
||||
"enabled": false,
|
||||
"url": "***"
|
||||
},
|
||||
"ntfy": {
|
||||
"enabled": false,
|
||||
"url": "",
|
||||
"topic": "rewards",
|
||||
"authToken": ""
|
||||
}
|
||||
},
|
||||
|
||||
"logging": {
|
||||
"excludeFunc": [
|
||||
"SEARCH-CLOSE-TABS",
|
||||
"LOGIN-NO-PROMPT",
|
||||
"FLOW"
|
||||
],
|
||||
"webhookExcludeFunc": [
|
||||
"SEARCH-CLOSE-TABS",
|
||||
"LOGIN-NO-PROMPT",
|
||||
"FLOW"
|
||||
],
|
||||
"redactEmails": true
|
||||
},
|
||||
|
||||
"diagnostics": {
|
||||
"enabled": true,
|
||||
"saveScreenshot": true,
|
||||
"saveHtml": true,
|
||||
"maxPerRun": 2,
|
||||
"retentionDays": 7
|
||||
},
|
||||
|
||||
"jobState": {
|
||||
"enabled": true,
|
||||
"dir": ""
|
||||
},
|
||||
|
||||
"schedule": {
|
||||
"enabled": false,
|
||||
"useAmPm": false,
|
||||
"time12": "9:00 AM",
|
||||
"time24": "09:00",
|
||||
"timeZone": "America/New_York",
|
||||
"runImmediatelyOnStart": false
|
||||
},
|
||||
|
||||
"update": {
|
||||
"git": true,
|
||||
"docker": false,
|
||||
"scriptPath": "setup/update/update.mjs"
|
||||
}
|
||||
}
|
||||
120
docs/config-presets/minimal.jsonc
Normal file
120
docs/config-presets/minimal.jsonc
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"baseURL": "https://rewards.bing.com",
|
||||
"sessionPath": "sessions",
|
||||
"browser": {
|
||||
"headless": true,
|
||||
"globalTimeout": "45s"
|
||||
},
|
||||
"execution": {
|
||||
"parallel": false,
|
||||
"runOnZeroPoints": true,
|
||||
"clusters": 1,
|
||||
"passesPerRun": 1
|
||||
},
|
||||
"fingerprinting": {
|
||||
"saveFingerprint": {
|
||||
"mobile": false,
|
||||
"desktop": false
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"useLocalQueries": false,
|
||||
"settings": {
|
||||
"useGeoLocaleQueries": false,
|
||||
"scrollRandomResults": true,
|
||||
"clickRandomResults": true,
|
||||
"retryMobileSearchAmount": 1,
|
||||
"delay": {
|
||||
"min": "6s",
|
||||
"max": "15s"
|
||||
}
|
||||
}
|
||||
},
|
||||
"humanization": {
|
||||
"enabled": true,
|
||||
"stopOnBan": true,
|
||||
"immediateBanAlert": true,
|
||||
"actionDelay": {
|
||||
"min": 200,
|
||||
"max": 750
|
||||
},
|
||||
"gestureMoveProb": 0.5,
|
||||
"gestureScrollProb": 0.25,
|
||||
"allowedWindows": []
|
||||
},
|
||||
"vacation": {
|
||||
"enabled": false,
|
||||
"minDays": 2,
|
||||
"maxDays": 3
|
||||
},
|
||||
"retryPolicy": {
|
||||
"maxAttempts": 3,
|
||||
"baseDelay": 1000,
|
||||
"maxDelay": "30s",
|
||||
"multiplier": 2,
|
||||
"jitter": 0.2
|
||||
},
|
||||
"workers": {
|
||||
"doDailySet": true,
|
||||
"doMorePromotions": true,
|
||||
"doPunchCards": true,
|
||||
"doDesktopSearch": true,
|
||||
"doMobileSearch": true,
|
||||
"doDailyCheckIn": true,
|
||||
"doReadToEarn": true,
|
||||
"bundleDailySetWithSearch": false
|
||||
},
|
||||
"proxy": {
|
||||
"proxyGoogleTrends": true,
|
||||
"proxyBingTerms": true
|
||||
},
|
||||
"notifications": {
|
||||
"webhook": {
|
||||
"enabled": false,
|
||||
"url": ""
|
||||
},
|
||||
"conclusionWebhook": {
|
||||
"enabled": false,
|
||||
"url": ""
|
||||
},
|
||||
"ntfy": {
|
||||
"enabled": false,
|
||||
"url": "",
|
||||
"topic": "rewards",
|
||||
"authToken": ""
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"excludeFunc": [
|
||||
"FLOW"
|
||||
],
|
||||
"webhookExcludeFunc": [
|
||||
"FLOW"
|
||||
],
|
||||
"redactEmails": true
|
||||
},
|
||||
"diagnostics": {
|
||||
"enabled": true,
|
||||
"saveScreenshot": true,
|
||||
"saveHtml": false,
|
||||
"maxPerRun": 1,
|
||||
"retentionDays": 5
|
||||
},
|
||||
"jobState": {
|
||||
"enabled": true,
|
||||
"dir": ""
|
||||
},
|
||||
"schedule": {
|
||||
"enabled": false,
|
||||
"useAmPm": false,
|
||||
"time12": "8:30 AM",
|
||||
"time24": "08:30",
|
||||
"timeZone": "UTC",
|
||||
"runImmediatelyOnStart": true
|
||||
},
|
||||
"update": {
|
||||
"git": true,
|
||||
"docker": false,
|
||||
"scriptPath": "setup/update/update.mjs"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
# ⚙️ 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.
|
||||
This page documents every field in the configuration file. The default ships as `src/config.jsonc` so you get inline `//` guidance without editor warnings, and the loader still accepts traditional `config.json` files if you prefer plain JSON.
|
||||
|
||||
Looking for ready-to-use presets? Check `docs/config-presets/` for curated examples such as `balanced.jsonc` (full automation with humanization) and `minimal.jsonc` (lean runs with quick scheduling).
|
||||
|
||||
> NOTE: Previous versions had `logging.live` (live streaming webhook); it was removed and replaced by a simple `logging.redactEmails` flag.
|
||||
|
||||
@@ -17,7 +19,7 @@ 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. |
|
||||
| headless | boolean | false | Run browser UI-less. Set to `false` to keep the browser visible (default). |
|
||||
| globalTimeout | string/number | "30s" | Max time for common Playwright operations. Accepts ms number or time string (e.g. `"45s"`, `"2min"`). |
|
||||
|
||||
---
|
||||
@@ -122,7 +124,7 @@ Manages notification channels (Discord webhooks, NTFY, etc.).
|
||||
Primary webhook (can be used for summary or generic messages).
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| enabled | false | Allow sending webhook-based notifications. |
|
||||
| enabled | false | Allow sending webhook-based notifications and live log streaming. |
|
||||
| url | "" | Webhook endpoint. |
|
||||
|
||||
### notifications.conclusionWebhook
|
||||
@@ -148,6 +150,7 @@ Lightweight push notifications.
|
||||
| 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. |
|
||||
| liveWebhookUrl | string | Optional override URL for live log streaming (falls back to `notifications.webhook.url`). |
|
||||
|
||||
_Removed fields_: `live.enabled`, `live.url`, `live.redactEmails` — replaced by `redactEmails` only.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ The diagnostics system **automatically captures** error screenshots and HTML sna
|
||||
## ⚙️ Configuration
|
||||
|
||||
### **Basic Setup**
|
||||
Add to `src/config.json`:
|
||||
Add to `src/config.jsonc`:
|
||||
```json
|
||||
{
|
||||
"diagnostics": {
|
||||
@@ -82,6 +82,8 @@ reports/
|
||||
└── ...
|
||||
```
|
||||
|
||||
> 🔐 Security incidents (login blocks, recovery mismatches) are stored separately under `diagnostics/security-incidents/<timestamp>-slug/` and always include both a screenshot and HTML snapshot for investigation.
|
||||
|
||||
### **File Naming Convention**
|
||||
```
|
||||
error_[runId]_[sequence].[ext]
|
||||
|
||||
@@ -9,77 +9,82 @@
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
## 🚀 Quick Start Checklist
|
||||
|
||||
### **Prerequisites**
|
||||
- ✅ `src/accounts.json` configured with your Microsoft accounts
|
||||
- ✅ `src/config.json` exists (uses defaults if not customized)
|
||||
- ✅ Docker & Docker Compose installed
|
||||
1. `src/accounts.json` populated with your Microsoft credentials
|
||||
2. `src/config.jsonc` present (defaults are fine; comments stay intact)
|
||||
3. Docker + Docker Compose installed locally (Desktop app or CLI)
|
||||
|
||||
### **Launch**
|
||||
```bash
|
||||
# Build and start the container
|
||||
# Build and start the container (scheduler runs automatically)
|
||||
docker compose up -d
|
||||
|
||||
# Monitor the automation
|
||||
# Stream logs from the running container
|
||||
docker logs -f microsoft-rewards-script
|
||||
|
||||
# Stop when needed
|
||||
# Stop the stack when you are done
|
||||
docker compose down
|
||||
```
|
||||
|
||||
**That's it!** The container runs the built-in scheduler automatically.uide
|
||||
The compose file uses the same Playwright build as local runs but forces headless mode inside the container via `FORCE_HEADLESS=1`, matching the bundled image.
|
||||
|
||||
This project ships with a Docker setup tailored for headless runs. It uses Playwright’s 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`
|
||||
## 📦 What the Compose File Mounts
|
||||
|
||||
## Volumes & Files
|
||||
The compose file mounts:
|
||||
- `./src/accounts.json` → `/usr/src/microsoft-rewards-script/accounts.json` (read‑only)
|
||||
- `./src/config.json` → `/usr/src/microsoft-rewards-script/config.json` (read‑only)
|
||||
- `./sessions` → `/usr/src/microsoft-rewards-script/sessions` (persist login sessions)
|
||||
| Host path | Container path | Purpose |
|
||||
|-----------|----------------|---------|
|
||||
| `./src/accounts.json` | `/usr/src/microsoft-rewards-script/accounts.json` | Read-only account credentials |
|
||||
| `./src/config.jsonc` | `/usr/src/microsoft-rewards-script/config.json` | Read-only runtime configuration |
|
||||
| `./sessions` | `/usr/src/microsoft-rewards-script/sessions` | Persisted cookies & fingerprints |
|
||||
|
||||
You can also use env overrides supported by the app loader:
|
||||
- `ACCOUNTS_FILE=/path/to/accounts.json`
|
||||
- `ACCOUNTS_JSON='[ {"email":"...","password":"..."} ]'`
|
||||
Prefer environment variables? The loader accepts the same overrides as local runs:
|
||||
|
||||
## 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):
|
||||
```bash
|
||||
ACCOUNTS_FILE=/custom/accounts.json
|
||||
ACCOUNTS_JSON='[{"email":"name@example.com","password":"hunter2"}]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌍 Useful Environment Variables
|
||||
|
||||
- `TZ` — set container timezone (`Europe/Paris`, `America/New_York`, etc.)
|
||||
- `NODE_ENV=production` — default; keeps builds lean
|
||||
- `FORCE_HEADLESS=1` — required in Docker (Chromium Headless Shell only)
|
||||
- Scheduler tuning (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.
|
||||
## 🧠 Browser Footprint
|
||||
|
||||
## One‑shot vs. Scheduler
|
||||
- Default command runs the built‑in scheduler: `npm run start:schedule`
|
||||
- For one‑shot run, override the command:
|
||||
- `docker run --rm ... node ./dist/index.js`
|
||||
The Docker image installs Chromium Headless Shell via `npx playwright install --with-deps --only-shell`. This keeps the image compact while retaining Chromium’s Edge-compatible user agent. Installing full Edge or all browsers roughly doubles the footprint and adds instability, so we ship only the shell.
|
||||
|
||||
## Tips
|
||||
- If you see 2FA prompts, add your TOTP Base32 secret to `accounts.json` so the bot can auto‑fill codes.
|
||||
- Use a persistent `sessions` volume to avoid re‑logging every run.
|
||||
- For proxies per account, fill the `proxy` block in your `accounts.json` (see [Proxy](./proxy.md)).
|
||||
---
|
||||
|
||||
## 🔁 Alternate Commands
|
||||
|
||||
- **Default:** `npm run start:schedule` (inside container) — keeps the scheduler alive
|
||||
- **Single pass:** `docker compose run --rm app node ./dist/index.js`
|
||||
- **Custom script:** Override `command:` in `compose.yaml` to suit your workflow
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips
|
||||
|
||||
- Add TOTP secrets to `accounts.json` so the bot can respond to MFA prompts automatically
|
||||
- Keep the `sessions` volume; deleting it forces fresh logins and can trigger security reviews
|
||||
- Mixing proxies? Configure per-account proxies in `accounts.json` (see [Proxy Setup](./proxy.md))
|
||||
- Want notifications? Layer [NTFY](./ntfy.md) or [Discord Webhooks](./conclusionwebhook.md) on top once the container is stable
|
||||
|
||||
---
|
||||
|
||||
## 🔗 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
|
||||
- **[Getting Started](./getting-started.md)** — Prep work before switching to containers
|
||||
- **[Accounts & 2FA](./accounts.md)** — Ensure every account can pass MFA headlessly
|
||||
- **[Scheduler](./schedule.md)** — If you prefer a host-side cron instead of Docker
|
||||
- **[Diagnostics](./diagnostics.md)** — Capture logs and debug a failing container
|
||||
@@ -93,7 +93,7 @@ The script will automatically:
|
||||
If you prefer containers:
|
||||
|
||||
```bash
|
||||
# Ensure accounts.json and config.json exist
|
||||
# Ensure accounts.json and config.jsonc exist
|
||||
docker compose up -d
|
||||
|
||||
# Follow logs
|
||||
|
||||
@@ -57,7 +57,7 @@ Human Mode adds **subtle human-like behavior** to make your automation look and
|
||||
| `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 |
|
||||
| `randomOffDaysPerWeek` | `1` | Skip N random days per week. Set to `0` to disable (scheduler logs reference this setting explicitly). |
|
||||
|
||||
---
|
||||
|
||||
@@ -72,6 +72,7 @@ Human Mode adds **subtle human-like behavior** to make your automation look and
|
||||
- **Micro mouse moves** — Tiny cursor adjustments (safe zones only)
|
||||
- **Minor scrolls** — Small page movements (non-interactive areas)
|
||||
- **Probability-based** — Not every action includes gestures
|
||||
- **Centralized controller** — The `Humanizer` service now drives all gesture + pause behavior so every module uses the same probabilities and timing windows.
|
||||
|
||||
### **Temporal Patterns**
|
||||
- **Time windows** — Only run during specified hours
|
||||
|
||||
108
docs/index.md
108
docs/index.md
@@ -1,9 +1,9 @@
|
||||
# 📚 Microsoft Rewards Script V2 Documentation
|
||||
# 📚 Microsoft Rewards Script V2 Docs
|
||||
|
||||
<div align="center">
|
||||
|
||||
**🎯 Your complete guide to automating Microsoft Rewards**
|
||||
*Everything you need to get started and master the script*
|
||||
**🎯 Your companion for mastering the automation stack**
|
||||
*Curated guides, verified against the current codebase*
|
||||
|
||||
</div>
|
||||
|
||||
@@ -12,85 +12,51 @@
|
||||
## 🚀 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 |
|
||||
| Guide | Why you should read it |
|
||||
|-------|------------------------|
|
||||
| **[🎬 Getting Started](./getting-started.md)** | Install, configure, and run the bot in minutes |
|
||||
| **[👤 Accounts & 2FA](./accounts.md)** | Add Microsoft accounts, enable TOTP, and secure logins |
|
||||
| **[⚙️ Configuration Reference](./config.md)** | Understand every option in `src/config.jsonc` |
|
||||
|
||||
### **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 |
|
||||
### **Run & Operate**
|
||||
| Guide | Focus |
|
||||
|-------|-------|
|
||||
| **[⏰ Scheduling](./schedule.md)** | Cron-style automation and daily cadence |
|
||||
| **[🐳 Docker](./docker.md)** | Container deployment with prewired headless settings |
|
||||
| **[🛠️ Diagnostics](./diagnostics.md)** | Troubleshooting, log capture, and support checklist |
|
||||
| **[🧠 Humanization](./humanization.md)** | Natural browser behavior and ban avoidance |
|
||||
| **[🌐 Proxy Setup](./proxy.md)** | Per-account proxy routing and geo-tuning |
|
||||
| **[📊 Job State](./jobstate.md)** | How runs persist progress and recover |
|
||||
| **[🔄 Auto Update](./update.md)** | Keep the script current without manual pulls |
|
||||
| **[🛡️ Security Notes](./security.md)** | Threat model, secrets handling, and best practices |
|
||||
|
||||
### **Notifications & Monitoring**
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| **[📱 NTFY Push](./ntfy.md)** | Mobile push notifications |
|
||||
| **[🔗 Discord Webhooks](./conclusionwebhook.md)** | Rich server notifications |
|
||||
### **Notifications & Control**
|
||||
| Guide | Purpose |
|
||||
|-------|---------|
|
||||
| **[📱 NTFY Push](./ntfy.md)** | Real-time phone notifications |
|
||||
| **[<EFBFBD> Discord Webhooks](./conclusionwebhook.md)** | Detailed run summaries in your server |
|
||||
|
||||
### **Special Modes**
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| **[💳 Buy Mode](./buy-mode.md)** | Manual redemption with live monitoring |
|
||||
| Guide | Purpose |
|
||||
|-------|---------|
|
||||
| **[💳 Buy Mode](./buy-mode.md)** | Assisted manual redemption and live monitoring |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Recommended Reading Path
|
||||
## 🧭 Reading Paths
|
||||
|
||||
**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
|
||||
- **First install:** Getting Started → Accounts & 2FA → Configuration Reference → Scheduling **or** Docker
|
||||
- **Docker-first:** Getting Started prerequisites → Docker → Diagnostics → Notifications (NTFY or Webhooks)
|
||||
- **Optimizing runs:** Humanization → Schedule tuning → Proxy → Job State → Update
|
||||
|
||||
Welcome to the Microsoft Rewards Script V2 docs. Start here:
|
||||
|
||||
- Getting Started: high‑level 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 you’re 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: High‑level 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.
|
||||
Each guide now links back to the most relevant follow-up topics so you can jump between setup, operations, and troubleshooting without losing context.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start Path
|
||||
## 🔗 Useful Shortcuts
|
||||
|
||||
**New users should follow this sequence:**
|
||||
- Need sample configs? → [Config presets](./config-presets/)
|
||||
- Want a scripted environment? → [Scheduler](./schedule.md)
|
||||
- Looking to self-audit? → [Diagnostics](./diagnostics.md) + [Security](./security.md)
|
||||
|
||||
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
|
||||
If something feels out of sync with the code, open an issue or ping us on Discord—the docs are maintained to match the current defaults (`src/config.jsonc`, visible browsers by default, Docker headless enforcement via `FORCE_HEADLESS=1`).
|
||||
|
||||
@@ -19,6 +19,7 @@ The built-in scheduler provides **automated script execution** at specified time
|
||||
- 🔄 **Multiple passes** — Execute script multiple times per run
|
||||
- 🏖️ **Vacation mode** — Skip random days monthly
|
||||
- 🎲 **Jitter support** — Randomize execution times
|
||||
- 📅 **Humanization off-days** — Weekly random skips (disable via `humanization.randomOffDaysPerWeek`)
|
||||
- ⚡ **Immediate start** — Option to run on startup
|
||||
|
||||
---
|
||||
@@ -68,6 +69,28 @@ The built-in scheduler provides **automated script execution** at specified time
|
||||
| `vacation.enabled` | `false` | Skip random monthly off-block |
|
||||
| `vacation.minDays` | `3` | Minimum vacation days |
|
||||
| `vacation.maxDays` | `5` | Maximum vacation days |
|
||||
| `cron` | `undefined` | Optional cron expression (string or array) for advanced scheduling |
|
||||
|
||||
### **Cron Expressions (Advanced)**
|
||||
|
||||
You can now drive the scheduler with classic cron syntax instead of a single daily time. Provide either a string or an array in `schedule.cron`.
|
||||
|
||||
```json
|
||||
{
|
||||
"schedule": {
|
||||
"enabled": true,
|
||||
"cron": [
|
||||
"0 7 * * *", // every day at 07:00
|
||||
"30 19 * * 1-5" // weekdays at 19:30
|
||||
],
|
||||
"timeZone": "Europe/Paris"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Supports 5-field and 6-field cron expressions (`second minute hour day month weekday`).
|
||||
- When `cron` is set, the legacy `time`, `time12`, `time24`, and daily jitter env vars are ignored.
|
||||
- The scheduler still honors vacation mode, weekly random off-days, run-on-start, and watchdog features.
|
||||
|
||||
---
|
||||
|
||||
@@ -317,7 +340,7 @@ services:
|
||||
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)))"
|
||||
node -e "const fs=require('fs');const strip=input=>{let out='',inString=false,stringChar='',inLine=false,inBlock=false;for(let i=0;i<input.length;i++){const ch=input[i],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==='\\'){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;};console.log(JSON.parse(strip(fs.readFileSync('src/config.jsonc','utf8'))));"
|
||||
|
||||
# Check running processes
|
||||
Get-Process | Where-Object {$_.ProcessName -eq "node"}
|
||||
@@ -555,7 +578,7 @@ nohup npm run start:schedule > schedule.log 2>&1 &
|
||||
**Scheduler not running:**
|
||||
- Check `enabled: true` in config
|
||||
- Verify timezone format is correct
|
||||
- Ensure no syntax errors in config.json
|
||||
- Ensure no syntax errors in config.jsonc (remember it allows comments)
|
||||
|
||||
**Wrong execution time:**
|
||||
- Verify system clock is accurate
|
||||
@@ -578,7 +601,7 @@ nohup npm run start:schedule > schedule.log 2>&1 &
|
||||
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')))"
|
||||
node -e "const fs=require('fs');const strip=input=>{let out='',inString=false,stringChar='',inLine=false,inBlock=false;for(let i=0;i<input.length;i++){const ch=input[i],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==='\\'){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;};console.log(JSON.parse(strip(fs.readFileSync('src/config.jsonc','utf8'))));"
|
||||
|
||||
# Check process status
|
||||
ps aux | grep "start:schedule"
|
||||
@@ -621,7 +644,7 @@ services:
|
||||
```
|
||||
|
||||
Dans ce mode :
|
||||
- `passesPerRun` fonctionne (exécutera plusieurs passes à chaque horaire interne défini par `src/config.json`).
|
||||
- `passesPerRun` fonctionne (exécutera plusieurs passes à chaque horaire interne défini par `src/config.jsonc`).
|
||||
- Vous n'avez plus besoin de `CRON_SCHEDULE` ni de `run_daily.sh`.
|
||||
|
||||
### Docker + External Cron (par défaut du projet)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
playwright-test
|
||||
|
||||
# fixes "waiting until load" issue compared to
|
||||
# setting headless in config.json
|
||||
# setting headless in config.jsonc
|
||||
xvfb-run
|
||||
];
|
||||
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "microsoft-rewards-script",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "microsoft-rewards-script",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"cron-parser": "^4.9.0",
|
||||
"fingerprint-generator": "^2.1.66",
|
||||
"fingerprint-injector": "^2.1.66",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
@@ -869,6 +870,18 @@
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"luxon": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "microsoft-rewards-script",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
@@ -44,6 +44,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"cron-parser": "^4.9.0",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"fingerprint-generator": "^2.1.66",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Handles:
|
||||
* - Renaming accounts.example.json -> accounts.json (idempotent)
|
||||
* - Prompt loop to confirm passwords added
|
||||
* - Inform about config.json and conclusionWebhook
|
||||
* - Inform about config.jsonc and conclusionWebhook
|
||||
* - Run npm install + npm run build
|
||||
* - Optional start
|
||||
*/
|
||||
@@ -102,7 +102,7 @@ async function startOnly() {
|
||||
async function fullSetup() {
|
||||
renameAccountsIfNeeded();
|
||||
await loopForAccountsConfirmation();
|
||||
log('\nYou can now review config.json (same folder) to adjust settings such as conclusionWebhook.');
|
||||
log('\nYou can now review config.jsonc (same folder) to adjust settings such as conclusionWebhook.');
|
||||
log('(How to enable it is documented in the repository README.)\n');
|
||||
await ensureNpmAvailable();
|
||||
await runCommand(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install']);
|
||||
|
||||
@@ -40,7 +40,14 @@ class Browser {
|
||||
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)
|
||||
let headlessValue = envForceHeadless ? true : ((cfgAny['headless'] as boolean | undefined) ?? (cfgAny['browser'] && (cfgAny['browser'] as Record<string, unknown>)['headless'] as boolean | undefined) ?? false)
|
||||
if (this.bot.isBuyModeEnabled() && !envForceHeadless) {
|
||||
if (headlessValue !== false) {
|
||||
const target = this.bot.getBuyModeTarget()
|
||||
this.bot.log(this.bot.isMobile, 'BROWSER', `Buy mode detected${target ? ` for ${target}` : ''}; forcing headless=false so captchas and manual flows remain interactive.`, 'warn')
|
||||
}
|
||||
headlessValue = false
|
||||
}
|
||||
const headless: boolean = Boolean(headlessValue)
|
||||
|
||||
const engineName = 'chromium' // current hard-coded engine
|
||||
|
||||
@@ -127,7 +127,7 @@ export default class BrowserFunc {
|
||||
}
|
||||
}
|
||||
|
||||
const scriptContent = await target.evaluate(() => {
|
||||
let scriptContent = await target.evaluate(() => {
|
||||
const scripts = Array.from(document.querySelectorAll('script'))
|
||||
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
|
||||
|
||||
@@ -135,8 +135,22 @@ export default class BrowserFunc {
|
||||
})
|
||||
|
||||
if (!scriptContent) {
|
||||
await this.bot.browser.utils.captureDiagnostics(target, 'dashboard-data-missing').catch(()=>{})
|
||||
// Force a navigation retry once before failing hard
|
||||
try {
|
||||
await this.goHome(target)
|
||||
await target.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(()=>{})
|
||||
} catch {/* ignore */}
|
||||
const retryContent = await target.evaluate(() => {
|
||||
const scripts = Array.from(document.querySelectorAll('script'))
|
||||
const targetScript = scripts.find(script => script.innerText.includes('var dashboard'))
|
||||
return targetScript?.innerText ? targetScript.innerText : null
|
||||
}).catch(()=>null)
|
||||
if (!retryContent) {
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Dashboard data not found within script', 'error')
|
||||
}
|
||||
scriptContent = retryContent
|
||||
}
|
||||
|
||||
// Extract the dashboard object from the script content
|
||||
const dashboardData = await target.evaluate((scriptContent: string) => {
|
||||
@@ -151,6 +165,7 @@ export default class BrowserFunc {
|
||||
}, scriptContent)
|
||||
|
||||
if (!dashboardData) {
|
||||
await this.bot.browser.utils.captureDiagnostics(target, 'dashboard-data-parse').catch(()=>{})
|
||||
throw this.bot.log(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Unable to parse dashboard script', 'error')
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Page } from 'rebrowser-playwright'
|
||||
import { load } from 'cheerio'
|
||||
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { captureDiagnostics as captureSharedDiagnostics } from '../util/Diagnostics'
|
||||
|
||||
|
||||
export default class BrowserUtil {
|
||||
@@ -106,39 +107,8 @@ export default class BrowserUtil {
|
||||
*/
|
||||
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))
|
||||
}
|
||||
await this.bot.humanizer.microGestures(page)
|
||||
await this.bot.humanizer.actionPause()
|
||||
} catch { /* swallow */ }
|
||||
}
|
||||
|
||||
@@ -147,33 +117,7 @@ export default class BrowserUtil {
|
||||
* 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')
|
||||
}
|
||||
await captureSharedDiagnostics(this.bot, page, label)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"sessionPath": "sessions",
|
||||
|
||||
"browser": {
|
||||
// Run browser without UI (true=headless, false=visible). Visible can help with stability.
|
||||
// Keep headless=false so the browser window stays visible by default
|
||||
"headless": false,
|
||||
// Max time to wait for common operations (supports ms/s/min: e.g. 30000, "30s", "2min")
|
||||
"globalTimeout": "30s"
|
||||
@@ -31,17 +31,17 @@
|
||||
"fingerprinting": {
|
||||
// Persist browser fingerprints per device type to improve consistency across runs
|
||||
"saveFingerprint": {
|
||||
"mobile": false,
|
||||
"desktop": false
|
||||
"mobile": true,
|
||||
"desktop": true
|
||||
}
|
||||
},
|
||||
|
||||
"search": {
|
||||
// Use locale-specific query sources
|
||||
"useLocalQueries": false,
|
||||
"useLocalQueries": true,
|
||||
"settings": {
|
||||
// Add geo/locale signal into query selection
|
||||
"useGeoLocaleQueries": false,
|
||||
"useGeoLocaleQueries": true,
|
||||
// Randomly scroll search result pages to look more natural
|
||||
"scrollRandomResults": true,
|
||||
// Occasionally click a result (safe targets only)
|
||||
@@ -50,7 +50,7 @@
|
||||
"retryMobileSearchAmount": 2,
|
||||
// Delay between searches (supports numbers in ms or time strings)
|
||||
"delay": {
|
||||
"min": "3min",
|
||||
"min": "1min",
|
||||
"max": "5min"
|
||||
}
|
||||
}
|
||||
@@ -66,13 +66,13 @@
|
||||
"immediateBanAlert": true,
|
||||
// Extra random pause between actions (ms or time string e.g., "300ms", "1s")
|
||||
"actionDelay": {
|
||||
"min": 150,
|
||||
"max": 450
|
||||
"min": 500,
|
||||
"max": 2200
|
||||
},
|
||||
// Probability (0..1) to move mouse a tiny bit in between actions
|
||||
"gestureMoveProb": 0.4,
|
||||
"gestureMoveProb": 0.65,
|
||||
// Probability (0..1) to perform a very small scroll
|
||||
"gestureScrollProb": 0.2,
|
||||
"gestureScrollProb": 0.4,
|
||||
// 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": []
|
||||
@@ -80,12 +80,12 @@
|
||||
|
||||
// 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., 3–5 days) and all runs within
|
||||
// block between minDays and maxDays is selected (e.g., 2–4 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
|
||||
"enabled": true,
|
||||
"minDays": 2,
|
||||
"maxDays": 4
|
||||
},
|
||||
|
||||
"retryPolicy": {
|
||||
@@ -107,7 +107,7 @@
|
||||
"doDailyCheckIn": true,
|
||||
"doReadToEarn": true,
|
||||
// If true, run a desktop search bundle right after Daily Set
|
||||
"bundleDailySetWithSearch": false
|
||||
"bundleDailySetWithSearch": true
|
||||
},
|
||||
|
||||
"proxy": {
|
||||
@@ -1,15 +1,14 @@
|
||||
// Clean refactored Login implementation
|
||||
// Public API preserved: login(), getMobileAccessToken()
|
||||
|
||||
import type { Page } from 'playwright'
|
||||
import type { Page, Locator } from 'playwright'
|
||||
import * as crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import readline from 'readline'
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
import { generateTOTP } from '../util/Totp'
|
||||
import { saveSessionData } from '../util/Load'
|
||||
import { MicrosoftRewardsBot } from '../index'
|
||||
import { captureDiagnostics } from '../util/Diagnostics'
|
||||
import { OAuth } from '../interface/OAuth'
|
||||
|
||||
// -------------------------------
|
||||
@@ -203,6 +202,14 @@ export class Login {
|
||||
// --------------- 2FA Handling ---------------
|
||||
private async handle2FA(page: Page) {
|
||||
try {
|
||||
if (this.currentTotpSecret) {
|
||||
const totpSelector = await this.ensureTotpInput(page)
|
||||
if (totpSelector) {
|
||||
await this.submitTotpCode(page, totpSelector)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const number = await this.fetchAuthenticatorNumber(page)
|
||||
if (number) { await this.approveAuthenticator(page, number); return }
|
||||
await this.handleSMSOrTotp(page)
|
||||
@@ -255,16 +262,16 @@ export class Login {
|
||||
}
|
||||
|
||||
private async handleSMSOrTotp(page: Page) {
|
||||
// TOTP auto entry
|
||||
try {
|
||||
// TOTP auto entry (second chance if ensureTotpInput needed longer)
|
||||
if (this.currentTotpSecret) {
|
||||
const code = generateTOTP(this.currentTotpSecret.trim())
|
||||
await page.fill('input[name="otc"]', code)
|
||||
await page.keyboard.press('Enter')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted TOTP automatically')
|
||||
try {
|
||||
const totpSelector = await this.ensureTotpInput(page)
|
||||
if (totpSelector) {
|
||||
await this.submitTotpCode(page, totpSelector)
|
||||
return
|
||||
}
|
||||
} catch {/* ignore */}
|
||||
}
|
||||
|
||||
// Manual prompt
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Waiting for user 2FA code (SMS / Email / App fallback)')
|
||||
@@ -275,18 +282,194 @@ export class Login {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', '2FA code submitted')
|
||||
}
|
||||
|
||||
private async ensureTotpInput(page: Page): Promise<string | null> {
|
||||
const selector = await this.findFirstVisibleSelector(page, this.totpInputSelectors())
|
||||
if (selector) return selector
|
||||
|
||||
const attempts = 4
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
let acted = false
|
||||
|
||||
// Step 1: expose alternative verification options if hidden
|
||||
if (!acted) {
|
||||
acted = await this.clickFirstVisibleSelector(page, this.totpAltOptionSelectors())
|
||||
if (acted) await this.bot.utils.wait(900)
|
||||
}
|
||||
|
||||
// Step 2: choose authenticator code option if available
|
||||
if (!acted) {
|
||||
acted = await this.clickFirstVisibleSelector(page, this.totpChallengeSelectors())
|
||||
if (acted) await this.bot.utils.wait(900)
|
||||
}
|
||||
|
||||
const ready = await this.findFirstVisibleSelector(page, this.totpInputSelectors())
|
||||
if (ready) return ready
|
||||
|
||||
if (!acted) break
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private async submitTotpCode(page: Page, selector: string) {
|
||||
try {
|
||||
const code = generateTOTP(this.currentTotpSecret!.trim())
|
||||
const input = page.locator(selector).first()
|
||||
if (!await input.isVisible().catch(()=>false)) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'TOTP input unexpectedly hidden', 'warn')
|
||||
return
|
||||
}
|
||||
await input.fill('')
|
||||
await input.fill(code)
|
||||
const submitSelectors = [
|
||||
'#idSubmit_SAOTCC_Continue',
|
||||
'#idSubmit_SAOTCC_OTC',
|
||||
'button[type="submit"]:has-text("Verify")',
|
||||
'button[type="submit"]:has-text("Continuer")',
|
||||
'button:has-text("Verify")',
|
||||
'button:has-text("Continuer")',
|
||||
'button:has-text("Submit")'
|
||||
]
|
||||
const submit = await this.findFirstVisibleLocator(page, submitSelectors)
|
||||
if (submit) {
|
||||
await submit.click().catch(()=>{})
|
||||
} else {
|
||||
await page.keyboard.press('Enter').catch(()=>{})
|
||||
}
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted TOTP automatically')
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Failed to submit TOTP automatically: ' + error, 'warn')
|
||||
}
|
||||
}
|
||||
|
||||
private totpInputSelectors(): string[] {
|
||||
return [
|
||||
'input[name="otc"]',
|
||||
'#idTxtBx_SAOTCC_OTC',
|
||||
'#idTxtBx_SAOTCS_OTC',
|
||||
'input[data-testid="otcInput"]',
|
||||
'input[autocomplete="one-time-code"]',
|
||||
'input[type="tel"][name="otc"]'
|
||||
]
|
||||
}
|
||||
|
||||
private totpAltOptionSelectors(): string[] {
|
||||
return [
|
||||
'#idA_SAOTCS_ProofPickerChange',
|
||||
'#idA_SAOTCC_AlternateLogin',
|
||||
'a:has-text("Use a different verification option")',
|
||||
'a:has-text("Sign in another way")',
|
||||
'a:has-text("I can\'t use my Microsoft Authenticator app right now")',
|
||||
'button:has-text("Use a different verification option")',
|
||||
'button:has-text("Sign in another way")'
|
||||
]
|
||||
}
|
||||
|
||||
private totpChallengeSelectors(): string[] {
|
||||
return [
|
||||
'[data-value="PhoneAppOTP"]',
|
||||
'[data-value="OneTimeCode"]',
|
||||
'button:has-text("Use a verification code")',
|
||||
'button:has-text("Enter code manually")',
|
||||
'button:has-text("Enter a code from your authenticator app")',
|
||||
'button:has-text("Use code from your authentication app")',
|
||||
'button:has-text("Utiliser un code de vérification")',
|
||||
'button:has-text("Utiliser un code de verification")',
|
||||
'button:has-text("Entrer un code depuis votre application")',
|
||||
'button:has-text("Entrez un code depuis votre application")',
|
||||
'button:has-text("Entrez un code")',
|
||||
'div[role="button"]:has-text("Use a verification code")',
|
||||
'div[role="button"]:has-text("Enter a code")'
|
||||
]
|
||||
}
|
||||
|
||||
private async findFirstVisibleSelector(page: Page, selectors: string[]): Promise<string | null> {
|
||||
for (const sel of selectors) {
|
||||
const loc = page.locator(sel).first()
|
||||
if (await loc.isVisible().catch(() => false)) {
|
||||
return sel
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private async clickFirstVisibleSelector(page: Page, selectors: string[]): Promise<boolean> {
|
||||
for (const sel of selectors) {
|
||||
const loc = page.locator(sel).first()
|
||||
if (await loc.isVisible().catch(() => false)) {
|
||||
await loc.click().catch(()=>{})
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private async findFirstVisibleLocator(page: Page, selectors: string[]): Promise<Locator | null> {
|
||||
for (const sel of selectors) {
|
||||
const loc = page.locator(sel).first()
|
||||
if (await loc.isVisible().catch(() => false)) {
|
||||
return loc
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private async waitForRewardsRoot(page: Page, timeoutMs: number): Promise<string | null> {
|
||||
const selectors = [
|
||||
'html[data-role-name="RewardsPortal"]',
|
||||
'html[data-role-name*="RewardsPortal"]',
|
||||
'body[data-role-name*="RewardsPortal"]',
|
||||
'[data-role-name*="RewardsPortal"]',
|
||||
'[data-bi-name="rewards-dashboard"]',
|
||||
'main[data-bi-name="dashboard"]',
|
||||
'#more-activities',
|
||||
'#dashboard'
|
||||
]
|
||||
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
for (const sel of selectors) {
|
||||
const loc = page.locator(sel).first()
|
||||
if (await loc.isVisible().catch(()=>false)) {
|
||||
return sel
|
||||
}
|
||||
}
|
||||
await this.bot.utils.wait(350)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// --------------- Verification / State ---------------
|
||||
private async awaitRewardsPortal(page: Page) {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < DEFAULT_TIMEOUTS.loginMaxMs) {
|
||||
await this.handlePasskeyPrompts(page, 'main')
|
||||
const u = new URL(page.url())
|
||||
if (u.hostname === LOGIN_TARGET.host && u.pathname === LOGIN_TARGET.path) break
|
||||
const isRewardsHost = u.hostname === LOGIN_TARGET.host
|
||||
const isKnownPath = u.pathname === LOGIN_TARGET.path
|
||||
|| u.pathname === '/dashboard'
|
||||
|| u.pathname === '/rewardsapp/dashboard'
|
||||
|| u.pathname.startsWith('/?')
|
||||
if (isRewardsHost && isKnownPath) break
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
const portal = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 8000 }).catch(()=>null)
|
||||
if (!portal) throw this.bot.log(this.bot.isMobile, 'LOGIN', 'Portal root element missing after navigation', 'error')
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', 'Reached rewards portal')
|
||||
|
||||
const portalSelector = await this.waitForRewardsRoot(page, 8000)
|
||||
if (!portalSelector) {
|
||||
try {
|
||||
await this.bot.browser.func.goHome(page)
|
||||
} catch {/* ignore fallback errors */}
|
||||
|
||||
const fallbackSelector = await this.waitForRewardsRoot(page, 6000)
|
||||
if (!fallbackSelector) {
|
||||
await this.bot.browser.utils.captureDiagnostics(page, 'login-portal-missing').catch(()=>{})
|
||||
throw this.bot.log(this.bot.isMobile, 'LOGIN', 'Portal root element missing after navigation (saved diagnostics to reports/)', 'error')
|
||||
}
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Reached rewards portal via fallback (${fallbackSelector})`)
|
||||
return
|
||||
}
|
||||
|
||||
this.bot.log(this.bot.isMobile, 'LOGIN', `Reached rewards portal (${portalSelector})`)
|
||||
}
|
||||
|
||||
private async verifyBingContext(page: Page) {
|
||||
@@ -565,16 +748,7 @@ export class Login {
|
||||
}
|
||||
|
||||
private async saveIncidentArtifacts(page: Page, slug: string) {
|
||||
try {
|
||||
const base = path.join(process.cwd(),'diagnostics','security-incidents')
|
||||
await fs.promises.mkdir(base,{ recursive:true })
|
||||
const ts = new Date().toISOString().replace(/[:.]/g,'-')
|
||||
const dir = path.join(base, `${ts}-${slug}`)
|
||||
await fs.promises.mkdir(dir,{ recursive:true })
|
||||
try { await page.screenshot({ path: path.join(dir,'page.png'), fullPage:false }) } catch {/* ignore */}
|
||||
try { const html = await page.content(); await fs.promises.writeFile(path.join(dir,'page.html'), html) } catch {/* ignore */}
|
||||
this.bot.log(this.bot.isMobile,'SECURITY',`Saved incident artifacts: ${dir}`)
|
||||
} catch {/* ignore */}
|
||||
await captureDiagnostics(this.bot, page, slug, { scope: 'security', skipSlot: true, force: true })
|
||||
}
|
||||
|
||||
private async openDocsTab(page: Page, url: string) {
|
||||
|
||||
@@ -102,6 +102,14 @@ export class MicrosoftRewardsBot {
|
||||
}
|
||||
}
|
||||
|
||||
public isBuyModeEnabled(): boolean {
|
||||
return this.buyMode.enabled === true
|
||||
}
|
||||
|
||||
public getBuyModeTarget(): string | undefined {
|
||||
return this.buyMode.email
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.accounts = loadAccounts()
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ export interface ConfigSchedule {
|
||||
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
|
||||
cron?: string | string[]; // Optional cron expression(s) (standard 5-field or 6-field) for advanced scheduling
|
||||
}
|
||||
|
||||
export interface ConfigVacation {
|
||||
|
||||
2
src/types/luxon.d.ts → src/luxon.d.ts
vendored
2
src/types/luxon.d.ts → src/luxon.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
/* Minimal ambient declarations to unblock TypeScript when @types/luxon not present. */
|
||||
/* Minimal ambient declarations to unblock TypeScript when @types/luxon is absent. */
|
||||
declare module 'luxon' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const DateTime: any
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DateTime, IANAZone } from 'luxon'
|
||||
import cronParser from 'cron-parser'
|
||||
import { spawn } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
@@ -7,6 +8,9 @@ import { loadConfig } from './util/Load'
|
||||
import { log } from './util/Logger'
|
||||
import type { Config } from './interface/Config'
|
||||
|
||||
type CronExpressionInfo = { expression: string; tz: string }
|
||||
type DateTimeInstance = ReturnType<typeof DateTime.fromJSDate>
|
||||
|
||||
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
|
||||
@@ -47,6 +51,55 @@ function parseTargetToday(now: Date, schedule: Config['schedule'] | undefined) {
|
||||
return dtn.set({ hour, minute, second: 0, millisecond: 0 })
|
||||
}
|
||||
|
||||
function normalizeCronExpressions(schedule: Config['schedule'] | undefined, fallbackTz: string): CronExpressionInfo[] {
|
||||
if (!schedule) return []
|
||||
const raw = schedule.cron
|
||||
if (!raw) return []
|
||||
const expressions = Array.isArray(raw) ? raw : [raw]
|
||||
return expressions
|
||||
.map(expr => (typeof expr === 'string' ? expr.trim() : ''))
|
||||
.filter(expr => expr.length > 0)
|
||||
.map(expr => ({ expression: expr, tz: (schedule.timeZone && IANAZone.isValidZone(schedule.timeZone)) ? schedule.timeZone : fallbackTz }))
|
||||
}
|
||||
|
||||
function getNextCronOccurrence(after: DateTimeInstance, items: CronExpressionInfo[]): { next: DateTimeInstance; source: string } | null {
|
||||
let soonest: { next: DateTimeInstance; source: string } | null = null
|
||||
for (const item of items) {
|
||||
try {
|
||||
const iterator = cronParser.parseExpression(item.expression, {
|
||||
currentDate: after.toJSDate(),
|
||||
tz: item.tz
|
||||
})
|
||||
const nextDate = iterator.next().toDate()
|
||||
const nextDt = DateTime.fromJSDate(nextDate, { zone: item.tz })
|
||||
if (!soonest || nextDt < soonest.next) {
|
||||
soonest = { next: nextDt, source: item.expression }
|
||||
}
|
||||
} catch (error) {
|
||||
void log('main', 'SCHEDULER', `Invalid cron expression "${item.expression}": ${error instanceof Error ? error.message : String(error)}`, 'warn')
|
||||
}
|
||||
}
|
||||
return soonest
|
||||
}
|
||||
|
||||
function getNextDailyOccurrence(after: DateTimeInstance, schedule: Config['schedule'] | undefined): DateTimeInstance {
|
||||
const todayTarget = parseTargetToday(after.toJSDate(), schedule)
|
||||
const target = after >= todayTarget ? todayTarget.plus({ days: 1 }) : todayTarget
|
||||
return target
|
||||
}
|
||||
|
||||
function computeNextRun(after: DateTimeInstance, schedule: Config['schedule'] | undefined, cronItems: CronExpressionInfo[]): { next: DateTimeInstance; source: 'cron' | 'daily'; detail?: string } {
|
||||
if (cronItems.length > 0) {
|
||||
const cronNext = getNextCronOccurrence(after, cronItems)
|
||||
if (cronNext) {
|
||||
return { next: cronNext.next, source: 'cron', detail: cronNext.source }
|
||||
}
|
||||
void log('main', 'SCHEDULER', 'All cron expressions invalid; falling back to daily schedule', 'warn')
|
||||
}
|
||||
|
||||
return { next: getNextDailyOccurrence(after, schedule), source: 'daily' }
|
||||
}
|
||||
|
||||
async function runOnePass(): Promise<void> {
|
||||
const bot = new MicrosoftRewardsBot(false)
|
||||
await bot.initialize()
|
||||
@@ -195,7 +248,8 @@ async function main() {
|
||||
}
|
||||
offDays = chosen.sort((a,b)=>a-b)
|
||||
offWeek = week
|
||||
await log('main','SCHEDULER',`Selected random off-days this week (ISO): ${offDays.join(', ')}`,'warn')
|
||||
const msg = offDays.length ? offDays.join(', ') : 'none'
|
||||
await log('main','SCHEDULER',`Weekly humanization off-day sample (ISO weekday): ${msg} | adjust via config.humanization.randomOffDaysPerWeek`,'warn')
|
||||
}
|
||||
|
||||
const chooseVacationRange = async (now: typeof DateTime.prototype) => {
|
||||
@@ -226,6 +280,7 @@ async function main() {
|
||||
}
|
||||
|
||||
const tz = (schedule.timeZone && IANAZone.isValidZone(schedule.timeZone)) ? schedule.timeZone : 'UTC'
|
||||
const cronExpressions = normalizeCronExpressions(schedule, tz)
|
||||
// Default to false to avoid unexpected immediate runs
|
||||
const runImmediate = schedule.runImmediatelyOnStart === true
|
||||
let running = false
|
||||
@@ -256,7 +311,7 @@ async function main() {
|
||||
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')
|
||||
await log('main','SCHEDULER',`Skipping immediate run: humanization off-day (ISO weekday ${nowDT.weekday}). Set humanization.randomOffDaysPerWeek=0 to disable.`,'warn')
|
||||
} else {
|
||||
await runPasses(passes)
|
||||
}
|
||||
@@ -264,23 +319,18 @@ async function main() {
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
const nowDT = DateTime.local().setZone(tz)
|
||||
const nextInfo = computeNextRun(nowDT, schedule, cronExpressions)
|
||||
const next = nextInfo.next
|
||||
let ms = Math.max(0, next.toMillis() - nowDT.toMillis())
|
||||
|
||||
// Optional daily jitter to further randomize the exact start time each day
|
||||
let extraMs = 0
|
||||
if (cronExpressions.length === 0) {
|
||||
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))
|
||||
@@ -288,14 +338,13 @@ async function main() {
|
||||
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)`)
|
||||
}
|
||||
const jitterMsg = extraMs > 0 ? ` plus daily jitter (+${Math.round(extraMs/60000)}m)` : ''
|
||||
const sourceMsg = nextInfo.source === 'cron' ? ` [cron: ${nextInfo.detail}]` : ''
|
||||
await log('main', 'SCHEDULER', `Next run at ${human}${jitterMsg}${sourceMsg} (in ${totalSec}s)`)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
@@ -310,7 +359,7 @@ async function main() {
|
||||
continue
|
||||
}
|
||||
if (offDays.includes(nowRun.weekday)) {
|
||||
await log('main','SCHEDULER',`Skipping scheduled run: off-day (weekday ${nowRun.weekday})`,'warn')
|
||||
await log('main','SCHEDULER',`Skipping scheduled run: humanization off-day (ISO weekday ${nowRun.weekday}). Set humanization.randomOffDaysPerWeek=0 to disable.`,'warn')
|
||||
continue
|
||||
}
|
||||
if (!running) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
@@ -45,6 +45,14 @@ class AxiosClient {
|
||||
try {
|
||||
return await this.instance.request(config)
|
||||
} catch (err: unknown) {
|
||||
const axiosErr = err as AxiosError | undefined
|
||||
|
||||
// Detect HTTP proxy auth failures (status 407) and retry without proxy once.
|
||||
if (!bypassProxy && axiosErr && axiosErr.response && axiosErr.response.status === 407) {
|
||||
const bypassInstance = axios.create()
|
||||
return bypassInstance.request(config)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
74
src/util/Diagnostics.ts
Normal file
74
src/util/Diagnostics.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import type { Page } from 'rebrowser-playwright'
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
|
||||
export type DiagnosticsScope = 'default' | 'security'
|
||||
|
||||
export interface DiagnosticsOptions {
|
||||
scope?: DiagnosticsScope
|
||||
skipSlot?: boolean
|
||||
force?: boolean
|
||||
}
|
||||
|
||||
export async function captureDiagnostics(bot: MicrosoftRewardsBot, page: Page, rawLabel: string, options?: DiagnosticsOptions): Promise<void> {
|
||||
try {
|
||||
const scope: DiagnosticsScope = options?.scope ?? 'default'
|
||||
const cfg = bot.config?.diagnostics ?? {}
|
||||
const forceCapture = options?.force === true || scope === 'security'
|
||||
if (!forceCapture && cfg.enabled === false) return
|
||||
|
||||
if (scope === 'default') {
|
||||
const maxPerRun = typeof cfg.maxPerRun === 'number' ? cfg.maxPerRun : 8
|
||||
if (!options?.skipSlot && !bot.tryReserveDiagSlot(maxPerRun)) return
|
||||
}
|
||||
|
||||
const saveScreenshot = scope === 'security' ? true : cfg.saveScreenshot !== false
|
||||
const saveHtml = scope === 'security' ? true : cfg.saveHtml !== false
|
||||
if (!saveScreenshot && !saveHtml) return
|
||||
|
||||
const safeLabel = rawLabel.replace(/[^a-z0-9-_]/gi, '_').slice(0, 64) || 'capture'
|
||||
const now = new Date()
|
||||
const timestamp = `${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`
|
||||
|
||||
let dir: string
|
||||
if (scope === 'security') {
|
||||
const base = path.join(process.cwd(), 'diagnostics', 'security-incidents')
|
||||
fs.mkdirSync(base, { recursive: true })
|
||||
const sub = `${now.toISOString().replace(/[:.]/g, '-')}-${safeLabel}`
|
||||
dir = path.join(base, sub)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
} else {
|
||||
const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||||
dir = path.join(process.cwd(), 'reports', day)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
if (saveScreenshot) {
|
||||
const shotName = scope === 'security' ? 'page.png' : `${timestamp}_${safeLabel}.png`
|
||||
const shotPath = path.join(dir, shotName)
|
||||
await page.screenshot({ path: shotPath }).catch(() => {})
|
||||
if (scope === 'security') {
|
||||
bot.log(bot.isMobile, 'DIAG', `Saved security screenshot to ${shotPath}`)
|
||||
} else {
|
||||
bot.log(bot.isMobile, 'DIAG', `Saved diagnostics screenshot to ${shotPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (saveHtml) {
|
||||
const htmlName = scope === 'security' ? 'page.html' : `${timestamp}_${safeLabel}.html`
|
||||
const htmlPath = path.join(dir, htmlName)
|
||||
try {
|
||||
const html = await page.content()
|
||||
await fs.promises.writeFile(htmlPath, html, 'utf-8')
|
||||
if (scope === 'security') {
|
||||
bot.log(bot.isMobile, 'DIAG', `Saved security HTML to ${htmlPath}`)
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
bot.log(bot.isMobile, 'DIAG', `Failed to capture diagnostics: ${error instanceof Error ? error.message : error}`, 'warn')
|
||||
}
|
||||
}
|
||||
@@ -255,14 +255,21 @@ export function loadConfig(): Config {
|
||||
return configCache
|
||||
}
|
||||
|
||||
// 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
|
||||
// Resolve configuration file from common locations (supports .jsonc and .json)
|
||||
const names = ['config.jsonc', 'config.json']
|
||||
const bases = [
|
||||
path.join(__dirname, '../'), // dist root when compiled
|
||||
path.join(__dirname, '../src'), // fallback: running dist but config still in src
|
||||
process.cwd(), // repo root
|
||||
path.join(process.cwd(), 'src'), // repo/src when running ts-node
|
||||
__dirname // dist/util
|
||||
]
|
||||
const candidates: string[] = []
|
||||
for (const base of bases) {
|
||||
for (const name of names) {
|
||||
candidates.push(path.join(base, name))
|
||||
}
|
||||
}
|
||||
let cfgPath: string | null = null
|
||||
for (const p of candidates) {
|
||||
try { if (fs.existsSync(p)) { cfgPath = p; break } } catch { /* ignore */ }
|
||||
|
||||
@@ -1,8 +1,70 @@
|
||||
import axios from 'axios'
|
||||
import chalk from 'chalk'
|
||||
|
||||
import { Ntfy } from './Ntfy'
|
||||
import { loadConfig } from './Load'
|
||||
|
||||
type WebhookBuffer = {
|
||||
lines: string[]
|
||||
sending: boolean
|
||||
timer?: NodeJS.Timeout
|
||||
}
|
||||
|
||||
const webhookBuffers = new Map<string, WebhookBuffer>()
|
||||
|
||||
function getBuffer(url: string): WebhookBuffer {
|
||||
let buf = webhookBuffers.get(url)
|
||||
if (!buf) {
|
||||
buf = { lines: [], sending: false }
|
||||
webhookBuffers.set(url, buf)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
async function sendBatch(url: string, buf: WebhookBuffer) {
|
||||
if (buf.sending) return
|
||||
buf.sending = true
|
||||
while (buf.lines.length > 0) {
|
||||
const chunk: string[] = []
|
||||
let currentLength = 0
|
||||
while (buf.lines.length > 0) {
|
||||
const next = buf.lines[0]!
|
||||
const projected = currentLength + next.length + (chunk.length > 0 ? 1 : 0)
|
||||
if (projected > 1900 && chunk.length > 0) break
|
||||
buf.lines.shift()
|
||||
chunk.push(next)
|
||||
currentLength = projected
|
||||
}
|
||||
|
||||
const content = chunk.join('\n').slice(0, 1900)
|
||||
if (!content) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(url, { content }, { headers: { 'Content-Type': 'application/json' }, timeout: 10000 })
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
} catch (error) {
|
||||
// Re-queue failed batch at front and exit loop
|
||||
buf.lines = chunk.concat(buf.lines)
|
||||
console.error('[Webhook] live log delivery failed:', error)
|
||||
break
|
||||
}
|
||||
}
|
||||
buf.sending = false
|
||||
}
|
||||
|
||||
function enqueueWebhookLog(url: string, line: string) {
|
||||
const buf = getBuffer(url)
|
||||
buf.lines.push(line)
|
||||
if (!buf.timer) {
|
||||
buf.timer = setTimeout(() => {
|
||||
buf.timer = undefined
|
||||
void sendBatch(url, buf)
|
||||
}, 750)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
@@ -84,6 +146,21 @@ export function log(isMobile: boolean | 'main', title: string, message: string,
|
||||
break
|
||||
}
|
||||
|
||||
// Webhook streaming (live logs)
|
||||
try {
|
||||
const loggingCfg: Record<string, unknown> = (configAny.logging || {}) as Record<string, unknown>
|
||||
const webhookCfg = configData.webhook
|
||||
const liveUrlRaw = typeof loggingCfg.liveWebhookUrl === 'string' ? loggingCfg.liveWebhookUrl.trim() : ''
|
||||
const liveUrl = liveUrlRaw || (webhookCfg?.enabled && webhookCfg.url ? webhookCfg.url : '')
|
||||
const webhookExclude = Array.isArray(loggingCfg.webhookExcludeFunc) ? loggingCfg.webhookExcludeFunc : configData.webhookLogExcludeFunc || []
|
||||
const webhookExcluded = Array.isArray(webhookExclude) && webhookExclude.some((x: string) => x.toLowerCase() === title.toLowerCase())
|
||||
if (liveUrl && !webhookExcluded) {
|
||||
enqueueWebhookLog(liveUrl, cleanStr)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Logger] Failed to enqueue webhook log:', error)
|
||||
}
|
||||
|
||||
// Return an Error when logging an error so callers can `throw log(...)`
|
||||
if (type === 'error') {
|
||||
// CommunityReporter disabled per project policy
|
||||
|
||||
@@ -2,10 +2,21 @@ import axios from 'axios'
|
||||
import { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
|
||||
|
||||
import { log } from './Logger'
|
||||
import Retry from './Retry'
|
||||
|
||||
import { ChromeVersion, EdgeVersion } from '../interface/UserAgentUtil'
|
||||
import { ChromeVersion, EdgeVersion, Architecture, Platform } from '../interface/UserAgentUtil'
|
||||
|
||||
const NOT_A_BRAND_VERSION = '99'
|
||||
const EDGE_VERSION_URL = 'https://edgeupdates.microsoft.com/api/products'
|
||||
const EDGE_VERSION_CACHE_TTL_MS = 1000 * 60 * 60
|
||||
|
||||
type EdgeVersionResult = {
|
||||
android?: string
|
||||
windows?: string
|
||||
}
|
||||
|
||||
let edgeVersionCache: { data: EdgeVersionResult; expiresAt: number } | null = null
|
||||
let edgeVersionInFlight: Promise<EdgeVersionResult> | null = null
|
||||
|
||||
export async function getUserAgent(isMobile: boolean) {
|
||||
const system = getSystemComponents(isMobile)
|
||||
@@ -18,6 +29,7 @@ export async function getUserAgent(isMobile: boolean) {
|
||||
const platformVersion = `${isMobile ? Math.floor(Math.random() * 5) + 9 : Math.floor(Math.random() * 15) + 1}.0.0`
|
||||
|
||||
const uaMetadata = {
|
||||
mobile: isMobile,
|
||||
isMobile,
|
||||
platform: isMobile ? 'Android' : 'Windows',
|
||||
fullVersionList: [
|
||||
@@ -33,7 +45,8 @@ export async function getUserAgent(isMobile: boolean) {
|
||||
platformVersion,
|
||||
architecture: isMobile ? '' : 'x86',
|
||||
bitness: isMobile ? '' : '64',
|
||||
model: ''
|
||||
model: '',
|
||||
uaFullVersion: app['chrome_version']
|
||||
}
|
||||
|
||||
return { userAgent: uaTemplate, userAgentMetadata: uaMetadata }
|
||||
@@ -59,38 +72,49 @@ export async function getChromeVersion(isMobile: boolean): Promise<string> {
|
||||
}
|
||||
|
||||
export async function getEdgeVersions(isMobile: boolean) {
|
||||
const now = Date.now()
|
||||
if (edgeVersionCache && edgeVersionCache.expiresAt > now) {
|
||||
return edgeVersionCache.data
|
||||
}
|
||||
|
||||
if (edgeVersionInFlight) {
|
||||
try {
|
||||
const request = {
|
||||
url: 'https://edgeupdates.microsoft.com/api/products',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axios(request)
|
||||
const data: EdgeVersion[] = response.data
|
||||
const stable = data.find(x => x.Product == 'Stable') as EdgeVersion
|
||||
return {
|
||||
android: stable.Releases.find(x => x.Platform == 'Android')?.ProductVersion,
|
||||
windows: stable.Releases.find(x => (x.Platform == 'Windows' && x.Architecture == 'x64'))?.ProductVersion
|
||||
}
|
||||
|
||||
|
||||
return await edgeVersionInFlight
|
||||
} catch (error) {
|
||||
throw log(isMobile, 'USERAGENT-EDGE-VERSION', 'An error occurred:' + error, 'error')
|
||||
if (edgeVersionCache) {
|
||||
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Using cached Edge versions after in-flight failure: ' + formatEdgeError(error), 'warn')
|
||||
return edgeVersionCache.data
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPromise = fetchEdgeVersionsWithRetry(isMobile)
|
||||
.then(result => {
|
||||
edgeVersionCache = { data: result, expiresAt: Date.now() + EDGE_VERSION_CACHE_TTL_MS }
|
||||
edgeVersionInFlight = null
|
||||
return result
|
||||
})
|
||||
.catch(error => {
|
||||
edgeVersionInFlight = null
|
||||
if (edgeVersionCache) {
|
||||
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Falling back to cached Edge versions: ' + formatEdgeError(error), 'warn')
|
||||
return edgeVersionCache.data
|
||||
}
|
||||
throw log(isMobile, 'USERAGENT-EDGE-VERSION', 'Failed to fetch Edge versions: ' + formatEdgeError(error), 'error')
|
||||
})
|
||||
|
||||
edgeVersionInFlight = fetchPromise
|
||||
return fetchPromise
|
||||
}
|
||||
|
||||
export function getSystemComponents(mobile: boolean): string {
|
||||
const osId: string = mobile ? 'Linux' : 'Windows NT 10.0'
|
||||
const uaPlatform: string = mobile ? `Android 1${Math.floor(Math.random() * 5)}` : 'Win64; x64'
|
||||
|
||||
if (mobile) {
|
||||
return `${uaPlatform}; ${osId}; K`
|
||||
const androidVersion = 10 + Math.floor(Math.random() * 5)
|
||||
return `Linux; Android ${androidVersion}; K`
|
||||
}
|
||||
|
||||
return `${uaPlatform}; ${osId}`
|
||||
return 'Windows NT 10.0; Win64; x64'
|
||||
}
|
||||
|
||||
export async function getAppComponents(isMobile: boolean) {
|
||||
@@ -113,12 +137,124 @@ export async function getAppComponents(isMobile: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEdgeVersionsWithRetry(isMobile: boolean): Promise<EdgeVersionResult> {
|
||||
const retry = new Retry()
|
||||
return retry.run(async () => {
|
||||
const versions = await fetchEdgeVersionsOnce(isMobile)
|
||||
if (!versions.android && !versions.windows) {
|
||||
throw new Error('Stable Edge releases did not include Android or Windows versions')
|
||||
}
|
||||
return versions
|
||||
}, () => true)
|
||||
}
|
||||
|
||||
async function fetchEdgeVersionsOnce(isMobile: boolean): Promise<EdgeVersionResult> {
|
||||
try {
|
||||
const response = await axios<EdgeVersion[]>({
|
||||
url: EDGE_VERSION_URL,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; rewards-bot/2.1)' // Provide UA to avoid stricter servers
|
||||
},
|
||||
timeout: 10000
|
||||
})
|
||||
return mapEdgeVersions(response.data)
|
||||
|
||||
} catch (primaryError) {
|
||||
const fallback = await tryNativeFetchFallback(isMobile)
|
||||
if (fallback) {
|
||||
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Axios failed, native fetch succeeded: ' + formatEdgeError(primaryError), 'warn')
|
||||
return fallback
|
||||
}
|
||||
throw primaryError
|
||||
}
|
||||
}
|
||||
|
||||
async function tryNativeFetchFallback(isMobile: boolean): Promise<EdgeVersionResult | null> {
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 10000)
|
||||
const response = await fetch(EDGE_VERSION_URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; rewards-bot/2.1)'
|
||||
},
|
||||
signal: controller.signal
|
||||
})
|
||||
clearTimeout(timeout)
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status)
|
||||
}
|
||||
const data = await response.json() as EdgeVersion[]
|
||||
return mapEdgeVersions(data)
|
||||
} catch (error) {
|
||||
log(isMobile, 'USERAGENT-EDGE-VERSION', 'Native fetch fallback failed: ' + formatEdgeError(error), 'warn')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function mapEdgeVersions(data: EdgeVersion[]): EdgeVersionResult {
|
||||
const stable = data.find(entry => entry.Product.toLowerCase() === 'stable')
|
||||
?? data.find(entry => /stable/i.test(entry.Product))
|
||||
if (!stable) {
|
||||
throw new Error('Stable Edge channel not found in response payload')
|
||||
}
|
||||
|
||||
const androidRelease = stable.Releases.find(release => release.Platform === Platform.Android)
|
||||
const windowsRelease = stable.Releases.find(release => release.Platform === Platform.Windows && release.Architecture === Architecture.X64)
|
||||
?? stable.Releases.find(release => release.Platform === Platform.Windows)
|
||||
|
||||
return {
|
||||
android: androidRelease?.ProductVersion,
|
||||
windows: windowsRelease?.ProductVersion
|
||||
}
|
||||
}
|
||||
|
||||
function formatEdgeError(error: unknown): string {
|
||||
if (isAggregateErrorLike(error)) {
|
||||
const inner = error.errors
|
||||
.map(innerErr => formatEdgeError(innerErr))
|
||||
.filter(Boolean)
|
||||
.join('; ')
|
||||
const message = error.message || 'AggregateError'
|
||||
return inner ? `${message} | causes: ${inner}` : message
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
const parts = [`${error.name}: ${error.message}`]
|
||||
const cause = getErrorCause(error)
|
||||
if (cause) {
|
||||
parts.push('cause => ' + formatEdgeError(cause))
|
||||
}
|
||||
return parts.join(' | ')
|
||||
}
|
||||
|
||||
return String(error)
|
||||
}
|
||||
|
||||
type AggregateErrorLike = { message?: string; errors: unknown[] }
|
||||
|
||||
function isAggregateErrorLike(error: unknown): error is AggregateErrorLike {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return false
|
||||
}
|
||||
const candidate = error as { errors?: unknown }
|
||||
return Array.isArray(candidate.errors)
|
||||
}
|
||||
|
||||
function getErrorCause(error: { cause?: unknown } | Error): unknown {
|
||||
if (typeof (error as { cause?: unknown }).cause === 'undefined') {
|
||||
return undefined
|
||||
}
|
||||
return (error as { cause?: unknown }).cause
|
||||
}
|
||||
|
||||
export async function updateFingerprintUserAgent(fingerprint: BrowserFingerprintWithHeaders, isMobile: boolean): Promise<BrowserFingerprintWithHeaders> {
|
||||
try {
|
||||
const userAgentData = await getUserAgent(isMobile)
|
||||
const componentData = await getAppComponents(isMobile)
|
||||
|
||||
//@ts-expect-error Errors due it not exactly matching
|
||||
fingerprint.fingerprint.navigator.userAgentData = userAgentData.userAgentMetadata
|
||||
fingerprint.fingerprint.navigator.userAgent = userAgentData.userAgent
|
||||
fingerprint.fingerprint.navigator.appVersion = userAgentData.userAgent.replace(`${fingerprint.fingerprint.navigator.appCodeName}/`, '')
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/functions/queries.json"
|
||||
],
|
||||
"exclude": [
|
||||
|
||||
Reference in New Issue
Block a user