19 Commits
v3.0.1 ... v3

Author SHA1 Message Date
Michael Cammarata
b0c01fd433 Update README.md (#451)
Update Nix instructions for v3, minor tidy-ups.
2026-01-15 19:55:55 +01:00
Netsky
00bf632ac7 Update Node.js requirement from 22 to 24 2026-01-13 00:02:05 +01:00
Michael Cammarata
3375fe6382 Update README.md (#446)
Add v3 account sample, and added [!TIP] for how to obtain totpSecret. Tidied up some punctuation.
2026-01-12 21:58:39 +01:00
Otavio Bigogno
97c705cd7f Apply yellowBright color on LOGIN-PASSWORDLESS events for visibility and fixes mobile flow failed (need tests) (#442)
* Apply yellowBright color on LOGIN-PASSWORDLESS events for visibility

* Fix Mobile flow failed for abc@email.com: Request failed with status code 400
Fixes #441
2026-01-12 20:22:16 +01:00
Michael Cammarata
f0bee7db5d Update README.md (#445)
v3 Readme, simplified, clearer warnings for common issues.
2026-01-12 19:31:03 +01:00
hmcdat
cbd8842f2f fix: try fixing error fetching dashboard data on mobile func (#444) 2026-01-12 19:30:29 +01:00
Simon Gardling
a03bc903ad bump nixpkgs version (#443) 2026-01-12 14:28:36 +01:00
qingzt
2f9c88f8d8 fix: select wrong button when Attempting to bypass "Get code" (#437) 2026-01-08 09:49:40 +01:00
Michael Cammarata
032debed62 Bump Node.js Docker image from 22 to 24 (#439)
* update compose.yaml

Simplify compose.yaml, remove resource limits.

* Bump Node.js Docker image from 22 to 24 (slim)
2026-01-06 20:01:26 +01:00
Michael Cammarata
ca3253ac52 update compose.yaml (#438)
Simplify compose.yaml, remove resource limits.
2026-01-06 19:40:38 +01:00
Netsky
171521c51f Update Node.js engine requirement to 24.0.0 2026-01-06 19:20:15 +01:00
Netsky
8365a0c422 Delete dockerignore 2026-01-05 16:30:38 +01:00
Netsky
62eb1775e3 Update .dockerignore to exclude additional files 2026-01-05 16:30:18 +01:00
TheNetsky
576899f39d v3.1.0 initial 2026-01-05 16:26:47 +01:00
AariaX
a8ddb65b21 fix: workers not exiting when using mutliple clusters (#435)
* fix: workers not exiting when using mutliple clusters

* refactor: use process.disconnect for cleaner exit
2026-01-03 11:48:29 +01:00
Heikon Silva Costa
0e8ca0c862 Add .dockerignore to exclude dist and node_modules (#433) 2025-12-31 11:44:17 +01:00
Gianluca Lauro
ffb4a28785 added github workflow (#424) 2025-12-25 11:09:02 +01:00
Netsky
efbadb7d0b Re-add queries.json file 2025-12-17 17:54:18 +01:00
Netsky
f9fcbd851e Temp: Refer to v1/2 for installation instructions 2025-12-17 17:50:45 +01:00
46 changed files with 4284 additions and 886 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
node_modules
dist
.git
.gitignore
.vscode
.DS_Store
sessions/
.dev/
diagnostics/
note
accounts.dev.json
accounts.main.json
.playwright-chromium-installed

71
.github/workflows/auto-release.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Auto Release on package.json version bump
on:
push:
branches: [v3]
paths:
- package.json
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read current version
id: current
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Read previous version (from previous commit)
id: previous
run: |
PREV_VERSION=$(git show HEAD~1:package.json 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" || echo "")
echo "version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
- name: Check if version increased
id: check
run: |
CUR="${{ steps.current.outputs.version }}"
PREV="${{ steps.previous.outputs.version }}"
echo "Current: $CUR"
echo "Previous: $PREV"
# if previous doesn't exist (first commit containing package.json), skip
if [ -z "$PREV" ]; then
echo "should_release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Compare semver using node's semver package if available; fallback to simple inequality
node -e "
const cur='${CUR}';
const prev='${PREV}';
let should = cur !== prev;
try {
const semver = require('semver');
should = semver.gt(cur, prev);
} catch (_) {}
console.log('should_release=' + should);
" >> "$GITHUB_OUTPUT"
- name: Stop if no bump
if: steps.check.outputs.should_release != 'true'
run: echo "No version increase detected."
- name: Create tag + GitHub Release
if: steps.check.outputs.should_release == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.current.outputs.version }}
name: v${{ steps.current.outputs.version }}
generate_release_notes: true

51
.github/workflows/docker-release.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Build and Push Docker Image on Release
on:
release:
types: [published]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ accounts.dev.json
accounts.main.json accounts.main.json
.DS_Store .DS_Store
.playwright-chromium-installed .playwright-chromium-installed
bun.lock

View File

@@ -1,7 +1,7 @@
############################################################################### ###############################################################################
# Stage 1: Builder # Stage 1: Builder
############################################################################### ###############################################################################
FROM node:22-slim AS builder FROM node:24-slim AS builder
WORKDIR /usr/src/microsoft-rewards-script WORKDIR /usr/src/microsoft-rewards-script
@@ -29,7 +29,7 @@ RUN npx patchright install --with-deps --only-shell chromium \
############################################################################### ###############################################################################
# Stage 2: Runtime # Stage 2: Runtime
############################################################################### ###############################################################################
FROM node:22-slim AS runtime FROM node:24-slim AS runtime
WORKDIR /usr/src/microsoft-rewards-script WORKDIR /usr/src/microsoft-rewards-script

213
README.md
View File

@@ -2,7 +2,218 @@
--- ---
TODO ## Table of Contents
- [Quick Setup](#quick-setup)
- [Nix Setup](#nix-setup)
- [Configuration Options](#configuration-options)
- [Account Setup](#account-setup)
- [Troubleshooting](#troubleshooting)
- [Disclaimer](#disclaimer)
---
## Quick Setup
**Requirements:** Node.js >= 24 and Git
Works on Windows, Linux, macOS, and WSL.
### Get the script
```bash
git clone https://github.com/TheNetsky/Microsoft-Rewards-Script.git
cd Microsoft-Rewards-Script
```
Or, download the latest release ZIP and extract it.
### Create an account.json and config.json
Copy, rename, and edit your account and configuration files before deploying the script.
- Copy or rename `src/accounts.example.json` to `src/accounts.json` and add your credentials
- Copy or rename `src/config.example.json` to `src/config.json` and customize your preferences.
> [!CAUTION]
> Do not skip this step.
> Prior versions of accounts.json and config.json are not compatible with current release.
> [!WARNING]
> You must rebuild your script after making any changes to accounts.json and config.json.
### Build and run the script (bare metal version)
```bash
npm run pre-build
npm run build
npm run start
```
### Build and run the script (docker version)
```bash
docker compose up -d
```
> [!CAUTION]
> Set `headless` to `true` in the `src/config.json` when using Docker.
> Additional docker-specific scheduling options are in the `compose.yaml`
> [!TIP]
> When headeless, monitor logs with `docker logs microsoft-rewards-script` (for example, to view passwordless codes), or enable a webhook service in the `src/config.json`.
---
## Nix Setup
If using Nix: `bash scripts/nix/run.sh`
---
## Configuration Reference
Edit `src/config.json` to customize behavior. Below are all currently available options.
> [!WARNING]
> Rebuild the script after all changes.
### Core
| Setting | Type | Default | Description |
|----------|------|----------|-------------|
| `baseURL` | string | `"https://rewards.bing.com"` | Microsoft Rewards base URL |
| `sessionPath` | string | `"sessions"` | Directory to store browser sessions |
| `headless` | boolean | `false` | Run browser invisibly |
| `runOnZeroPoints` | boolean | `false` | Run even when no points are available |
| `clusters` | number | `1` | Number of concurrent account clusters |
| `errorDiagnostics` | boolean | `false` | Enable error diagnostics |
| `searchOnBingLocalQueries` | boolean | `false` | Use local query list |
| `globalTimeout` | string | `"30sec"` | Timeout for all actions |
> [!CAUTION]
> Set `headless` to `true` when using docker
### Workers
| Setting | Type | Default | Description |
|----------|------|----------|-------------|
| `workers.doDailySet` | boolean | `true` | Complete daily set |
| `workers.doSpecialPromotions` | boolean | `true` | Complete special promotions |
| `workers.doMorePromotions` | boolean | `true` | Complete more promotions |
| `workers.doPunchCards` | boolean | `true` | Complete punchcards |
| `workers.doAppPromotions` | boolean | `true` | Complete app promotions |
| `workers.doDesktopSearch` | boolean | `true` | Perform desktop searches |
| `workers.doMobileSearch` | boolean | `true` | Perform mobile searches |
| `workers.doDailyCheckIn` | boolean | `true` | Complete daily check-in |
| `workers.doReadToEarn` | boolean | `true` | Complete Read-to-Earn |
### Search Settings
| Setting | Type | Default | Description |
|----------|------|----------|-------------|
| `searchSettings.scrollRandomResults` | boolean | `false` | Scroll randomly on results |
| `searchSettings.clickRandomResults` | boolean | `false` | Click random links |
| `searchSettings.parallelSearching` | boolean | `true` | Run searches in parallel |
| `searchSettings.queryEngines` | string[] | `["google", "wikipedia", "reddit", "local"]` | Query engines to use |
| `searchSettings.searchResultVisitTime` | string | `"10sec"` | Time to spend on each search result |
| `searchSettings.searchDelay.min` | string | `"30sec"` | Minimum delay between searches |
| `searchSettings.searchDelay.max` | string | `"1min"` | Maximum delay between searches |
| `searchSettings.readDelay.min` | string | `"30sec"` | Minimum delay for reading |
| `searchSettings.readDelay.max` | string | `"1min"` | Maximum delay for reading |
### Logging
| Setting | Type | Default | Description |
|----------|------|----------|-------------|
| `debugLogs` | boolean | `false` | Enable debug logging |
| `consoleLogFilter.enabled` | boolean | `false` | Enable console log filtering |
| `consoleLogFilter.mode` | string | `"whitelist"` | Filter mode (whitelist/blacklist) |
| `consoleLogFilter.levels` | string[] | `["error", "warn"]` | Log levels to filter |
| `consoleLogFilter.keywords` | string[] | `["starting account"]` | Keywords to filter |
| `consoleLogFilter.regexPatterns` | string[] | `[]` | Regex patterns for filtering |
### Proxy
| Setting | Type | Default | Description |
|----------|------|----------|-------------|
| `proxy.queryEngine` | boolean | `true` | Proxy query engine requests |
### Webhooks
| Setting | Type | Default | Description |
|----------|------|----------|-------------|
| `webhook.discord.enabled` | boolean | `false` | Enable Discord webhook |
| `webhook.discord.url` | string | `""` | Discord webhook URL |
| `webhook.ntfy.enabled` | boolean | `false` | Enable ntfy notifications |
| `webhook.ntfy.url` | string | `""` | ntfy server URL |
| `webhook.ntfy.topic` | string | `""` | ntfy topic |
| `webhook.ntfy.token` | string | `""` | ntfy authentication token |
| `webhook.ntfy.title` | string | `"Microsoft-Rewards-Script"` | Notification title |
| `webhook.ntfy.tags` | string[] | `["bot", "notify"]` | Notification tags |
| `webhook.ntfy.priority` | number | `3` | Notification priority (1-5) |
| `webhook.webhookLogFilter.enabled` | boolean | `false` | Enable webhook log filtering |
| `webhook.webhookLogFilter.mode` | string | `"whitelist"` | Filter mode (whitelist/blacklist) |
| `webhook.webhookLogFilter.levels` | string[] | `["error"]` | Log levels to send |
| `webhook.webhookLogFilter.keywords` | string[] | `["starting account", "select number", "collected"]` | Keywords to filter |
| `webhook.webhookLogFilter.regexPatterns` | string[] | `[]` | Regex patterns for filtering |
> [!WARNING]
> **NTFY** users set the `webhookLogFilter` to `enabled`, or you will receive push notifications for *all* logs.
> When enabled, only account start, 2FA codes, and account completion summaries are delivered as push notifcations.
> Customize which notifications you receive with the `keywords` options.
---
## Account Setup
Edit `src/accounts.json`.
> [!WARNING]
> The file is a **flat array** of accounts, not `{ "accounts": [ ... ] }`.
> Rebuild the script after all changes.
```json
[
{
"email": "email_1",
"password": "password_1",
"totpSecret": "",
"recoveryEmail": "",
"geoLocale": "auto",
"langCode": "en",
"proxy": {
"proxyAxios": false,
"url": "",
"port": 0,
"username": "",
"password": ""
},
"saveFingerprint": {
"mobile": false,
"desktop": false
}
},
{
"email": "email_2",
"password": "password_2",
"totpSecret": "",
"recoveryEmail": "",
"geoLocale": "auto",
"langCode": "en",
"proxy": {
"proxyAxios": false,
"url": "",
"port": 0,
"username": "",
"password": ""
},
"saveFingerprint": {
"mobile": false,
"desktop": false
}
}
]
```
> [!NOTE]
> `geoLocale` uses the default locale of your Microsoft profile. You can overwrite it here with a custom locale.
> [!TIP]
> When using 2FA login, adding your `totpSecret` will enable the script to automatically generate and enter the timed 6 digit code to login. To get your `totpSecret` in your Microsoft Security settings, click 'Manage how you sign in'. Add Authenticator app, when shown the QR code, select 'enter code manually'. Use this code in the `accounts.json`.
---
## Troubleshooting
> [!TIP]
> Most login issues can be fixed by deleting your /sessions folder, and redeploying the script
---
## Disclaimer ## Disclaimer

View File

@@ -4,11 +4,11 @@ services:
container_name: microsoft-rewards-script container_name: microsoft-rewards-script
restart: unless-stopped restart: unless-stopped
# Volume mounts: Specify a location where you want to save the files on your local machine. # Create and customize your accounts.json and config.json prior to deploying the container (default location: /src/)
volumes: volumes:
- ./src/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json:ro - ./src/accounts.json:/usr/src/microsoft-rewards-script/dist/accounts.json:ro
- ./src/config.json:/usr/src/microsoft-rewards-script/dist/config.json:ro - ./src/config.json:/usr/src/microsoft-rewards-script/dist/config.json:ro
- ./sessions:/usr/src/microsoft-rewards-script/dist/browser/sessions # Optional, saves your login session - ./sessions:/usr/src/microsoft-rewards-script/dist/browser/sessions
environment: environment:
TZ: 'America/Toronto' # Set your timezone for proper scheduling TZ: 'America/Toronto' # Set your timezone for proper scheduling
@@ -16,20 +16,15 @@ services:
CRON_SCHEDULE: '0 7 * * *' # Customize your schedule, use crontab.guru for formatting CRON_SCHEDULE: '0 7 * * *' # Customize your schedule, use crontab.guru for formatting
RUN_ON_START: 'true' # Runs the script immediately on container startup RUN_ON_START: 'true' # Runs the script immediately on container startup
# Add scheduled start-time randomization (uncomment to customize or disable, default: enabled) # Add a small random delay to the scheduled start time (uncomment to customize delay, or disable)
#MIN_SLEEP_MINUTES: "5" #MIN_SLEEP_MINUTES: "5"
#MAX_SLEEP_MINUTES: "50" #MAX_SLEEP_MINUTES: "50"
SKIP_RANDOM_SLEEP: 'false' SKIP_RANDOM_SLEEP: 'false'
# Optionally set how long to wait before killing a stuck script run (prevents blocking future runs, default: 8 hours) # Set a timeout for stuck script runs (default: 8h, uncomment to customize)
#STUCK_PROCESS_TIMEOUT_HOURS: "8" #STUCK_PROCESS_TIMEOUT_HOURS: "8"
# Optional resource limits for the container # Health check: ensures cron is running, container marked unhealthy if cron stops
mem_limit: 4g
cpus: 2
# Health check - monitors if cron daemon is running to ensure scheduled jobs can execute
# Container marked unhealthy if cron process dies
healthcheck: healthcheck:
test: ['CMD', 'sh', '-c', 'pgrep cron > /dev/null || exit 1'] test: ['CMD', 'sh', '-c', 'pgrep cron > /dev/null || exit 1']
interval: 60s interval: 60s

8
flake.lock generated
View File

@@ -20,16 +20,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1749727998, "lastModified": 1767654587,
"narHash": "sha256-mHv/yeUbmL91/TvV95p+mBVahm9mdQMJoqaTVTALaFw=", "narHash": "sha256-FUB8UtsxWLWBzI7bObgQ0LEyvWL870NwUxbtqL5SK1A=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "fd487183437963a59ba763c0cc4f27e3447dd6dd", "rev": "c559c793525baabe46a431c6bc092ebcb5224a58",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-25.05", "ref": "master",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View File

@@ -1,6 +1,6 @@
{ {
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; nixpkgs.url = "github:NixOS/nixpkgs/master";
flake-utils = { flake-utils = {
url = "github:numtide/flake-utils"; url = "github:numtide/flake-utils";
}; };

430
package-lock.json generated
View File

@@ -1,20 +1,20 @@
{ {
"name": "microsoft-rewards-script", "name": "microsoft-rewards-script",
"version": "3.0.0", "version": "3.0.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "microsoft-rewards-script", "name": "microsoft-rewards-script",
"version": "3.0.0", "version": "3.0.2",
"license": "ISC", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^1.13.2", "axios": "^1.13.2",
"axios-retry": "^4.5.0", "axios-retry": "^4.5.0",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.77", "fingerprint-generator": "^2.1.79",
"fingerprint-injector": "^2.1.77", "fingerprint-injector": "^2.1.79",
"ghost-cursor-playwright-port": "^1.4.3", "ghost-cursor-playwright-port": "^1.4.3",
"http-proxy-agent": "^7.0.2", "http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
@@ -22,11 +22,15 @@
"otpauth": "^9.4.1", "otpauth": "^9.4.1",
"p-queue": "^9.0.1", "p-queue": "^9.0.1",
"patchright": "^1.57.0", "patchright": "^1.57.0",
"ts-node": "^10.9.2" "semver": "^7.7.3",
"socks-proxy-agent": "^8.0.5",
"ts-node": "^10.9.2",
"zod": "^4.3.5"
}, },
"devDependencies": { "devDependencies": {
"@types/ms": "^2.1.0", "@types/ms": "^2.1.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/semver": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^8.48.0", "@typescript-eslint/eslint-plugin": "^8.48.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-modules-newline": "^0.0.6", "eslint-plugin-modules-newline": "^0.0.6",
@@ -35,7 +39,7 @@
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=24.0.0"
} }
}, },
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
@@ -51,9 +55,9 @@
} }
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0", "version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -145,9 +149,9 @@
} }
}, },
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "3.3.1", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -157,7 +161,7 @@
"globals": "^14.0.0", "globals": "^14.0.0",
"ignore": "^5.2.0", "ignore": "^5.2.0",
"import-fresh": "^3.2.1", "import-fresh": "^3.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.1",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1" "strip-json-comments": "^3.1.1"
}, },
@@ -179,6 +183,16 @@
"concat-map": "0.0.1" "concat-map": "0.0.1"
} }
}, },
"node_modules/@eslint/eslintrc/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@eslint/eslintrc/node_modules/minimatch": { "node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -193,9 +207,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.39.1", "version": "9.39.2",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -342,13 +356,13 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.56.1", "version": "1.57.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"playwright": "1.56.1" "playwright": "1.57.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -357,25 +371,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@playwright/test/node_modules/playwright": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/@sindresorhus/is": { "node_modules/@sindresorhus/is": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -389,9 +384,9 @@
} }
}, },
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.11", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tsconfig/node12": { "node_modules/@tsconfig/node12": {
@@ -440,30 +435,37 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.10.1", "version": "24.10.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
}, },
"node_modules/@types/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.48.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz",
"integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/scope-manager": "8.51.0",
"@typescript-eslint/type-utils": "8.48.0", "@typescript-eslint/type-utils": "8.51.0",
"@typescript-eslint/utils": "8.48.0", "@typescript-eslint/utils": "8.51.0",
"@typescript-eslint/visitor-keys": "8.48.0", "@typescript-eslint/visitor-keys": "8.51.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0", "ignore": "^7.0.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.2.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -473,33 +475,23 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.48.0", "@typescript-eslint/parser": "^8.51.0",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <6.0.0"
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.48.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz",
"integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/scope-manager": "8.51.0",
"@typescript-eslint/types": "8.48.0", "@typescript-eslint/types": "8.51.0",
"@typescript-eslint/typescript-estree": "8.48.0", "@typescript-eslint/typescript-estree": "8.51.0",
"@typescript-eslint/visitor-keys": "8.48.0", "@typescript-eslint/visitor-keys": "8.51.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -515,14 +507,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.48.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz",
"integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.48.0", "@typescript-eslint/tsconfig-utils": "^8.51.0",
"@typescript-eslint/types": "^8.48.0", "@typescript-eslint/types": "^8.51.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -537,14 +529,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.48.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz",
"integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.48.0", "@typescript-eslint/types": "8.51.0",
"@typescript-eslint/visitor-keys": "8.48.0" "@typescript-eslint/visitor-keys": "8.51.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -555,9 +547,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.48.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz",
"integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -572,17 +564,17 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.48.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz",
"integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.48.0", "@typescript-eslint/types": "8.51.0",
"@typescript-eslint/typescript-estree": "8.48.0", "@typescript-eslint/typescript-estree": "8.51.0",
"@typescript-eslint/utils": "8.48.0", "@typescript-eslint/utils": "8.51.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.2.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -597,9 +589,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.48.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz",
"integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -611,21 +603,21 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.48.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz",
"integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.48.0", "@typescript-eslint/project-service": "8.51.0",
"@typescript-eslint/tsconfig-utils": "8.48.0", "@typescript-eslint/tsconfig-utils": "8.51.0",
"@typescript-eslint/types": "8.48.0", "@typescript-eslint/types": "8.51.0",
"@typescript-eslint/visitor-keys": "8.48.0", "@typescript-eslint/visitor-keys": "8.51.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"minimatch": "^9.0.4", "minimatch": "^9.0.4",
"semver": "^7.6.0", "semver": "^7.6.0",
"tinyglobby": "^0.2.15", "tinyglobby": "^0.2.15",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.2.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -639,16 +631,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.48.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz",
"integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.7.0", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/scope-manager": "8.51.0",
"@typescript-eslint/types": "8.48.0", "@typescript-eslint/types": "8.51.0",
"@typescript-eslint/typescript-estree": "8.48.0" "@typescript-eslint/typescript-estree": "8.51.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -663,13 +655,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.48.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz",
"integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.48.0", "@typescript-eslint/types": "8.51.0",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
@@ -698,6 +690,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -801,6 +794,7 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.4", "form-data": "^4.0.4",
@@ -827,9 +821,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.31", "version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
"integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.js"
@@ -862,9 +856,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.28.0", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -880,12 +874,13 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.25", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001754", "caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.249", "electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27", "node-releases": "^2.0.27",
"update-browserslist-db": "^1.1.4" "update-browserslist-db": "^1.2.0"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@@ -917,9 +912,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001757", "version": "1.0.30001762",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -1207,9 +1202,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.262", "version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
"integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/encoding-sniffer": { "node_modules/encoding-sniffer": {
@@ -1305,11 +1300,12 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.39.1", "version": "9.39.2",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -1317,7 +1313,7 @@
"@eslint/config-helpers": "^0.4.2", "@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0", "@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.39.1", "@eslint/js": "9.39.2",
"@eslint/plugin-kit": "^0.4.1", "@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
@@ -1431,6 +1427,16 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/eslint/node_modules/minimatch": { "node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -1476,9 +1482,9 @@
} }
}, },
"node_modules/esquery": { "node_modules/esquery": {
"version": "1.6.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
@@ -1597,13 +1603,13 @@
} }
}, },
"node_modules/fingerprint-generator": { "node_modules/fingerprint-generator": {
"version": "2.1.77", "version": "2.1.79",
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.77.tgz", "resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.79.tgz",
"integrity": "sha512-wR15VUEZnwozFiSDRV+40zxlEt3ZV3JNYvLx0CSF9D9smov4pUC6MJZJnlxtDr+Ir4oppU8vn1JXApLk/Qr5Uw==", "integrity": "sha512-0dr3kTgvRYHleRPp6OBDcPb8amJmOyFr9aOuwnpN6ooWJ5XyT+/aL/SZ6CU4ZrEtzV26EyJ2Lg7PT32a0NdrRA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"generative-bayesian-network": "^2.1.77", "generative-bayesian-network": "^2.1.79",
"header-generator": "^2.1.77", "header-generator": "^2.1.79",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"engines": { "engines": {
@@ -1611,12 +1617,12 @@
} }
}, },
"node_modules/fingerprint-injector": { "node_modules/fingerprint-injector": {
"version": "2.1.77", "version": "2.1.79",
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.77.tgz", "resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.79.tgz",
"integrity": "sha512-R778SIyrqgWO0P+UWKzIFWUWZz13EGu6UmV7CX3vuFDbsYIL1xiH+s+/nzPSOqFdhXyLo7d8aTOjbGbRLULoQQ==", "integrity": "sha512-0tKb3wCQ92ZlLLbxRfhoduCI+rIgpsUdb3Jp66TrwoMlSeVDlmoBZfkQW4FheQFau1kg0lIg4Zk4xetBubetQQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"fingerprint-generator": "^2.1.77", "fingerprint-generator": "^2.1.79",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"engines": { "engines": {
@@ -1677,9 +1683,9 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.4", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
@@ -1716,9 +1722,9 @@
} }
}, },
"node_modules/generative-bayesian-network": { "node_modules/generative-bayesian-network": {
"version": "2.1.77", "version": "2.1.79",
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.77.tgz", "resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.79.tgz",
"integrity": "sha512-viU4CRPsmgiklR94LhvdMndaY73BkCH1pGjmOjWbLR/ZwcUd06gKF3TCcsS3npRl74o33YSInSixxm16wIukcA==", "integrity": "sha512-aPH+V2wO+HE0BUX1LbsM8Ak99gmV43lgh+D7GDteM0zgnPqiAwcK9JZPxMPZa3aJUleFtFaL1lAei8g9zNrDIA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
@@ -1850,13 +1856,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true,
"license": "MIT"
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -1906,13 +1905,13 @@
} }
}, },
"node_modules/header-generator": { "node_modules/header-generator": {
"version": "2.1.77", "version": "2.1.79",
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.77.tgz", "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.79.tgz",
"integrity": "sha512-ggSG/mfkFMu8CO7xP591G8kp1IJCBvgXu7M8oxTjC9u914JsIzE6zIfoFsXzA+pf0utWJhUsdqU0oV/DtQ4DFQ==", "integrity": "sha512-YvHx8teq4QmV5mz7wdPMsj9n1OZBPnZxA4QE+EOrtx7xbmGvd1gBvDNKCb5XqS4GR/TL75MU5hqMqqqANdILRg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"browserslist": "^4.21.1", "browserslist": "^4.21.1",
"generative-bayesian-network": "^2.1.77", "generative-bayesian-network": "^2.1.79",
"ow": "^0.28.1", "ow": "^0.28.1",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
@@ -1990,9 +1989,9 @@
} }
}, },
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2026,6 +2025,15 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2166,11 +2174,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "11.2.2", "version": "11.2.4",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
"integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
"dev": true, "dev": true,
"license": "ISC", "license": "BlueOak-1.0.0",
"engines": { "engines": {
"node": "20 || >=22" "node": "20 || >=22"
} }
@@ -2525,6 +2533,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -2552,19 +2561,6 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/playwright-core": {
"version": "1.57.0", "version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
@@ -2588,9 +2584,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.7.1", "version": "3.7.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -2669,7 +2665,6 @@
"version": "7.7.3", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -2701,6 +2696,44 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -2744,9 +2777,9 @@
} }
}, },
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.1.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2823,6 +2856,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -2847,9 +2881,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.4", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -2905,6 +2939,7 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"iconv-lite": "0.6.3" "iconv-lite": "0.6.3"
@@ -2969,6 +3004,15 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zod": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }

View File

@@ -1,12 +1,11 @@
{ {
"name": "microsoft-rewards-script", "name": "microsoft-rewards-script",
"version": "3.0.1", "version": "3.0.2",
"description": "Automatically do tasks for Microsoft Rewards but in TS!", "description": "Automatically do tasks for Microsoft Rewards but in TS!",
"author": "Netsky", "author": "Netsky",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"main": "dist/index.js",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=24.0.0"
}, },
"scripts": { "scripts": {
"pre-build": "npm i && rimraf dist && npx patchright install chromium", "pre-build": "npm i && rimraf dist && npx patchright install chromium",
@@ -18,8 +17,10 @@
"create-docker": "docker build -t microsoft-rewards-script-docker .", "create-docker": "docker build -t microsoft-rewards-script-docker .",
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check .", "format:check": "prettier --check .",
"clear-sessions": "node ./scripts/clearSessions.js", "clear-diagnostics": "rimraf diagnostics",
"clear-diagnostics": "rimraf diagnostics" "clear-sessions": "node ./scripts/main/clearSessions.js",
"open-session": "node ./scripts/main/browserSession.js -email",
"open-session:dev": "node ./scripts/main/browserSession.js -dev -email ItsNetsky@protonmail.com"
}, },
"keywords": [ "keywords": [
"Bing Rewards", "Bing Rewards",
@@ -33,6 +34,7 @@
"devDependencies": { "devDependencies": {
"@types/ms": "^2.1.0", "@types/ms": "^2.1.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/semver": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^8.48.0", "@typescript-eslint/eslint-plugin": "^8.48.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-modules-newline": "^0.0.6", "eslint-plugin-modules-newline": "^0.0.6",
@@ -45,8 +47,8 @@
"axios-retry": "^4.5.0", "axios-retry": "^4.5.0",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.77", "fingerprint-generator": "^2.1.79",
"fingerprint-injector": "^2.1.77", "fingerprint-injector": "^2.1.79",
"ghost-cursor-playwright-port": "^1.4.3", "ghost-cursor-playwright-port": "^1.4.3",
"http-proxy-agent": "^7.0.2", "http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
@@ -54,6 +56,9 @@
"otpauth": "^9.4.1", "otpauth": "^9.4.1",
"p-queue": "^9.0.1", "p-queue": "^9.0.1",
"patchright": "^1.57.0", "patchright": "^1.57.0",
"ts-node": "^10.9.2" "semver": "^7.7.3",
"socks-proxy-agent": "^8.0.5",
"ts-node": "^10.9.2",
"zod": "^4.3.5"
} }
} }

View File

@@ -1,104 +0,0 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const projectRoot = path.resolve(__dirname, '..')
const possibleConfigPaths = [
path.join(projectRoot, 'config.json'),
path.join(projectRoot, 'src', 'config.json'),
path.join(projectRoot, 'dist', 'config.json')
]
console.log('[DEBUG] Project root:', projectRoot)
console.log('[DEBUG] Searching for config.json...')
let configPath = null
for (const p of possibleConfigPaths) {
console.log('[DEBUG] Checking:', p)
if (fs.existsSync(p)) {
configPath = p
console.log('[DEBUG] Found config at:', p)
break
}
}
if (!configPath) {
console.error('[ERROR] config.json not found in any expected location!')
console.error('[ERROR] Searched:', possibleConfigPaths)
process.exit(1)
}
console.log('[INFO] Using config:', configPath)
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
if (!config.sessionPath) {
console.error("[ERROR] config.json missing 'sessionPath' key!")
process.exit(1)
}
console.log('[INFO] Session path from config:', config.sessionPath)
const configDir = path.dirname(configPath)
const possibleSessionDirs = [
path.resolve(configDir, config.sessionPath),
path.join(projectRoot, 'src/browser', config.sessionPath),
path.join(projectRoot, 'dist/browser', config.sessionPath)
]
console.log('[DEBUG] Searching for session directory...')
let sessionDir = null
for (const p of possibleSessionDirs) {
console.log('[DEBUG] Checking:', p)
if (fs.existsSync(p)) {
sessionDir = p
console.log('[DEBUG] Found session directory at:', p)
break
}
}
if (!sessionDir) {
sessionDir = path.resolve(configDir, config.sessionPath)
console.log('[DEBUG] Using fallback session directory:', sessionDir)
}
const normalizedSessionDir = path.normalize(sessionDir)
const normalizedProjectRoot = path.normalize(projectRoot)
if (!normalizedSessionDir.startsWith(normalizedProjectRoot)) {
console.error('[ERROR] Session directory is outside project root!')
console.error('[ERROR] Project root:', normalizedProjectRoot)
console.error('[ERROR] Session directory:', normalizedSessionDir)
process.exit(1)
}
if (normalizedSessionDir === normalizedProjectRoot) {
console.error('[ERROR] Session directory cannot be the project root!')
process.exit(1)
}
const pathSegments = normalizedSessionDir.split(path.sep)
if (pathSegments.length < 3) {
console.error('[ERROR] Session path is too shallow (safety check failed)!')
console.error('[ERROR] Path:', normalizedSessionDir)
process.exit(1)
}
if (fs.existsSync(sessionDir)) {
console.log('[INFO] Removing session folder:', sessionDir)
try {
fs.rmSync(sessionDir, { recursive: true, force: true })
console.log('[SUCCESS] Session folder removed successfully')
} catch (error) {
console.error('[ERROR] Failed to remove session folder:', error.message)
process.exit(1)
}
} else {
console.log('[INFO] Session folder does not exist:', sessionDir)
}
console.log('[INFO] Done.')

View File

@@ -0,0 +1,168 @@
import fs from 'fs'
import { chromium } from 'patchright'
import { newInjectedContext } from 'fingerprint-injector'
import {
getDirname,
getProjectRoot,
log,
parseArgs,
validateEmail,
loadConfig,
loadAccounts,
findAccountByEmail,
getRuntimeBase,
getSessionPath,
loadCookies,
loadFingerprint,
buildProxyConfig,
setupCleanupHandlers
} from '../utils.js'
const __dirname = getDirname(import.meta.url)
const projectRoot = getProjectRoot(__dirname)
const args = parseArgs()
args.dev = args.dev || false
validateEmail(args.email)
const { data: config } = loadConfig(projectRoot, args.dev)
const { data: accounts } = loadAccounts(projectRoot, args.dev)
const account = findAccountByEmail(accounts, args.email)
if (!account) {
log('ERROR', `Account not found: ${args.email}`)
log('ERROR', 'Available accounts:')
accounts.forEach(acc => {
if (acc?.email) log('ERROR', ` - ${acc.email}`)
})
process.exit(1)
}
async function main() {
const runtimeBase = getRuntimeBase(projectRoot, args.dev)
const sessionBase = getSessionPath(runtimeBase, config.sessionPath, args.email)
log('INFO', 'Validating session data...')
if (!fs.existsSync(sessionBase)) {
log('ERROR', `Session directory does not exist: ${sessionBase}`)
log('ERROR', 'Please ensure the session has been created for this account')
process.exit(1)
}
if (!config.baseURL) {
log('ERROR', 'baseURL is not set in config.json')
process.exit(1)
}
let cookies = await loadCookies(sessionBase, 'desktop')
let sessionType = 'desktop'
if (cookies.length === 0) {
log('WARN', 'No desktop session cookies found, trying mobile session...')
cookies = await loadCookies(sessionBase, 'mobile')
sessionType = 'mobile'
if (cookies.length === 0) {
log('ERROR', 'No cookies found in desktop or mobile session')
log('ERROR', `Session directory: ${sessionBase}`)
log('ERROR', 'Please ensure a valid session exists for this account')
process.exit(1)
}
log('INFO', `Using mobile session (${cookies.length} cookies)`)
}
const isMobile = sessionType === 'mobile'
const fingerprintEnabled = isMobile ? account.saveFingerprint?.mobile : account.saveFingerprint?.desktop
let fingerprint = null
if (fingerprintEnabled) {
fingerprint = await loadFingerprint(sessionBase, sessionType)
if (!fingerprint) {
log('ERROR', `Fingerprint is enabled for ${sessionType} but fingerprint file not found`)
log('ERROR', `Expected file: ${sessionBase}/session_fingerprint_${sessionType}.json`)
log('ERROR', 'Cannot start browser without fingerprint when it is explicitly enabled')
process.exit(1)
}
log('INFO', `Loaded ${sessionType} fingerprint`)
}
const proxy = buildProxyConfig(account)
if (account.proxy && account.proxy.url && (!proxy || !proxy.server)) {
log('ERROR', 'Proxy is configured in account but proxy data is invalid or incomplete')
log('ERROR', 'Account proxy config:', JSON.stringify(account.proxy, null, 2))
log('ERROR', 'Required fields: proxy.url, proxy.port')
log('ERROR', 'Cannot start browser without proxy when it is explicitly configured')
process.exit(1)
}
const userAgent = fingerprint?.fingerprint?.navigator?.userAgent || fingerprint?.fingerprint?.userAgent || null
log('INFO', `Session: ${args.email} (${sessionType})`)
log('INFO', ` Cookies: ${cookies.length}`)
log('INFO', ` Fingerprint: ${fingerprint ? 'Yes' : 'No'}`)
log('INFO', ` User-Agent: ${userAgent || 'Default'}`)
log('INFO', ` Proxy: ${proxy ? 'Yes' : 'No'}`)
log('INFO', 'Launching browser...')
const browser = await chromium.launch({
headless: false,
...(proxy ? { proxy } : {}),
args: [
'--no-sandbox',
'--mute-audio',
'--disable-setuid-sandbox',
'--ignore-certificate-errors',
'--ignore-certificate-errors-spki-list',
'--ignore-ssl-errors',
'--no-first-run',
'--no-default-browser-check',
'--disable-user-media-security=true',
'--disable-blink-features=Attestation',
'--disable-features=WebAuthentication,PasswordManagerOnboarding,PasswordManager,EnablePasswordsAccountStorage,Passkeys',
'--disable-save-password-bubble'
]
})
let context
if (fingerprint) {
context = await newInjectedContext(browser, { fingerprint })
await context.addInitScript(() => {
Object.defineProperty(navigator, 'credentials', {
value: {
create: () => Promise.reject(new Error('WebAuthn disabled')),
get: () => Promise.reject(new Error('WebAuthn disabled'))
}
})
})
log('SUCCESS', 'Fingerprint injected into browser context')
} else {
context = await browser.newContext({
viewport: isMobile ? { width: 375, height: 667 } : { width: 1366, height: 768 }
})
}
if (cookies.length) {
await context.addCookies(cookies)
log('INFO', `Added ${cookies.length} cookies to context`)
}
const page = await context.newPage()
await page.goto(config.baseURL, { waitUntil: 'domcontentloaded' })
log('SUCCESS', 'Browser opened with session loaded')
log('INFO', `Navigated to: ${config.baseURL}`)
setupCleanupHandlers(async () => {
if (browser?.isConnected?.()) {
await browser.close()
}
})
}
main()

View File

@@ -0,0 +1,67 @@
import path from 'path'
import fs from 'fs'
import {
getDirname,
getProjectRoot,
log,
loadJsonFile,
safeRemoveDirectory
} from '../utils.js'
const __dirname = getDirname(import.meta.url)
const projectRoot = getProjectRoot(__dirname)
const possibleConfigPaths = [
path.join(projectRoot, 'config.json'),
path.join(projectRoot, 'src', 'config.json'),
path.join(projectRoot, 'dist', 'config.json')
]
log('DEBUG', 'Project root:', projectRoot)
log('DEBUG', 'Searching for config.json...')
const configResult = loadJsonFile(possibleConfigPaths, true)
const config = configResult.data
const configPath = configResult.path
log('INFO', 'Using config:', configPath)
if (!config.sessionPath) {
log('ERROR', 'Invalid config.json - missing required field: sessionPath')
log('ERROR', `Config file: ${configPath}`)
process.exit(1)
}
log('INFO', 'Session path from config:', config.sessionPath)
const configDir = path.dirname(configPath)
const possibleSessionDirs = [
path.resolve(configDir, config.sessionPath),
path.join(projectRoot, 'src/browser', config.sessionPath),
path.join(projectRoot, 'dist/browser', config.sessionPath)
]
log('DEBUG', 'Searching for session directory...')
let sessionDir = null
for (const p of possibleSessionDirs) {
log('DEBUG', 'Checking:', p)
if (fs.existsSync(p)) {
sessionDir = p
log('DEBUG', 'Found session directory at:', p)
break
}
}
if (!sessionDir) {
sessionDir = path.resolve(configDir, config.sessionPath)
log('DEBUG', 'Using fallback session directory:', sessionDir)
}
const success = safeRemoveDirectory(sessionDir, projectRoot)
if (!success) {
process.exit(1)
}
log('INFO', 'Done.')

269
scripts/utils.js Normal file
View File

@@ -0,0 +1,269 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
export function getDirname(importMetaUrl) {
const __filename = fileURLToPath(importMetaUrl)
return path.dirname(__filename)
}
export function getProjectRoot(currentDir) {
let dir = currentDir
while (dir !== path.parse(dir).root) {
if (fs.existsSync(path.join(dir, 'package.json'))) {
return dir
}
dir = path.dirname(dir)
}
throw new Error('Could not find project root (package.json not found)')
}
export function log(level, ...args) {
console.log(`[${level}]`, ...args)
}
export function parseArgs(argv = process.argv.slice(2)) {
const args = {}
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]
if (arg.startsWith('-')) {
const key = arg.substring(1)
if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
args[key] = argv[i + 1]
i++
} else {
args[key] = true
}
}
}
return args
}
export function validateEmail(email) {
if (!email) {
log('ERROR', 'Missing -email argument')
log('ERROR', 'Usage: node script.js -email you@example.com')
process.exit(1)
}
if (typeof email !== 'string') {
log('ERROR', `Invalid email type: expected string, got ${typeof email}`)
log('ERROR', 'Usage: node script.js -email you@example.com')
process.exit(1)
}
if (!email.includes('@')) {
log('ERROR', `Invalid email format: "${email}"`)
log('ERROR', 'Email must contain "@" symbol')
log('ERROR', 'Example: you@example.com')
process.exit(1)
}
return email
}
export function loadJsonFile(possiblePaths, required = true) {
for (const filePath of possiblePaths) {
if (fs.existsSync(filePath)) {
try {
const content = fs.readFileSync(filePath, 'utf8')
return { data: JSON.parse(content), path: filePath }
} catch (error) {
log('ERROR', `Failed to parse JSON file: ${filePath}`)
log('ERROR', `Parse error: ${error.message}`)
if (required) process.exit(1)
return null
}
}
}
if (required) {
log('ERROR', 'Required file not found')
log('ERROR', 'Searched in the following locations:')
possiblePaths.forEach(p => log('ERROR', ` - ${p}`))
process.exit(1)
}
return null
}
export function loadConfig(projectRoot, isDev = false) {
const possiblePaths = isDev
? [path.join(projectRoot, 'src', 'config.json')]
: [
path.join(projectRoot, 'dist', 'config.json'),
path.join(projectRoot, 'config.json')
]
const result = loadJsonFile(possiblePaths, true)
const missingFields = []
if (!result.data.baseURL) missingFields.push('baseURL')
if (!result.data.sessionPath) missingFields.push('sessionPath')
if (result.data.headless === undefined) missingFields.push('headless')
if (!result.data.workers) missingFields.push('workers')
if (missingFields.length > 0) {
log('ERROR', 'Invalid config.json - missing required fields:')
missingFields.forEach(field => log('ERROR', ` - ${field}`))
log('ERROR', `Config file: ${result.path}`)
process.exit(1)
}
return result
}
export function loadAccounts(projectRoot, isDev = false) {
const possiblePaths = isDev
? [path.join(projectRoot, 'src', 'accounts.dev.json')]
: [
path.join(projectRoot, 'dist', 'accounts.json'),
path.join(projectRoot, 'accounts.json'),
path.join(projectRoot, 'accounts.example.json')
]
return loadJsonFile(possiblePaths, true)
}
export function findAccountByEmail(accounts, email) {
if (!email || typeof email !== 'string') return null
return accounts.find(a => a?.email && typeof a.email === 'string' && a.email.toLowerCase() === email.toLowerCase()) || null
}
export function getRuntimeBase(projectRoot, isDev = false) {
return path.join(projectRoot, isDev ? 'src' : 'dist')
}
export function getSessionPath(runtimeBase, sessionPath, email) {
return path.join(runtimeBase, 'browser', sessionPath, email)
}
export async function loadCookies(sessionBase, type = 'desktop') {
const cookiesFile = path.join(sessionBase, `session_${type}.json`)
if (!fs.existsSync(cookiesFile)) {
return []
}
try {
const content = await fs.promises.readFile(cookiesFile, 'utf8')
return JSON.parse(content)
} catch (error) {
log('WARN', `Failed to load cookies from: ${cookiesFile}`)
log('WARN', `Error: ${error.message}`)
return []
}
}
export async function loadFingerprint(sessionBase, type = 'desktop') {
const fpFile = path.join(sessionBase, `session_fingerprint_${type}.json`)
if (!fs.existsSync(fpFile)) {
return null
}
try {
const content = await fs.promises.readFile(fpFile, 'utf8')
return JSON.parse(content)
} catch (error) {
log('WARN', `Failed to load fingerprint from: ${fpFile}`)
log('WARN', `Error: ${error.message}`)
return null
}
}
export function getUserAgent(fingerprint) {
if (!fingerprint) return null
return fingerprint?.fingerprint?.userAgent || fingerprint?.userAgent || null
}
export function buildProxyConfig(account) {
if (!account.proxy || !account.proxy.url || !account.proxy.port) {
return null
}
const proxy = {
server: `${account.proxy.url}:${account.proxy.port}`
}
if (account.proxy.username && account.proxy.password) {
proxy.username = account.proxy.username
proxy.password = account.proxy.password
}
return proxy
}
export function setupCleanupHandlers(cleanupFn) {
const cleanup = async () => {
try {
await cleanupFn()
} catch (error) {
log('ERROR', 'Cleanup failed:', error.message)
}
process.exit(0)
}
process.on('SIGINT', cleanup)
process.on('SIGTERM', cleanup)
}
export function validateDeletionPath(targetPath, projectRoot) {
const normalizedTarget = path.normalize(targetPath)
const normalizedRoot = path.normalize(projectRoot)
if (!normalizedTarget.startsWith(normalizedRoot)) {
return {
valid: false,
error: 'Path is outside project root'
}
}
if (normalizedTarget === normalizedRoot) {
return {
valid: false,
error: 'Cannot delete project root'
}
}
const pathSegments = normalizedTarget.split(path.sep)
if (pathSegments.length < 3) {
return {
valid: false,
error: 'Path is too shallow (safety check failed)'
}
}
return { valid: true, error: null }
}
export function safeRemoveDirectory(dirPath, projectRoot) {
const validation = validateDeletionPath(dirPath, projectRoot)
if (!validation.valid) {
log('ERROR', 'Directory deletion failed - safety check:')
log('ERROR', ` Reason: ${validation.error}`)
log('ERROR', ` Target: ${dirPath}`)
log('ERROR', ` Project root: ${projectRoot}`)
return false
}
if (!fs.existsSync(dirPath)) {
log('INFO', `Directory does not exist: ${dirPath}`)
return true
}
try {
fs.rmSync(dirPath, { recursive: true, force: true })
log('SUCCESS', `Directory removed: ${dirPath}`)
return true
} catch (error) {
log('ERROR', `Failed to remove directory: ${dirPath}`)
log('ERROR', `Error: ${error.message}`)
return false
}
}

View File

@@ -2,27 +2,39 @@
{ {
"email": "email_1", "email": "email_1",
"password": "password_1", "password": "password_1",
"totp": "", "totpSecret": "",
"recoveryEmail": "",
"geoLocale": "auto", "geoLocale": "auto",
"langCode": "en",
"proxy": { "proxy": {
"proxyAxios": true, "proxyAxios": false,
"url": "", "url": "",
"port": 0, "port": 0,
"username": "", "username": "",
"password": "" "password": ""
},
"saveFingerprint": {
"mobile": false,
"desktop": false
} }
}, },
{ {
"email": "email_2", "email": "email_2",
"password": "password_2", "password": "password_2",
"totp": "", "totpSecret": "",
"recoveryEmail": "",
"geoLocale": "auto", "geoLocale": "auto",
"langCode": "en",
"proxy": { "proxy": {
"proxyAxios": true, "proxyAxios": false,
"url": "", "url": "",
"port": 0, "port": 0,
"username": "", "username": "",
"password": "" "password": ""
},
"saveFingerprint": {
"mobile": false,
"desktop": false
} }
} }
] ]

View File

@@ -1,5 +1,4 @@
import rebrowser, { BrowserContext } from 'patchright' import rebrowser, { BrowserContext } from 'patchright'
import { newInjectedContext } from 'fingerprint-injector' import { newInjectedContext } from 'fingerprint-injector'
import { BrowserFingerprintWithHeaders, FingerprintGenerator } from 'fingerprint-generator' import { BrowserFingerprintWithHeaders, FingerprintGenerator } from 'fingerprint-generator'
@@ -7,7 +6,7 @@ import type { MicrosoftRewardsBot } from '../index'
import { loadSessionData, saveFingerprintData } from '../util/Load' import { loadSessionData, saveFingerprintData } from '../util/Load'
import { UserAgentManager } from './UserAgent' import { UserAgentManager } from './UserAgent'
import type { AccountProxy } from '../interface/Account' import type { Account, AccountProxy } from '../interface/Account'
/* Test Stuff /* Test Stuff
https://abrahamjuliot.github.io/creepjs/ https://abrahamjuliot.github.io/creepjs/
@@ -17,28 +16,14 @@ https://pixelscan.net/
https://www.browserscan.net/ https://www.browserscan.net/
*/ */
class Browser { interface BrowserCreationResult {
private bot: MicrosoftRewardsBot
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
async createBrowser(
proxy: AccountProxy,
email: string
): Promise<{
context: BrowserContext context: BrowserContext
fingerprint: BrowserFingerprintWithHeaders fingerprint: BrowserFingerprintWithHeaders
}> { }
let browser: rebrowser.Browser
try { class Browser {
browser = await rebrowser.chromium.launch({ private readonly bot: MicrosoftRewardsBot
headless: this.bot.config.headless, private static readonly BROWSER_ARGS = [
...(proxy.url && {
proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` }
}),
args: [
'--no-sandbox', '--no-sandbox',
'--mute-audio', '--mute-audio',
'--disable-setuid-sandbox', '--disable-setuid-sandbox',
@@ -51,29 +36,48 @@ class Browser {
'--disable-blink-features=Attestation', '--disable-blink-features=Attestation',
'--disable-features=WebAuthentication,PasswordManagerOnboarding,PasswordManager,EnablePasswordsAccountStorage,Passkeys', '--disable-features=WebAuthentication,PasswordManagerOnboarding,PasswordManager,EnablePasswordsAccountStorage,Passkeys',
'--disable-save-password-bubble' '--disable-save-password-bubble'
] ] as const
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
async createBrowser(account: Account): Promise<BrowserCreationResult> {
let browser: rebrowser.Browser
try {
const proxyConfig = account.proxy.url
? {
server: this.formatProxyServer(account.proxy),
...(account.proxy.username &&
account.proxy.password && {
username: account.proxy.username,
password: account.proxy.password
})
}
: undefined
browser = await rebrowser.chromium.launch({
headless: this.bot.config.headless,
...(proxyConfig && { proxy: proxyConfig }),
args: [...Browser.BROWSER_ARGS]
}) })
} catch (error) { } catch (error) {
this.bot.logger.error( const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.isMobile, this.bot.logger.error(this.bot.isMobile, 'BROWSER', `Launch failed: ${errorMessage}`)
'BROWSER',
`Launch failed: ${error instanceof Error ? error.message : String(error)}`
)
throw error throw error
} }
try {
const sessionData = await loadSessionData( const sessionData = await loadSessionData(
this.bot.config.sessionPath, this.bot.config.sessionPath,
email, account.email,
this.bot.config.saveFingerprint, account.saveFingerprint,
this.bot.isMobile this.bot.isMobile
) )
const fingerprint = sessionData.fingerprint const fingerprint = sessionData.fingerprint ?? (await this.generateFingerprint(this.bot.isMobile))
? sessionData.fingerprint
: await this.generateFingerprint(this.bot.isMobile)
const context = await newInjectedContext(browser as any, { fingerprint: fingerprint }) const context = await newInjectedContext(browser as any, { fingerprint })
await context.addInitScript(() => { await context.addInitScript(() => {
Object.defineProperty(navigator, 'credentials', { Object.defineProperty(navigator, 'credentials', {
@@ -88,8 +92,11 @@ class Browser {
await context.addCookies(sessionData.cookies) await context.addCookies(sessionData.cookies)
if (this.bot.config.saveFingerprint) { if (
await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerprint) (account.saveFingerprint.mobile && this.bot.isMobile) ||
(account.saveFingerprint.desktop && !this.bot.isMobile)
) {
await saveFingerprintData(this.bot.config.sessionPath, account.email, this.bot.isMobile, fingerprint)
} }
this.bot.logger.info( this.bot.logger.info(
@@ -97,12 +104,22 @@ class Browser {
'BROWSER', 'BROWSER',
`Created browser with User-Agent: "${fingerprint.fingerprint.navigator.userAgent}"` `Created browser with User-Agent: "${fingerprint.fingerprint.navigator.userAgent}"`
) )
this.bot.logger.debug(this.bot.isMobile, 'BROWSER-FINGERPRINT', JSON.stringify(fingerprint)) this.bot.logger.debug(this.bot.isMobile, 'BROWSER-FINGERPRINT', JSON.stringify(fingerprint))
return { return { context: context as unknown as BrowserContext, fingerprint }
context: context as unknown as BrowserContext, } catch (error) {
fingerprint: fingerprint await browser.close().catch(() => {})
throw error
}
}
private formatProxyServer(proxy: AccountProxy): string {
try {
const urlObj = new URL(proxy.url)
const protocol = urlObj.protocol.replace(':', '')
return `${protocol}://${urlObj.hostname}:${proxy.port}`
} catch {
return `${proxy.url}:${proxy.port}`
} }
} }

View File

@@ -23,12 +23,27 @@ export default class BrowserFunc {
*/ */
async getDashboardData(): Promise<DashboardData> { async getDashboardData(): Promise<DashboardData> {
try { try {
const cookieHeader = this.bot.cookies.mobile const allowedDomains = ['bing.com', 'live.com', 'microsoftonline.com'];
.map((c: { name: string; value: string }) => `${c.name}=${c.value}`)
.join('; ') const cookieHeader = [
...new Map(
this.bot.cookies.mobile
.filter(
(c: { name: string; value: string; domain?: string }) =>
typeof c.domain === 'string' &&
allowedDomains.some(d =>
c.domain && c.domain.toLowerCase().endsWith(d)
)
)
.map(c => [c.name, c]) // dedupe by name, keep last
).values()
]
.map(c => `${c.name}=${c.value}`)
.join('; ');
const request: AxiosRequestConfig = { const request: AxiosRequestConfig = {
url: `https://rewards.bing.com/api/getuserinfo?type=1&X-Requested-With=XMLHttpRequest&_=${Date.now()}`, url: 'https://rewards.bing.com/api/getuserinfo?type=1',
method: 'GET', method: 'GET',
headers: { headers: {
...(this.bot.fingerprint?.headers ?? {}), ...(this.bot.fingerprint?.headers ?? {}),

View File

@@ -2,25 +2,31 @@ import type { Page } from 'patchright'
import type { MicrosoftRewardsBot } from '../../index' import type { MicrosoftRewardsBot } from '../../index'
import { saveSessionData } from '../../util/Load' import { saveSessionData } from '../../util/Load'
// Methods
import { MobileAccessLogin } from './methods/MobileAccessLogin' import { MobileAccessLogin } from './methods/MobileAccessLogin'
import { EmailLogin } from './methods/EmailLogin' import { EmailLogin } from './methods/EmailLogin'
import { PasswordlessLogin } from './methods/PasswordlessLogin' import { PasswordlessLogin } from './methods/PasswordlessLogin'
import { TotpLogin } from './methods/Totp2FALogin' import { TotpLogin } from './methods/Totp2FALogin'
import { CodeLogin } from './methods/GetACodeLogin'
import { RecoveryLogin } from './methods/RecoveryEmailLogin'
import type { Account } from '../../interface/Account'
type LoginState = type LoginState =
| 'EMAIL_INPUT' | 'EMAIL_INPUT'
| 'PASSWORD_INPUT' | 'PASSWORD_INPUT'
| 'SIGN_IN_ANOTHER_WAY' | 'SIGN_IN_ANOTHER_WAY'
| 'SIGN_IN_ANOTHER_WAY_EMAIL'
| 'PASSKEY_ERROR' | 'PASSKEY_ERROR'
| 'PASSKEY_VIDEO' | 'PASSKEY_VIDEO'
| 'KMSI_PROMPT' | 'KMSI_PROMPT'
| 'LOGGED_IN' | 'LOGGED_IN'
| 'RECOVERY_EMAIL_INPUT'
| 'ACCOUNT_LOCKED' | 'ACCOUNT_LOCKED'
| 'ERROR_ALERT' | 'ERROR_ALERT'
| '2FA_TOTP' | '2FA_TOTP'
| 'LOGIN_PASSWORDLESS' | 'LOGIN_PASSWORDLESS'
| 'GET_A_CODE' | 'GET_A_CODE'
| 'GET_A_CODE_2'
| 'UNKNOWN' | 'UNKNOWN'
| 'CHROMEWEBDATA_ERROR' | 'CHROMEWEBDATA_ERROR'
@@ -28,28 +34,52 @@ export class Login {
emailLogin: EmailLogin emailLogin: EmailLogin
passwordlessLogin: PasswordlessLogin passwordlessLogin: PasswordlessLogin
totp2FALogin: TotpLogin totp2FALogin: TotpLogin
codeLogin: CodeLogin
recoveryLogin: RecoveryLogin
private readonly selectors = {
primaryButton: 'button[data-testid="primaryButton"]',
secondaryButton: 'button[data-testid="secondaryButton"]',
emailIcon: '[data-testid="tile"]:has(svg path[d*="M5.25 4h13.5a3.25"])',
emailIconOld: 'img[data-testid="accessibleImg"][src*="picker_verify_email"]',
recoveryEmail: '[data-testid="proof-confirmation"]',
passwordIcon: '[data-testid="tile"]:has(svg path[d*="M11.78 10.22a.75.75"])',
accountLocked: '#serviceAbuseLandingTitle',
errorAlert: 'div[role="alert"]',
passwordEntry: '[data-testid="passwordEntry"]',
emailEntry: 'input#usernameEntry',
kmsiVideo: '[data-testid="kmsiVideo"]',
passKeyVideo: '[data-testid="biometricVideo"]',
passKeyError: '[data-testid="registrationImg"]',
passwordlessCheck: '[data-testid="deviceShieldCheckmarkVideo"]',
totpInput: 'input[name="otc"]',
totpInputOld: 'form[name="OneTimeCodeViewForm"]',
identityBanner: '[data-testid="identityBanner"]',
viewFooter: '[data-testid="viewFooter"] >> [role="button"]',
bingProfile: '#id_n',
requestToken: 'input[name="__RequestVerificationToken"]',
requestTokenMeta: 'meta[name="__RequestVerificationToken"]'
} as const
constructor(private bot: MicrosoftRewardsBot) { constructor(private bot: MicrosoftRewardsBot) {
this.emailLogin = new EmailLogin(this.bot) this.emailLogin = new EmailLogin(this.bot)
this.passwordlessLogin = new PasswordlessLogin(this.bot) this.passwordlessLogin = new PasswordlessLogin(this.bot)
this.totp2FALogin = new TotpLogin(this.bot) this.totp2FALogin = new TotpLogin(this.bot)
this.codeLogin = new CodeLogin(this.bot)
this.recoveryLogin = new RecoveryLogin(this.bot)
} }
private readonly primaryButtonSelector = 'button[data-testid="primaryButton"]' async login(page: Page, account: Account) {
private readonly secondaryButtonSelector = 'button[data-testid="secondaryButton"]'
async login(page: Page, email: string, password: string, totpSecret?: string) {
try { try {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting login process') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting login process')
await page.goto('https://www.bing.com/rewards/dashboard', { waitUntil: 'domcontentloaded' }).catch(() => {}) await page.goto('https://www.bing.com/rewards/dashboard', { waitUntil: 'domcontentloaded' }).catch(() => {})
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
await this.bot.browser.utils.reloadBadPage(page) await this.bot.browser.utils.reloadBadPage(page)
await this.bot.browser.utils.disableFido(page) await this.bot.browser.utils.disableFido(page)
const maxIterations = 25 const maxIterations = 25
let iteration = 0 let iteration = 0
let previousState: LoginState = 'UNKNOWN' let previousState: LoginState = 'UNKNOWN'
let sameStateCount = 0 let sameStateCount = 0
@@ -59,7 +89,7 @@ export class Login {
iteration++ iteration++
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `State check iteration ${iteration}/${maxIterations}`) this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `State check iteration ${iteration}/${maxIterations}`)
const state = await this.detectCurrentState(page) const state = await this.detectCurrentState(page, account)
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `Current state: ${state}`) this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `Current state: ${state}`)
if (state !== previousState && previousState !== 'UNKNOWN') { if (state !== previousState && previousState !== 'UNKNOWN') {
@@ -68,11 +98,16 @@ export class Login {
if (state === previousState && state !== 'LOGGED_IN' && state !== 'UNKNOWN') { if (state === previousState && state !== 'LOGGED_IN' && state !== 'UNKNOWN') {
sameStateCount++ sameStateCount++
this.bot.logger.debug(
this.bot.isMobile,
'LOGIN',
`Same state count: ${sameStateCount}/4 for state "${state}"`
)
if (sameStateCount >= 4) { if (sameStateCount >= 4) {
this.bot.logger.warn( this.bot.logger.warn(
this.bot.isMobile, this.bot.isMobile,
'LOGIN', 'LOGIN',
`Stuck in state "${state}" for 4 loops. Refreshing page...` `Stuck in state "${state}" for 4 loops, refreshing page`
) )
await page.reload({ waitUntil: 'domcontentloaded' }) await page.reload({ waitUntil: 'domcontentloaded' })
await this.bot.utils.wait(3000) await this.bot.utils.wait(3000)
@@ -90,8 +125,7 @@ export class Login {
break break
} }
const shouldContinue = await this.handleState(state, page, email, password, totpSecret) const shouldContinue = await this.handleState(state, page, account)
if (!shouldContinue) { if (!shouldContinue) {
throw new Error(`Login failed or aborted at state: ${state}`) throw new Error(`Login failed or aborted at state: ${state}`)
} }
@@ -103,142 +137,151 @@ export class Login {
throw new Error('Login timeout: exceeded maximum iterations') throw new Error('Login timeout: exceeded maximum iterations')
} }
await this.finalizeLogin(page, email) await this.finalizeLogin(page, account.email)
} catch (error) { } catch (error) {
this.bot.logger.error( this.bot.logger.error(
this.bot.isMobile, this.bot.isMobile,
'LOGIN', 'LOGIN',
`An error occurred: ${error instanceof Error ? error.message : String(error)}` `Fatal error: ${error instanceof Error ? error.message : String(error)}`
) )
throw error throw error
} }
} }
private async detectCurrentState(page: Page): Promise<LoginState> { private async detectCurrentState(page: Page, account?: Account): Promise<LoginState> {
// Make sure we settled before getting a URL
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
const url = new URL(page.url()) const url = new URL(page.url())
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Current URL: ${url.hostname}${url.pathname}`)
this.bot.logger.debug(this.bot.isMobile, 'DETECT-CURRENT-STATE', `Current URL: ${url}`)
if (url.hostname === 'chromewebdata') { if (url.hostname === 'chromewebdata') {
this.bot.logger.warn(this.bot.isMobile, 'DETECT-CURRENT-STATE', 'Detected chromewebdata error page') this.bot.logger.warn(this.bot.isMobile, 'DETECT-STATE', 'Detected chromewebdata error page')
return 'CHROMEWEBDATA_ERROR' return 'CHROMEWEBDATA_ERROR'
} }
const isLocked = await page const isLocked = await this.checkSelector(page, this.selectors.accountLocked)
.waitForSelector('#serviceAbuseLandingTitle', { state: 'visible', timeout: 200 })
.then(() => true)
.catch(() => false)
if (isLocked) { if (isLocked) {
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'Account locked selector found')
return 'ACCOUNT_LOCKED' return 'ACCOUNT_LOCKED'
} }
// If instantly loading rewards dash, logged in if (url.hostname === 'rewards.bing.com' || url.hostname === 'account.microsoft.com') {
if (url.hostname === 'rewards.bing.com') { this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'On rewards/account page, assuming logged in')
return 'LOGGED_IN' return 'LOGGED_IN'
} }
// If account dash, logged in const stateChecks: Array<[string, LoginState]> = [
if (url.hostname === 'account.microsoft.com') { [this.selectors.errorAlert, 'ERROR_ALERT'],
return 'LOGGED_IN' [this.selectors.passwordEntry, 'PASSWORD_INPUT'],
[this.selectors.emailEntry, 'EMAIL_INPUT'],
[this.selectors.recoveryEmail, 'RECOVERY_EMAIL_INPUT'],
[this.selectors.kmsiVideo, 'KMSI_PROMPT'],
[this.selectors.passKeyVideo, 'PASSKEY_VIDEO'],
[this.selectors.passKeyError, 'PASSKEY_ERROR'],
[this.selectors.passwordIcon, 'SIGN_IN_ANOTHER_WAY'],
[this.selectors.emailIcon, 'SIGN_IN_ANOTHER_WAY_EMAIL'],
[this.selectors.emailIconOld, 'SIGN_IN_ANOTHER_WAY_EMAIL'],
[this.selectors.passwordlessCheck, 'LOGIN_PASSWORDLESS'],
[this.selectors.totpInput, '2FA_TOTP'],
[this.selectors.totpInputOld, '2FA_TOTP']
]
const results = await Promise.all(
stateChecks.map(async ([sel, state]) => {
const visible = await this.checkSelector(page, sel)
return visible ? state : null
})
)
const visibleStates = results.filter((s): s is LoginState => s !== null)
if (visibleStates.length > 0) {
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Visible states: [${visibleStates.join(', ')}]`)
} }
const check = async (selector: string, state: LoginState): Promise<LoginState | null> => { const [identityBanner, primaryButton, passwordEntry] = await Promise.all([
return page this.checkSelector(page, this.selectors.identityBanner),
.waitForSelector(selector, { state: 'visible', timeout: 200 }) this.checkSelector(page, this.selectors.primaryButton),
.then(visible => (visible ? state : null)) this.checkSelector(page, this.selectors.passwordEntry)
.catch(() => null)
}
const results = await Promise.all([
check('div[role="alert"]', 'ERROR_ALERT'),
check('[data-testid="passwordEntry"]', 'PASSWORD_INPUT'),
check('input#usernameEntry', 'EMAIL_INPUT'),
check('[data-testid="kmsiVideo"]', 'KMSI_PROMPT'),
check('[data-testid="biometricVideo"]', 'PASSKEY_VIDEO'),
check('[data-testid="registrationImg"]', 'PASSKEY_ERROR'),
check('[data-testid="tile"]:has(svg path[d*="M11.78 10.22a.75.75"])', 'SIGN_IN_ANOTHER_WAY'),
check('[data-testid="deviceShieldCheckmarkVideo"]', 'LOGIN_PASSWORDLESS'),
check('input[name="otc"]', '2FA_TOTP'),
check('form[name="OneTimeCodeViewForm"]', '2FA_TOTP')
]) ])
// Get a code
const identityBanner = await page
.waitForSelector('[data-testid="identityBanner"]', { state: 'visible', timeout: 200 })
.then(() => true)
.catch(() => false)
const primaryButton = await page
.waitForSelector(this.primaryButtonSelector, { state: 'visible', timeout: 200 })
.then(() => true)
.catch(() => false)
const passwordEntry = await page
.waitForSelector('[data-testid="passwordEntry"]', { state: 'visible', timeout: 200 })
.then(() => true)
.catch(() => false)
if (identityBanner && primaryButton && !passwordEntry && !results.includes('2FA_TOTP')) { if (identityBanner && primaryButton && !passwordEntry && !results.includes('2FA_TOTP')) {
results.push('GET_A_CODE') // Lower prio const codeState = account?.password ? 'GET_A_CODE' : 'GET_A_CODE_2'
this.bot.logger.debug(
this.bot.isMobile,
'DETECT-STATE',
`Get code state detected: ${codeState} (has password: ${!!account?.password})`
)
results.push(codeState)
} }
// Final
let foundStates = results.filter((s): s is LoginState => s !== null) let foundStates = results.filter((s): s is LoginState => s !== null)
if (foundStates.length === 0) return 'UNKNOWN' if (foundStates.length === 0) {
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'No matching states found')
return 'UNKNOWN'
}
if (foundStates.includes('ERROR_ALERT')) { if (foundStates.includes('ERROR_ALERT')) {
this.bot.logger.debug(
this.bot.isMobile,
'DETECT-STATE',
`ERROR_ALERT found - hostname: ${url.hostname}, has 2FA: ${foundStates.includes('2FA_TOTP')}`
)
if (url.hostname !== 'login.live.com') { if (url.hostname !== 'login.live.com') {
// Remove ERROR_ALERT if not on login.live.com
foundStates = foundStates.filter(s => s !== 'ERROR_ALERT') foundStates = foundStates.filter(s => s !== 'ERROR_ALERT')
} }
if (foundStates.includes('2FA_TOTP')) { if (foundStates.includes('2FA_TOTP')) {
// Don't throw on TOTP if expired code is entered
foundStates = foundStates.filter(s => s !== 'ERROR_ALERT') foundStates = foundStates.filter(s => s !== 'ERROR_ALERT')
} }
// On login.live.com, keep it
return 'ERROR_ALERT'
}
if (foundStates.includes('ERROR_ALERT')) return 'ERROR_ALERT' if (foundStates.includes('ERROR_ALERT')) return 'ERROR_ALERT'
if (foundStates.includes('ACCOUNT_LOCKED')) return 'ACCOUNT_LOCKED'
if (foundStates.includes('PASSKEY_VIDEO')) return 'PASSKEY_VIDEO'
if (foundStates.includes('PASSKEY_ERROR')) return 'PASSKEY_ERROR'
if (foundStates.includes('KMSI_PROMPT')) return 'KMSI_PROMPT'
if (foundStates.includes('PASSWORD_INPUT')) return 'PASSWORD_INPUT'
if (foundStates.includes('EMAIL_INPUT')) return 'EMAIL_INPUT'
if (foundStates.includes('SIGN_IN_ANOTHER_WAY')) return 'SIGN_IN_ANOTHER_WAY'
if (foundStates.includes('LOGIN_PASSWORDLESS')) return 'LOGIN_PASSWORDLESS'
if (foundStates.includes('2FA_TOTP')) return '2FA_TOTP'
const mainState = foundStates[0] as LoginState
return mainState
} }
private async handleState( const priorities: LoginState[] = [
state: LoginState, 'ACCOUNT_LOCKED',
page: Page, 'PASSKEY_VIDEO',
email: string, 'PASSKEY_ERROR',
password: string, 'KMSI_PROMPT',
totpSecret?: string 'PASSWORD_INPUT',
): Promise<boolean> { 'EMAIL_INPUT',
'SIGN_IN_ANOTHER_WAY_EMAIL',
'SIGN_IN_ANOTHER_WAY',
'LOGIN_PASSWORDLESS',
'2FA_TOTP'
]
for (const priority of priorities) {
if (foundStates.includes(priority)) {
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Selected state by priority: ${priority}`)
return priority
}
}
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Returning first found state: ${foundStates[0]}`)
return foundStates[0] as LoginState
}
private async checkSelector(page: Page, selector: string): Promise<boolean> {
return page
.waitForSelector(selector, { state: 'visible', timeout: 200 })
.then(() => true)
.catch(() => false)
}
private async handleState(state: LoginState, page: Page, account: Account): Promise<boolean> {
this.bot.logger.debug(this.bot.isMobile, 'HANDLE-STATE', `Processing state: ${state}`)
switch (state) { switch (state) {
case 'ACCOUNT_LOCKED': { case 'ACCOUNT_LOCKED': {
const msg = 'This account has been locked! Remove from config and restart!' const msg = 'This account has been locked! Remove from config and restart!'
this.bot.logger.error(this.bot.isMobile, 'CHECK-LOCKED', msg) this.bot.logger.error(this.bot.isMobile, 'LOGIN', msg)
throw new Error(msg) throw new Error(msg)
} }
case 'ERROR_ALERT': { case 'ERROR_ALERT': {
const alertEl = page.locator('div[role="alert"]') const alertEl = page.locator(this.selectors.errorAlert)
const errorMsg = await alertEl.innerText().catch(() => 'Unknown Error') const errorMsg = await alertEl.innerText().catch(() => 'Unknown Error')
this.bot.logger.error(this.bot.isMobile, 'LOGIN', `Account error: ${errorMsg}`) this.bot.logger.error(this.bot.isMobile, 'LOGIN', `Account error: ${errorMsg}`)
throw new Error(`Microsoft login error message: ${errorMsg}`) throw new Error(`Microsoft login error: ${errorMsg}`)
} }
case 'LOGGED_IN': case 'LOGGED_IN':
@@ -246,96 +289,161 @@ export class Login {
case 'EMAIL_INPUT': { case 'EMAIL_INPUT': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering email') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering email')
await this.emailLogin.enterEmail(page, email) await this.emailLogin.enterEmail(page, account.email)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after email entry')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Email entered successfully')
return true return true
} }
case 'PASSWORD_INPUT': { case 'PASSWORD_INPUT': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering password') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering password')
await this.emailLogin.enterPassword(page, password) await this.emailLogin.enterPassword(page, account.password)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after password entry')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Password entered successfully')
return true return true
} }
case 'GET_A_CODE': { case 'GET_A_CODE': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code"') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code" via footer')
// Select sign in other way await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
await this.bot.browser.utils.ghostClick(page, '[data-testid="viewFooter"] span[role="button"]') await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}) this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after footer click')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Footer clicked, proceeding')
return true
}
case 'GET_A_CODE_2': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Handling "Get a code" flow')
await this.bot.browser.utils.ghostClick(page, this.selectors.primaryButton)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after primary button click')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Initiating code login handler')
await this.codeLogin.handle(page)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Code login handler completed successfully')
return true
}
case 'SIGN_IN_ANOTHER_WAY_EMAIL': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Selecting "Send a code to email"')
const emailSelector = await Promise.race([
this.checkSelector(page, this.selectors.emailIcon).then(found =>
found ? this.selectors.emailIcon : null
),
this.checkSelector(page, this.selectors.emailIconOld).then(found =>
found ? this.selectors.emailIconOld : null
)
])
if (!emailSelector) {
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Email icon not found')
return false
}
this.bot.logger.info(
this.bot.isMobile,
'LOGIN',
`Using ${emailSelector === this.selectors.emailIcon ? 'new' : 'old'} email icon selector`
)
await this.bot.browser.utils.ghostClick(page, emailSelector)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after email icon click')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Initiating code login handler')
await this.codeLogin.handle(page)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Code login handler completed successfully')
return true
}
case 'RECOVERY_EMAIL_INPUT': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery email input detected')
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout on recovery page')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Initiating recovery email handler')
await this.recoveryLogin.handle(page, account?.recoveryEmail)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery email handler completed successfully')
return true return true
} }
case 'CHROMEWEBDATA_ERROR': { case 'CHROMEWEBDATA_ERROR': {
this.bot.logger.warn( this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'chromewebdata error detected, attempting recovery')
this.bot.isMobile,
'LOGIN',
'chromewebdata error page detected, attempting to recover to Rewards home'
)
// Try go to Rewards dashboard
try { try {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', `Navigating to ${this.bot.config.baseURL}`)
await page await page
.goto(this.bot.config.baseURL, { .goto(this.bot.config.baseURL, {
waitUntil: 'domcontentloaded', waitUntil: 'domcontentloaded',
timeout: 10000 timeout: 10000
}) })
.catch(() => {}) .catch(() => {})
await this.bot.utils.wait(3000) await this.bot.utils.wait(3000)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery navigation successful')
return true return true
} catch { } catch {
// If even that fails, fall back to login.live.com this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Fallback to login.live.com')
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN',
'Failed to navigate to baseURL from chromewebdata, retrying login.live.com'
)
await page await page
.goto('https://login.live.com/', { .goto('https://login.live.com/', {
waitUntil: 'domcontentloaded', waitUntil: 'domcontentloaded',
timeout: 10000 timeout: 10000
}) })
.catch(() => {}) .catch(() => {})
await this.bot.utils.wait(3000) await this.bot.utils.wait(3000)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Fallback navigation successful')
return true return true
} }
} }
case '2FA_TOTP': { case '2FA_TOTP': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA required') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA authentication required')
await this.totp2FALogin.handle(page, totpSecret) await this.totp2FALogin.handle(page, account.totpSecret)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA handler completed successfully')
return true return true
} }
case 'SIGN_IN_ANOTHER_WAY': { case 'SIGN_IN_ANOTHER_WAY': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Selecting "Use my password"') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Selecting "Use my password"')
const passwordOption = '[data-testid="tile"]:has(svg path[d*="M11.78 10.22a.75.75"])' await this.bot.browser.utils.ghostClick(page, this.selectors.passwordIcon)
await this.bot.browser.utils.ghostClick(page, passwordOption) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}) this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after password icon click')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Password option selected')
return true return true
} }
case 'KMSI_PROMPT': { case 'KMSI_PROMPT': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Accepting KMSI prompt') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Accepting KMSI prompt')
await this.bot.browser.utils.ghostClick(page, this.primaryButtonSelector) await this.bot.browser.utils.ghostClick(page, this.selectors.primaryButton)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after KMSI acceptance')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'KMSI prompt accepted')
return true return true
} }
case 'PASSKEY_VIDEO': case 'PASSKEY_VIDEO':
case 'PASSKEY_ERROR': { case 'PASSKEY_ERROR': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Skipping Passkey prompt') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Skipping Passkey prompt')
await this.bot.browser.utils.ghostClick(page, this.secondaryButtonSelector) await this.bot.browser.utils.ghostClick(page, this.selectors.secondaryButton)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after Passkey skip')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Passkey prompt skipped')
return true return true
} }
case 'LOGIN_PASSWORDLESS': { case 'LOGIN_PASSWORDLESS': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Handling passwordless authentication') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Handling passwordless authentication')
await this.passwordlessLogin.handle(page) await this.passwordlessLogin.handle(page)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after passwordless auth')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Passwordless authentication completed successfully')
return true return true
} }
@@ -344,12 +452,13 @@ export class Login {
this.bot.logger.warn( this.bot.logger.warn(
this.bot.isMobile, this.bot.isMobile,
'LOGIN', 'LOGIN',
`Unknown state at host:${url.hostname} path:${url.pathname}. Waiting...` `Unknown state at ${url.hostname}${url.pathname}, waiting`
) )
return true return true
} }
default: default:
this.bot.logger.debug(this.bot.isMobile, 'HANDLE-STATE', `Unhandled state: ${state}, continuing`)
return true return true
} }
} }
@@ -363,21 +472,21 @@ export class Login {
if (loginRewardsSuccess) { if (loginRewardsSuccess) {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Logged into Microsoft Rewards successfully') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Logged into Microsoft Rewards successfully')
} else { } else {
this.bot.logger.warn( this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Could not verify Rewards Dashboard, assuming login valid')
this.bot.isMobile,
'LOGIN',
'Could not verify Rewards Dashboard. Assuming login valid anyway.'
)
} }
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting Bing session verification')
await this.verifyBingSession(page) await this.verifyBingSession(page)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting rewards session verification')
await this.getRewardsSession(page) await this.getRewardsSession(page)
const browser = page.context() const browser = page.context()
const cookies = await browser.cookies() const cookies = await browser.cookies()
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `Retrieved ${cookies.length} cookies`)
await saveSessionData(this.bot.config.sessionPath, cookies, email, this.bot.isMobile) await saveSessionData(this.bot.config.sessionPath, cookies, email, this.bot.isMobile)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Login completed! Session saved!') this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Login completed, session saved')
} }
async verifyBingSession(page: Page) { async verifyBingSession(page: Page) {
@@ -393,30 +502,34 @@ export class Login {
for (let i = 0; i < loopMax; i++) { for (let i = 0; i < loopMax; i++) {
if (page.isClosed()) break if (page.isClosed()) break
// Rare error state this.bot.logger.debug(this.bot.isMobile, 'LOGIN-BING', `Verification loop ${i + 1}/${loopMax}`)
const state = await this.detectCurrentState(page) const state = await this.detectCurrentState(page)
if (state === 'PASSKEY_ERROR') { if (state === 'PASSKEY_ERROR') {
this.bot.logger.debug( this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Dismissing Passkey error state')
this.bot.isMobile, await this.bot.browser.utils.ghostClick(page, this.selectors.secondaryButton)
'LOGIN-BING',
'Verification landed on Passkey error state! Trying to dismiss.'
)
await this.bot.browser.utils.ghostClick(page, this.secondaryButtonSelector)
} }
const u = new URL(page.url()) const u = new URL(page.url())
const atBingHome = u.hostname === 'www.bing.com' && u.pathname === '/' const atBingHome = u.hostname === 'www.bing.com' && u.pathname === '/'
this.bot.logger.debug(
this.bot.isMobile,
'LOGIN-BING',
`At Bing home: ${atBingHome} (${u.hostname}${u.pathname})`
)
if (atBingHome) { if (atBingHome) {
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => {}) await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => {})
const signedIn = await page const signedIn = await page
.waitForSelector('#id_n', { timeout: 3000 }) .waitForSelector(this.selectors.bingProfile, { timeout: 3000 })
.then(() => true) .then(() => true)
.catch(() => false) .catch(() => false)
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-BING', `Profile element found: ${signedIn}`)
if (signedIn || this.bot.isMobile) { if (signedIn || this.bot.isMobile) {
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Bing session established') this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Bing session verified successfully')
return return
} }
} }
@@ -424,16 +537,12 @@ export class Login {
await this.bot.utils.wait(1000) await this.bot.utils.wait(1000)
} }
this.bot.logger.warn( this.bot.logger.warn(this.bot.isMobile, 'LOGIN-BING', 'Could not verify Bing session, continuing anyway')
this.bot.isMobile,
'LOGIN-BING',
'Could not confirm Bing session after retries; continuing'
)
} catch (error) { } catch (error) {
this.bot.logger.warn( this.bot.logger.warn(
this.bot.isMobile, this.bot.isMobile,
'LOGIN-BING', 'LOGIN-BING',
`Bing verification error: ${error instanceof Error ? error.message : String(error)}` `Verification error: ${error instanceof Error ? error.message : String(error)}`
) )
} }
} }
@@ -441,7 +550,7 @@ export class Login {
private async getRewardsSession(page: Page) { private async getRewardsSession(page: Page) {
const loopMax = 5 const loopMax = 5
this.bot.logger.info(this.bot.isMobile, 'GET-REQUEST-TOKEN', 'Fetching request token') this.bot.logger.info(this.bot.isMobile, 'GET-REWARD-SESSION', 'Fetching request token')
try { try {
await page await page
@@ -451,11 +560,7 @@ export class Login {
for (let i = 0; i < loopMax; i++) { for (let i = 0; i < loopMax; i++) {
if (page.isClosed()) break if (page.isClosed()) break
this.bot.logger.debug( this.bot.logger.debug(this.bot.isMobile, 'GET-REWARD-SESSION', `Token fetch loop ${i + 1}/${loopMax}`)
this.bot.isMobile,
'GET-REWARD-SESSION',
`Loop ${i + 1}/${loopMax} | URL=${page.url()}`
)
const u = new URL(page.url()) const u = new URL(page.url())
const atRewardHome = u.hostname === 'rewards.bing.com' && u.pathname === '/' const atRewardHome = u.hostname === 'rewards.bing.com' && u.pathname === '/'
@@ -467,23 +572,27 @@ export class Login {
const $ = await this.bot.browser.utils.loadInCheerio(html) const $ = await this.bot.browser.utils.loadInCheerio(html)
const token = const token =
$('input[name="__RequestVerificationToken"]').attr('value') ?? $(this.selectors.requestToken).attr('value') ??
$('meta[name="__RequestVerificationToken"]').attr('content') ?? $(this.selectors.requestTokenMeta).attr('content') ??
null null
if (token) { if (token) {
this.bot.requestToken = token this.bot.requestToken = token
this.bot.logger.info(this.bot.isMobile, 'GET-REQUEST-TOKEN', 'Request token has been set!') this.bot.logger.info(
this.bot.logger.debug(
this.bot.isMobile, this.bot.isMobile,
'GET-REWARD-SESSION', 'GET-REWARD-SESSION',
`Token extracted: ${token.substring(0, 10)}...` `Request token retrieved: ${token.substring(0, 10)}...`
) )
return return
} }
this.bot.logger.debug(this.bot.isMobile, 'GET-REWARD-SESSION', 'Token NOT found on page') this.bot.logger.debug(this.bot.isMobile, 'GET-REWARD-SESSION', 'Token not found on page')
} else {
this.bot.logger.debug(
this.bot.isMobile,
'GET-REWARD-SESSION',
`Not at reward home: ${u.hostname}${u.pathname}`
)
} }
await this.bot.utils.wait(1000) await this.bot.utils.wait(1000)
@@ -491,19 +600,20 @@ export class Login {
this.bot.logger.warn( this.bot.logger.warn(
this.bot.isMobile, this.bot.isMobile,
'GET-REQUEST-TOKEN', 'GET-REWARD-SESSION',
'No RequestVerificationToken found some activities may not work' 'No RequestVerificationToken found, some activities may not work'
) )
} catch (error) { } catch (error) {
throw this.bot.logger.error( throw this.bot.logger.error(
this.bot.isMobile, this.bot.isMobile,
'GET-REQUEST-TOKEN', 'GET-REWARD-SESSION',
`Reward session error: ${error instanceof Error ? error.message : String(error)}` `Fatal error: ${error instanceof Error ? error.message : String(error)}`
) )
} }
} }
async getAppAccessToken(page: Page, email: string) { async getAppAccessToken(page: Page, email: string) {
this.bot.logger.info(this.bot.isMobile, 'GET-APP-TOKEN', 'Requesting mobile access token')
return await new MobileAccessLogin(this.bot, page).get(email) return await new MobileAccessLogin(this.bot, page).get(email)
} }
} }

View File

@@ -0,0 +1,129 @@
import type { Page } from 'patchright'
import type { MicrosoftRewardsBot } from '../../../index'
import { getErrorMessage, getSubtitleMessage, promptInput } from './LoginUtils'
export class CodeLogin {
private readonly textInputSelector = '[data-testid="codeInputWrapper"]'
private readonly secondairyInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
private readonly maxManualSeconds = 60
private readonly maxManualAttempts = 5
constructor(private bot: MicrosoftRewardsBot) {}
private async fillCode(page: Page, code: string): Promise<boolean> {
try {
const visibleInput = await page
.waitForSelector(this.textInputSelector, { state: 'visible', timeout: 500 })
.catch(() => null)
if (visibleInput) {
await page.keyboard.type(code, { delay: 50 })
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Filled code input')
return true
}
const secondairyInput = await page.$(this.secondairyInputSelector)
if (secondairyInput) {
await page.keyboard.type(code, { delay: 50 })
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Filled code input')
return true
}
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-CODE', 'No code input field found')
return false
} catch (error) {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-CODE',
`Failed to fill code input: ${error instanceof Error ? error.message : String(error)}`
)
return false
}
}
async handle(page: Page): Promise<void> {
try {
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Code login authentication requested')
const emailMessage = await getSubtitleMessage(page)
if (emailMessage) {
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', `Page message: "${emailMessage}"`)
} else {
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-CODE', 'Unable to retrieve email code destination')
}
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
const code = await promptInput({
question: `Enter the 6-digit code (waiting ${this.maxManualSeconds}s): `,
timeoutSeconds: this.maxManualSeconds,
validate: code => /^\d{6}$/.test(code)
})
if (!code || !/^\d{6}$/.test(code)) {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-CODE',
`Invalid or missing code (attempt ${attempt}/${this.maxManualAttempts}) | input length=${code?.length}`
)
if (attempt === this.maxManualAttempts) {
throw new Error('Manual code input failed or timed out')
}
continue
}
const filled = await this.fillCode(page, code)
if (!filled) {
this.bot.logger.error(
this.bot.isMobile,
'LOGIN-CODE',
`Unable to fill code input (attempt ${attempt}/${this.maxManualAttempts})`
)
if (attempt === this.maxManualAttempts) {
throw new Error('Code input field not found')
}
continue
}
await this.bot.utils.wait(500)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
// Check if wrong code was entered
const errorMessage = await getErrorMessage(page)
if (errorMessage) {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-CODE',
`Incorrect code: ${errorMessage} (attempt ${attempt}/${this.maxManualAttempts})`
)
if (attempt === this.maxManualAttempts) {
throw new Error(`Maximum attempts reached: ${errorMessage}`)
}
// Clear the input field before retrying
const inputToClear = await page.$(this.textInputSelector).catch(() => null)
if (inputToClear) {
await inputToClear.click()
await page.keyboard.press('Control+A')
await page.keyboard.press('Backspace')
}
continue
}
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Code authentication completed successfully')
return
}
throw new Error(`Code input failed after ${this.maxManualAttempts} attempts`)
} catch (error) {
this.bot.logger.error(
this.bot.isMobile,
'LOGIN-CODE',
`Error occurred: ${error instanceof Error ? error.message : String(error)}`
)
throw error
}
}
}

View File

@@ -0,0 +1,66 @@
import type { Page } from 'patchright'
import readline from 'readline'
export interface PromptOptions {
question: string
timeoutSeconds?: number
validate?: (input: string) => boolean
transform?: (input: string) => string
}
export function promptInput(options: PromptOptions): Promise<string | null> {
const { question, timeoutSeconds = 60, validate, transform } = options
return new Promise(resolve => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
let resolved = false
const cleanup = (result: string | null) => {
if (resolved) return
resolved = true
clearTimeout(timer)
rl.close()
resolve(result)
}
const timer = setTimeout(() => cleanup(null), timeoutSeconds * 1000)
rl.question(question, answer => {
let value = answer.trim()
if (transform) value = transform(value)
if (validate && !validate(value)) {
cleanup(null)
return
}
cleanup(value)
})
})
}
export async function getSubtitleMessage(page: Page): Promise<string | null> {
const message = await page
.waitForSelector('[data-testid="subtitle"]', { state: 'visible', timeout: 1000 })
.catch(() => null)
if (!message) return null
const text = await message.innerText()
return text.trim()
}
export async function getErrorMessage(page: Page): Promise<string | null> {
const errorAlert = await page
.waitForSelector('div[role="alert"]', { state: 'visible', timeout: 1000 })
.catch(() => null)
if (!errorAlert) return null
const text = await errorAlert.innerText()
return text.trim()
}

View File

@@ -1,7 +1,6 @@
import type { Page } from 'patchright' import type { Page } from 'patchright'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
import { URLSearchParams } from 'url' import { URLSearchParams } from 'url'
import type { AxiosRequestConfig } from 'axios'
import type { MicrosoftRewardsBot } from '../../../index' import type { MicrosoftRewardsBot } from '../../../index'
@@ -29,25 +28,70 @@ export class MobileAccessLogin {
authorizeUrl.searchParams.append('access_type', 'offline_access') authorizeUrl.searchParams.append('access_type', 'offline_access')
authorizeUrl.searchParams.append('login_hint', email) authorizeUrl.searchParams.append('login_hint', email)
this.bot.logger.debug(
this.bot.isMobile,
'LOGIN-APP',
`Auth URL constructed: ${authorizeUrl.origin}${authorizeUrl.pathname}`
)
await this.bot.browser.utils.disableFido(this.page) await this.bot.browser.utils.disableFido(this.page)
await this.page.goto(authorizeUrl.href).catch(() => {}) this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Navigating to OAuth authorize URL')
await this.page.goto(authorizeUrl.href).catch(err => {
this.bot.logger.debug(
this.bot.isMobile,
'LOGIN-APP',
`page.goto() failed: ${err instanceof Error ? err.message : String(err)}`
)
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Waiting for mobile OAuth code...') this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Waiting for mobile OAuth code...')
const start = Date.now() const start = Date.now()
let code = '' let code = ''
let lastUrl = ''
while (Date.now() - start < this.maxTimeout) { while (Date.now() - start < this.maxTimeout) {
const url = new URL(this.page.url()) const currentUrl = this.page.url()
// Log only when URL changes (high signal, no spam)
if (currentUrl !== lastUrl) {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', `OAuth poll URL changed → ${currentUrl}`)
lastUrl = currentUrl
}
try {
const url = new URL(currentUrl)
if (url.hostname === 'login.live.com' && url.pathname === '/oauth20_desktop.srf') { if (url.hostname === 'login.live.com' && url.pathname === '/oauth20_desktop.srf') {
code = url.searchParams.get('code') || '' code = url.searchParams.get('code') || ''
if (code) break
if (code) {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'OAuth code detected in redirect URL')
break
} }
}
} catch (err) {
this.bot.logger.debug(
this.bot.isMobile,
'LOGIN-APP',
`Invalid URL while polling: ${String(currentUrl)}`
)
}
await this.bot.utils.wait(1000) await this.bot.utils.wait(1000)
} }
if (!code) { if (!code) {
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'Timed out waiting for OAuth code') this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-APP',
`Timed out waiting for OAuth code after ${Math.round((Date.now() - start) / 1000)}s`
)
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', `Final page URL: ${this.page.url()}`)
return '' return ''
} }
@@ -57,18 +101,24 @@ export class MobileAccessLogin {
data.append('code', code) data.append('code', code)
data.append('redirect_uri', this.redirectUrl) data.append('redirect_uri', this.redirectUrl)
const request: AxiosRequestConfig = { this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Exchanging OAuth code for access token')
const response = await this.bot.axios.request({
url: this.tokenUrl, url: this.tokenUrl,
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: data.toString() data: data.toString()
} })
const response = await this.bot.axios.request(request)
const token = (response?.data?.access_token as string) ?? '' const token = (response?.data?.access_token as string) ?? ''
if (!token) { if (!token) {
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'No access_token in token response') this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'No access_token in token response')
this.bot.logger.debug(
this.bot.isMobile,
'LOGIN-APP',
`Token response payload: ${JSON.stringify(response?.data)}`
)
return '' return ''
} }
@@ -78,10 +128,11 @@ export class MobileAccessLogin {
this.bot.logger.error( this.bot.logger.error(
this.bot.isMobile, this.bot.isMobile,
'LOGIN-APP', 'LOGIN-APP',
`MobileAccess error: ${error instanceof Error ? error.message : String(error)}` `MobileAccess error: ${error instanceof Error ? error.stack || error.message : String(error)}`
) )
return '' return ''
} finally { } finally {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Returning to base URL')
await this.page.goto(this.bot.config.baseURL, { timeout: 10000 }).catch(() => {}) await this.page.goto(this.bot.config.baseURL, { timeout: 10000 }).catch(() => {})
} }
} }

View File

@@ -79,13 +79,15 @@ export class PasswordlessLogin {
this.bot.logger.info( this.bot.logger.info(
this.bot.isMobile, this.bot.isMobile,
'LOGIN-PASSWORDLESS', 'LOGIN-PASSWORDLESS',
`Please approve login and select number: ${displayedNumber}` `Please approve login and select number: ${displayedNumber}`,
'yellowBright'
) )
} else { } else {
this.bot.logger.info( this.bot.logger.info(
this.bot.isMobile, this.bot.isMobile,
'LOGIN-PASSWORDLESS', 'LOGIN-PASSWORDLESS',
'Please approve login on your authenticator app' 'Please approve login on your authenticator app',
'yellowBright'
) )
} }

View File

@@ -0,0 +1,187 @@
import type { Page } from 'patchright'
import type { MicrosoftRewardsBot } from '../../../index'
import { getErrorMessage, promptInput } from './LoginUtils'
export class RecoveryLogin {
private readonly textInputSelector = '[data-testid="proof-confirmation"]'
private readonly maxManualSeconds = 60
private readonly maxManualAttempts = 5
constructor(private bot: MicrosoftRewardsBot) {}
private async fillEmail(page: Page, email: string): Promise<boolean> {
try {
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', `Attempting to fill email: ${email}`)
const visibleInput = await page
.waitForSelector(this.textInputSelector, { state: 'visible', timeout: 500 })
.catch(() => null)
if (visibleInput) {
await page.keyboard.type(email, { delay: 50 })
await page.keyboard.press('Enter')
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Successfully filled email input field')
return true
}
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-RECOVERY',
`Email input field not found with selector: ${this.textInputSelector}`
)
return false
} catch (error) {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-RECOVERY',
`Failed to fill email input: ${error instanceof Error ? error.message : String(error)}`
)
return false
}
}
async handle(page: Page, recoveryEmail: string): Promise<void> {
try {
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email recovery authentication flow initiated')
if (recoveryEmail) {
this.bot.logger.info(
this.bot.isMobile,
'LOGIN-RECOVERY',
`Using provided recovery email: ${recoveryEmail}`
)
const filled = await this.fillEmail(page, recoveryEmail)
if (!filled) {
throw new Error('Email input field not found')
}
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Waiting for page response')
await this.bot.utils.wait(500)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-RECOVERY', 'Network idle timeout reached')
})
const errorMessage = await getErrorMessage(page)
if (errorMessage) {
throw new Error(`Email verification failed: ${errorMessage}`)
}
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email authentication completed successfully')
return
}
this.bot.logger.info(
this.bot.isMobile,
'LOGIN-RECOVERY',
'No recovery email provided, will prompt user for input'
)
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
this.bot.logger.info(
this.bot.isMobile,
'LOGIN-RECOVERY',
`Starting attempt ${attempt}/${this.maxManualAttempts}`
)
this.bot.logger.info(
this.bot.isMobile,
'LOGIN-RECOVERY',
`Prompting user for email input (timeout: ${this.maxManualSeconds}s)`
)
const email = await promptInput({
question: `Recovery email (waiting ${this.maxManualSeconds}s): `,
timeoutSeconds: this.maxManualSeconds,
validate: email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
})
if (!email) {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-RECOVERY',
`No or invalid email input received (attempt ${attempt}/${this.maxManualAttempts})`
)
if (attempt === this.maxManualAttempts) {
throw new Error('Manual email input failed: no input received')
}
continue
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-RECOVERY',
`Invalid email format received (attempt ${attempt}/${this.maxManualAttempts}) | length=${email.length}`
)
if (attempt === this.maxManualAttempts) {
throw new Error('Manual email input failed: invalid format')
}
continue
}
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', `Valid email received from user: ${email}`)
const filled = await this.fillEmail(page, email)
if (!filled) {
this.bot.logger.error(
this.bot.isMobile,
'LOGIN-RECOVERY',
`Failed to fill email input field (attempt ${attempt}/${this.maxManualAttempts})`
)
if (attempt === this.maxManualAttempts) {
throw new Error('Email input field not found after maximum attempts')
}
await this.bot.utils.wait(1000)
continue
}
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Waiting for page response')
await this.bot.utils.wait(500)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-RECOVERY', 'Network idle timeout reached')
})
const errorMessage = await getErrorMessage(page)
if (errorMessage) {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-RECOVERY',
`Error from page: "${errorMessage}" (attempt ${attempt}/${this.maxManualAttempts})`
)
if (attempt === this.maxManualAttempts) {
throw new Error(`Maximum attempts reached. Last error: ${errorMessage}`)
}
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Clearing input field for retry')
const inputToClear = await page.$(this.textInputSelector).catch(() => null)
if (inputToClear) {
await inputToClear.click()
await page.keyboard.press('Control+A')
await page.keyboard.press('Backspace')
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Input field cleared')
} else {
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-RECOVERY', 'Could not find input field to clear')
}
await this.bot.utils.wait(1000)
continue
}
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email authentication completed successfully')
return
}
throw new Error(`Email input failed after ${this.maxManualAttempts} attempts`)
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
this.bot.logger.error(this.bot.isMobile, 'LOGIN-RECOVERY', `Fatal error: ${errorMsg}`)
throw error
}
}
}

View File

@@ -1,12 +1,12 @@
import type { Page } from 'patchright' import type { Page } from 'patchright'
import * as OTPAuth from 'otpauth' import * as OTPAuth from 'otpauth'
import readline from 'readline'
import type { MicrosoftRewardsBot } from '../../../index' import type { MicrosoftRewardsBot } from '../../../index'
import { getErrorMessage, promptInput } from './LoginUtils'
export class TotpLogin { export class TotpLogin {
private readonly textInputSelector = private readonly textInputSelector =
'form[name="OneTimeCodeViewForm"] input[type="text"], input#floatingLabelInput5' 'form[name="OneTimeCodeViewForm"] input[type="text"], input#floatingLabelInput5'
private readonly hiddenInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]' private readonly secondairyInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
private readonly submitButtonSelector = 'button[type="submit"]' private readonly submitButtonSelector = 'button[type="submit"]'
private readonly maxManualSeconds = 60 private readonly maxManualSeconds = 60
private readonly maxManualAttempts = 5 private readonly maxManualAttempts = 5
@@ -17,31 +17,6 @@ export class TotpLogin {
return new OTPAuth.TOTP({ secret, digits: 6 }).generate() return new OTPAuth.TOTP({ secret, digits: 6 }).generate()
} }
private async promptManualCode(): Promise<string | null> {
return await new Promise(resolve => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
let resolved = false
const cleanup = (result: string | null) => {
if (resolved) return
resolved = true
clearTimeout(timer)
rl.close()
resolve(result)
}
const timer = setTimeout(() => cleanup(null), this.maxManualSeconds * 1000)
rl.question(`Enter the 6-digit TOTP code (waiting ${this.maxManualSeconds}s): `, answer => {
cleanup(answer.trim())
})
})
}
private async fillCode(page: Page, code: string): Promise<boolean> { private async fillCode(page: Page, code: string): Promise<boolean> {
try { try {
const visibleInput = await page const visibleInput = await page
@@ -50,19 +25,18 @@ export class TotpLogin {
if (visibleInput) { if (visibleInput) {
await visibleInput.fill(code) await visibleInput.fill(code)
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled visible TOTP text input') this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled TOTP input')
return true return true
} }
const hiddenInput = await page.$(this.hiddenInputSelector) const secondairyInput = await page.$(this.secondairyInputSelector)
if (secondairyInput) {
if (hiddenInput) { await secondairyInput.fill(code)
await hiddenInput.fill(code) this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled TOTP input')
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled hidden TOTP input')
return true return true
} }
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP input field found (visible or hidden)') this.bot.logger.warn(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP input field found')
return false return false
} catch (error) { } catch (error) {
this.bot.logger.warn( this.bot.logger.warn(
@@ -83,9 +57,8 @@ export class TotpLogin {
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Generated TOTP code from secret') this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Generated TOTP code from secret')
const filled = await this.fillCode(page, code) const filled = await this.fillCode(page, code)
if (!filled) { if (!filled) {
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', 'Unable to locate or fill TOTP input field') this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', 'Unable to fill TOTP input field')
throw new Error('TOTP input field not found') throw new Error('TOTP input field not found')
} }
@@ -93,6 +66,12 @@ export class TotpLogin {
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector) await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
const errorMessage = await getErrorMessage(page)
if (errorMessage) {
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', `TOTP failed: ${errorMessage}`)
throw new Error(`TOTP authentication failed: ${errorMessage}`)
}
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully') this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
return return
} }
@@ -100,45 +79,36 @@ export class TotpLogin {
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP secret provided, awaiting manual input') this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP secret provided, awaiting manual input')
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) { for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
const code = await this.promptManualCode() const code = await promptInput({
question: `Enter the 6-digit TOTP code (waiting ${this.maxManualSeconds}s): `,
timeoutSeconds: this.maxManualSeconds,
validate: code => /^\d{6}$/.test(code)
})
if (!code || !/^\d{6}$/.test(code)) { if (!code || !/^\d{6}$/.test(code)) {
this.bot.logger.warn( this.bot.logger.warn(
this.bot.isMobile, this.bot.isMobile,
'LOGIN-TOTP', 'LOGIN-TOTP',
`Invalid or missing TOTP code (attempt ${attempt}/${this.maxManualAttempts})` `Invalid or missing code (attempt ${attempt}/${this.maxManualAttempts}) | input length=${code?.length}`
) )
if (attempt === this.maxManualAttempts) { if (attempt === this.maxManualAttempts) {
throw new Error('Manual TOTP input failed or timed out') throw new Error('Manual TOTP input failed or timed out')
} }
this.bot.logger.info(
this.bot.isMobile,
'LOGIN-TOTP',
'Retrying manual TOTP input due to invalid code'
)
continue continue
} }
const filled = await this.fillCode(page, code) const filled = await this.fillCode(page, code)
if (!filled) { if (!filled) {
this.bot.logger.error( this.bot.logger.error(
this.bot.isMobile, this.bot.isMobile,
'LOGIN-TOTP', 'LOGIN-TOTP',
`Unable to locate or fill TOTP input field (attempt ${attempt}/${this.maxManualAttempts})` `Unable to fill TOTP input (attempt ${attempt}/${this.maxManualAttempts})`
) )
if (attempt === this.maxManualAttempts) { if (attempt === this.maxManualAttempts) {
throw new Error('TOTP input field not found') throw new Error('TOTP input field not found')
} }
this.bot.logger.info(
this.bot.isMobile,
'LOGIN-TOTP',
'Retrying manual TOTP input due to fill failure'
)
continue continue
} }
@@ -146,16 +116,31 @@ export class TotpLogin {
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector) await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}) await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
// Check if wrong code was entered
const errorMessage = await getErrorMessage(page)
if (errorMessage) {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-TOTP',
`Incorrect code: ${errorMessage} (attempt ${attempt}/${this.maxManualAttempts})`
)
if (attempt === this.maxManualAttempts) {
throw new Error(`Maximum attempts reached: ${errorMessage}`)
}
continue
}
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully') this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
return return
} }
throw new Error(`Manual TOTP input failed after ${this.maxManualAttempts} attempts`) throw new Error(`TOTP input failed after ${this.maxManualAttempts} attempts`)
} catch (error) { } catch (error) {
this.bot.logger.error( this.bot.logger.error(
this.bot.isMobile, this.bot.isMobile,
'LOGIN-TOTP', 'LOGIN-TOTP',
`An error occurred: ${error instanceof Error ? error.message : String(error)}` `Error occurred: ${error instanceof Error ? error.message : String(error)}`
) )
throw error throw error
} }

View File

@@ -4,13 +4,10 @@
"headless": false, "headless": false,
"runOnZeroPoints": false, "runOnZeroPoints": false,
"clusters": 1, "clusters": 1,
"errorDiagnostics": true, "errorDiagnostics": false,
"saveFingerprint": {
"mobile": false,
"desktop": false
},
"workers": { "workers": {
"doDailySet": true, "doDailySet": true,
"doSpecialPromotions": true,
"doMorePromotions": true, "doMorePromotions": true,
"doPunchCards": true, "doPunchCards": true,
"doAppPromotions": true, "doAppPromotions": true,
@@ -25,6 +22,12 @@
"scrollRandomResults": false, "scrollRandomResults": false,
"clickRandomResults": false, "clickRandomResults": false,
"parallelSearching": true, "parallelSearching": true,
"queryEngines": [
"google",
"wikipedia",
"reddit",
"local"
],
"searchResultVisitTime": "10sec", "searchResultVisitTime": "10sec",
"searchDelay": { "searchDelay": {
"min": "30sec", "min": "30sec",
@@ -39,8 +42,13 @@
"consoleLogFilter": { "consoleLogFilter": {
"enabled": false, "enabled": false,
"mode": "whitelist", "mode": "whitelist",
"levels": ["error", "warn"], "levels": [
"keywords": ["starting account"], "error",
"warn"
],
"keywords": [
"starting account"
],
"regexPatterns": [] "regexPatterns": []
}, },
"proxy": { "proxy": {
@@ -57,14 +65,23 @@
"topic": "", "topic": "",
"token": "", "token": "",
"title": "Microsoft-Rewards-Script", "title": "Microsoft-Rewards-Script",
"tags": ["bot", "notify"], "tags": [
"bot",
"notify"
],
"priority": 3 "priority": 3
}, },
"webhookLogFilter": { "webhookLogFilter": {
"enabled": false, "enabled": false,
"mode": "whitelist", "mode": "whitelist",
"levels": ["error"], "levels": [
"keywords": ["starting account", "select number", "collected"], "error"
],
"keywords": [
"starting account",
"select number",
"collected"
],
"regexPatterns": [] "regexPatterns": []
} }
} }

View File

@@ -10,12 +10,18 @@ import { AppReward } from './activities/app/AppReward'
import { UrlReward } from './activities/api/UrlReward' import { UrlReward } from './activities/api/UrlReward'
import { Quiz } from './activities/api/Quiz' import { Quiz } from './activities/api/Quiz'
import { FindClippy } from './activities/api/FindClippy' import { FindClippy } from './activities/api/FindClippy'
import { DoubleSearchPoints } from './activities/api/DoubleSearchPoints'
// Browser // Browser
import { SearchOnBing } from './activities/browser/SearchOnBing' import { SearchOnBing } from './activities/browser/SearchOnBing'
import { Search } from './activities/browser/Search' import { Search } from './activities/browser/Search'
import type { BasePromotion, DashboardData, FindClippyPromotion } from '../interface/DashboardData' import type {
BasePromotion,
DashboardData,
FindClippyPromotion,
PurplePromotionalItem
} from '../interface/DashboardData'
import type { Promotion } from '../interface/AppDashBoardData' import type { Promotion } from '../interface/AppDashBoardData'
export default class Activities { export default class Activities {
@@ -68,9 +74,14 @@ export default class Activities {
await quiz.doQuiz(promotion) await quiz.doQuiz(promotion)
} }
doFindClippy = async (promotions: FindClippyPromotion): Promise<void> => { doFindClippy = async (promotion: FindClippyPromotion): Promise<void> => {
const urlReward = new FindClippy(this.bot) const findClippy = new FindClippy(this.bot)
await urlReward.doFindClippy(promotions) await findClippy.doFindClippy(promotion)
}
doDoubleSearchPoints = async (promotion: PurplePromotionalItem): Promise<void> => {
const doubleSearchPoints = new DoubleSearchPoints(this.bot)
await doubleSearchPoints.doDoubleSearchPoints(promotion)
} }
// App Activities // App Activities

View File

@@ -1,22 +1,207 @@
import type { AxiosRequestConfig } from 'axios' import type { AxiosRequestConfig } from 'axios'
import type { import * as fs from 'fs'
BingSuggestionResponse, import path from 'path'
BingTrendingTopicsResponse, import type { GoogleSearch, GoogleTrendsResponse, RedditListing, WikipediaTopResponse } from '../interface/Search'
GoogleSearch,
GoogleTrendsResponse
} from '../interface/Search'
import type { MicrosoftRewardsBot } from '../index' import type { MicrosoftRewardsBot } from '../index'
import { QueryEngine } from '../interface/Config'
export class QueryCore { export class QueryCore {
constructor(private bot: MicrosoftRewardsBot) {} constructor(private bot: MicrosoftRewardsBot) {}
async queryManager(
options: {
shuffle?: boolean
sourceOrder?: QueryEngine[]
related?: boolean
langCode?: string
geoLocale?: string
} = {}
): Promise<string[]> {
const {
shuffle = false,
sourceOrder = ['google', 'wikipedia', 'reddit', 'local'],
related = true,
langCode = 'en',
geoLocale = 'US'
} = options
try {
this.bot.logger.debug(
this.bot.isMobile,
'QUERY-MANAGER',
`start | shuffle=${shuffle}, related=${related}, lang=${langCode}, geo=${geoLocale}, sources=${sourceOrder.join(',')}`
)
const topicLists: string[][] = []
const sourceHandlers: Record<
'google' | 'wikipedia' | 'reddit' | 'local',
(() => Promise<string[]>) | (() => string[])
> = {
google: async () => {
const topics = await this.getGoogleTrends(geoLocale.toUpperCase()).catch(() => [])
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `google: ${topics.length}`)
return topics
},
wikipedia: async () => {
const topics = await this.getWikipediaTrending(langCode).catch(() => [])
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `wikipedia: ${topics.length}`)
return topics
},
reddit: async () => {
const topics = await this.getRedditTopics().catch(() => [])
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `reddit: ${topics.length}`)
return topics
},
local: () => {
const topics = this.getLocalQueryList()
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `local: ${topics.length}`)
return topics
}
}
for (const source of sourceOrder) {
const handler = sourceHandlers[source]
if (!handler) continue
const topics = await Promise.resolve(handler())
if (topics.length) topicLists.push(topics)
}
this.bot.logger.debug(
this.bot.isMobile,
'QUERY-MANAGER',
`sources combined | rawTotal=${topicLists.flat().length}`
)
const baseTopics = this.normalizeAndDedupe(topicLists.flat())
if (!baseTopics.length) {
this.bot.logger.warn(this.bot.isMobile, 'QUERY-MANAGER', 'No queries')
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'No base topics found (all sources empty)')
return []
}
this.bot.logger.debug(
this.bot.isMobile,
'QUERY-MANAGER',
`baseTopics dedupe | before=${topicLists.flat().length} | after=${baseTopics.length}`
)
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `baseTopics: ${baseTopics.length}`)
const clusters = related ? await this.buildRelatedClusters(baseTopics, langCode) : baseTopics.map(t => [t])
this.bot.utils.shuffleArray(clusters)
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'clusters shuffled')
let finalQueries = clusters.flat()
this.bot.logger.debug(
this.bot.isMobile,
'QUERY-MANAGER',
`clusters flattened | total=${finalQueries.length}`
)
// Do not cluster searches and shuffle
if (shuffle) {
this.bot.utils.shuffleArray(finalQueries)
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'finalQueries shuffled')
}
finalQueries = this.normalizeAndDedupe(finalQueries)
this.bot.logger.debug(
this.bot.isMobile,
'QUERY-MANAGER',
`finalQueries dedupe | after=${finalQueries.length}`
)
if (!finalQueries.length) {
this.bot.logger.warn(this.bot.isMobile, 'QUERY-MANAGER', 'No queries')
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'finalQueries deduped to 0')
return []
}
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `final queries: ${finalQueries.length}`)
return finalQueries
} catch (error) {
this.bot.logger.warn(this.bot.isMobile, 'QUERY-MANAGER', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'QUERY-MANAGER',
`error: ${error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)}`
)
return []
}
}
private async buildRelatedClusters(baseTopics: string[], langCode: string): Promise<string[][]> {
const clusters: string[][] = []
const LIMIT = 50
const head = baseTopics.slice(0, LIMIT)
const tail = baseTopics.slice(LIMIT)
this.bot.logger.debug(
this.bot.isMobile,
'QUERY-MANAGER',
`related enabled | baseTopics=${baseTopics.length} | expand=${head.length} | passthrough=${tail.length} | lang=${langCode}`
)
this.bot.logger.debug(
this.bot.isMobile,
'QUERY-MANAGER',
`bing expansion enabled | limit=${LIMIT} | totalCalls=${head.length * 2}`
)
for (const topic of head) {
const suggestions = await this.getBingSuggestions(topic, langCode).catch(() => [])
const relatedTerms = await this.getBingRelatedTerms(topic).catch(() => [])
const usedSuggestions = suggestions.slice(0, 6)
const usedRelated = relatedTerms.slice(0, 3)
const cluster = this.normalizeAndDedupe([topic, ...usedSuggestions, ...usedRelated])
this.bot.logger.debug(
this.bot.isMobile,
'QUERY-MANAGER',
`cluster expanded | topic="${topic}" | suggestions=${suggestions.length}->${usedSuggestions.length} | related=${relatedTerms.length}->${usedRelated.length} | clusterSize=${cluster.length}`
)
clusters.push(cluster)
}
if (tail.length) {
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `cluster passthrough | topics=${tail.length}`)
for (const topic of tail) {
clusters.push([topic])
}
}
return clusters
}
private normalizeAndDedupe(queries: string[]): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const q of queries) {
if (!q) continue
const trimmed = q.trim()
if (!trimmed) continue
const norm = trimmed.replace(/\s+/g, ' ').toLowerCase()
if (seen.has(norm)) continue
seen.add(norm)
out.push(trimmed)
}
return out
}
async getGoogleTrends(geoLocale: string): Promise<string[]> { async getGoogleTrends(geoLocale: string): Promise<string[]> {
const queryTerms: GoogleSearch[] = [] const queryTerms: GoogleSearch[] = []
this.bot.logger.info(
this.bot.isMobile,
'SEARCH-GOOGLE-TRENDS',
`Generating search queries, can take a while! | GeoLocale: ${geoLocale}`
)
try { try {
const request: AxiosRequestConfig = { const request: AxiosRequestConfig = {
@@ -29,163 +214,287 @@ export class QueryCore {
} }
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine) const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
const rawData = response.data const trendsData = this.extractJsonFromResponse(response.data)
const trendsData = this.extractJsonFromResponse(rawData)
if (!trendsData) { if (!trendsData) {
throw this.bot.logger.error( this.bot.logger.warn(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No queries')
this.bot.isMobile, this.bot.logger.debug(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No trendsData parsed from response')
'SEARCH-GOOGLE-TRENDS', return []
'Failed to parse Google Trends response'
)
} }
const mappedTrendsData = trendsData.map(query => [query[0], query[9]!.slice(1)]) const mapped = trendsData.map(q => [q[0], q[9]!.slice(1)])
if (mappedTrendsData.length < 90) {
this.bot.logger.warn( if (mapped.length < 90 && geoLocale !== 'US') {
this.bot.isMobile,
'SEARCH-GOOGLE-TRENDS',
'Insufficient search queries, falling back to US'
)
return this.getGoogleTrends('US') return this.getGoogleTrends('US')
} }
for (const [topic, relatedQueries] of mappedTrendsData) { for (const [topic, related] of mapped) {
queryTerms.push({ queryTerms.push({
topic: topic as string, topic: topic as string,
related: relatedQueries as string[] related: related as string[]
}) })
} }
} catch (error) { } catch (error) {
this.bot.logger.error( this.bot.logger.warn(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No queries')
this.bot.logger.debug(
this.bot.isMobile, this.bot.isMobile,
'SEARCH-GOOGLE-TRENDS', 'SEARCH-GOOGLE-TRENDS',
`An error occurred: ${error instanceof Error ? error.message : String(error)}` `request failed: ${
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
}`
) )
return []
} }
const queries = queryTerms.flatMap(x => [x.topic, ...x.related]) return queryTerms.flatMap(x => [x.topic, ...x.related])
return queries
} }
private extractJsonFromResponse(text: string): GoogleTrendsResponse[1] | null { private extractJsonFromResponse(text: string): GoogleTrendsResponse[1] | null {
const lines = text.split('\n') for (const line of text.split('\n')) {
for (const line of lines) {
const trimmed = line.trim() const trimmed = line.trim()
if (trimmed.startsWith('[') && trimmed.endsWith(']')) { if (!trimmed.startsWith('[')) continue
try { try {
return JSON.parse(JSON.parse(trimmed)[0][2])[1] return JSON.parse(JSON.parse(trimmed)[0][2])[1]
} catch { } catch {}
continue
} }
}
}
return null return null
} }
async getBingSuggestions(query: string = '', langCode: string = 'en'): Promise<string[]> { async getBingSuggestions(query = '', langCode = 'en'): Promise<string[]> {
this.bot.logger.info(
this.bot.isMobile,
'SEARCH-BING-SUGGESTIONS',
`Generating bing suggestions! | LangCode: ${langCode}`
)
try { try {
const request: AxiosRequestConfig = { const request: AxiosRequestConfig = {
url: `https://www.bingapis.com/api/v7/suggestions?q=${encodeURIComponent(query)}&appid=6D0A9B8C5100E9ECC7E11A104ADD76C10219804B&cc=xl&setlang=${langCode}`, url: `https://www.bingapis.com/api/v7/suggestions?q=${encodeURIComponent(
query
)}&appid=6D0A9B8C5100E9ECC7E11A104ADD76C10219804B&cc=xl&setlang=${langCode}`,
method: 'POST', method: 'POST',
headers: { headers: {
...(this.bot.fingerprint?.headers ?? {}),
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
} }
} }
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine) const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
const rawData: BingSuggestionResponse = response.data const suggestions =
response.data.suggestionGroups?.[0]?.searchSuggestions?.map((x: { query: any }) => x.query) ?? []
const searchSuggestions = rawData.suggestionGroups[0]?.searchSuggestions if (!suggestions.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'No queries')
if (!searchSuggestions?.length) { this.bot.logger.debug(
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'API returned no results')
return []
}
return searchSuggestions.map(x => x.query)
} catch (error) {
this.bot.logger.error(
this.bot.isMobile, this.bot.isMobile,
'SEARCH-GOOGLE-TRENDS', 'SEARCH-BING-SUGGESTIONS',
`An error occurred: ${error instanceof Error ? error.message : String(error)}` `empty suggestions | query="${query}" | lang=${langCode}`
) )
} }
return suggestions
} catch (error) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-BING-SUGGESTIONS',
`request failed | query="${query}" | lang=${langCode} | error=${
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
}`
)
return [] return []
} }
}
async getBingRelatedTerms(term: string): Promise<string[]> { async getBingRelatedTerms(query: string): Promise<string[]> {
try { try {
const request = { const request: AxiosRequestConfig = {
url: `https://api.bing.com/osjson.aspx?query=${term}`, url: `https://api.bing.com/osjson.aspx?query=${encodeURIComponent(query)}`,
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' ...(this.bot.fingerprint?.headers ?? {})
} }
} }
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine) const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
const rawData = response.data const related = response.data?.[1]
const out = Array.isArray(related) ? related : []
const relatedTerms = rawData[1] if (!out.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'No queries')
if (!relatedTerms?.length) { this.bot.logger.debug(
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'API returned no results')
return []
}
return relatedTerms
} catch (error) {
this.bot.logger.error(
this.bot.isMobile, this.bot.isMobile,
'SEARCH-BING-RELATED', 'SEARCH-BING-RELATED',
`An error occurred: ${error instanceof Error ? error.message : String(error)}` `empty related terms | query="${query}"`
) )
} }
return out
} catch (error) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-BING-RELATED',
`request failed | query="${query}" | error=${
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
}`
)
return [] return []
} }
}
async getBingTendingTopics(langCode: string = 'en'): Promise<string[]> { async getBingTrendingTopics(langCode = 'en'): Promise<string[]> {
try { try {
const request = { const request: AxiosRequestConfig = {
url: `https://www.bing.com/api/v7/news/trendingtopics?appid=91B36E34F9D1B900E54E85A77CF11FB3BE5279E6&cc=xl&setlang=${langCode}`, url: `https://www.bing.com/api/v7/news/trendingtopics?appid=91B36E34F9D1B900E54E85A77CF11FB3BE5279E6&cc=xl&setlang=${langCode}`,
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' Authorization: `Bearer ${this.bot.accessToken}`,
'User-Agent':
'Bing/32.5.431027001 (com.microsoft.bing; build:431027001; iOS 17.6.1) Alamofire/5.10.2',
'Content-Type': 'application/json',
'X-Rewards-Country': this.bot.userData.geoLocale,
'X-Rewards-Language': 'en',
'X-Rewards-ismobile': 'true'
} }
} }
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine) const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
const rawData: BingTrendingTopicsResponse = response.data const topics =
response.data.value?.map(
(x: { query: { text: string }; name: string }) => x.query?.text?.trim() || x.name.trim()
) ?? []
const trendingTopics = rawData.value if (!topics.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'No queries')
if (!trendingTopics?.length) { this.bot.logger.debug(
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'API returned no results')
return []
}
const queries = trendingTopics.map(x => x.query?.text?.trim() || x.name.trim())
return queries
} catch (error) {
this.bot.logger.error(
this.bot.isMobile, this.bot.isMobile,
'SEARCH-BING-TRENDING', 'SEARCH-BING-TRENDING',
`An error occurred: ${error instanceof Error ? error.message : String(error)}` `empty trending topics | lang=${langCode}`
) )
} }
return topics
} catch (error) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-BING-TRENDING',
`request failed | lang=${langCode} | error=${
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
}`
)
return [] return []
} }
} }
async getWikipediaTrending(langCode = 'en'): Promise<string[]> {
try {
const date = new Date(Date.now() - 24 * 60 * 60 * 1000)
const yyyy = date.getUTCFullYear()
const mm = String(date.getUTCMonth() + 1).padStart(2, '0')
const dd = String(date.getUTCDate()).padStart(2, '0')
const request: AxiosRequestConfig = {
url: `https://wikimedia.org/api/rest_v1/metrics/pageviews/top/${langCode}.wikipedia/all-access/${yyyy}/${mm}/${dd}`,
method: 'GET',
headers: {
...(this.bot.fingerprint?.headers ?? {})
}
}
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
const articles = (response.data as WikipediaTopResponse).items?.[0]?.articles ?? []
const out = articles.slice(0, 50).map(a => a.article.replace(/_/g, ' '))
if (!out.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-WIKIPEDIA-TRENDING', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-WIKIPEDIA-TRENDING',
`empty wikipedia top | lang=${langCode}`
)
}
return out
} catch (error) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-WIKIPEDIA-TRENDING', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-WIKIPEDIA-TRENDING',
`request failed | lang=${langCode} | error=${
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
}`
)
return []
}
}
async getRedditTopics(subreddit = 'popular'): Promise<string[]> {
try {
const safe = subreddit.replace(/[^a-zA-Z0-9_+]/g, '')
const request: AxiosRequestConfig = {
url: `https://www.reddit.com/r/${safe}.json?limit=50`,
method: 'GET',
headers: {
...(this.bot.fingerprint?.headers ?? {})
}
}
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
const posts = (response.data as RedditListing).data?.children ?? []
const out = posts.filter(p => !p.data.over_18).map(p => p.data.title)
if (!out.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-REDDIT-TRENDING', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-REDDIT-TRENDING',
`empty reddit listing | subreddit=${safe}`
)
}
return out
} catch (error) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-REDDIT', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-REDDIT',
`request failed | subreddit=${subreddit} | error=${
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
}`
)
return []
}
}
getLocalQueryList(): string[] {
try {
const file = path.join(__dirname, './search-queries.json')
const queries = JSON.parse(fs.readFileSync(file, 'utf8')) as string[]
const out = Array.isArray(queries) ? queries : []
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-LOCAL-QUERY-LIST',
'local queries loaded | file=search-queries.json'
)
if (!out.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-LOCAL-QUERY-LIST', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-LOCAL-QUERY-LIST',
'search-queries.json parsed but empty or invalid'
)
}
return out
} catch (error) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-LOCAL-QUERY-LIST', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-LOCAL-QUERY-LIST',
`read/parse failed | error=${
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
}`
)
return []
}
}
}

View File

@@ -76,7 +76,7 @@ export class SearchManager {
this.bot.logger.info('main', 'SEARCH-MANAGER', 'Closing mobile session') this.bot.logger.info('main', 'SEARCH-MANAGER', 'Closing mobile session')
try { try {
await executionContext.run({ isMobile: true, accountEmail }, async () => { await executionContext.run({ isMobile: true, account }, async () => {
await this.bot.browser.func.closeBrowser(mobileSession.context, accountEmail) await this.bot.browser.func.closeBrowser(mobileSession.context, accountEmail)
}) })
this.bot.logger.info('main', 'SEARCH-MANAGER', 'Mobile session closed') this.bot.logger.info('main', 'SEARCH-MANAGER', 'Mobile session closed')
@@ -368,7 +368,7 @@ export class SearchManager {
`Init | account=${accountEmail} | proxy=${account.proxy ?? 'none'}` `Init | account=${accountEmail} | proxy=${account.proxy ?? 'none'}`
) )
const session = await this.bot['browserFactory'].createBrowser(account.proxy, accountEmail) const session = await this.bot['browserFactory'].createBrowser(account)
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'Browser created, new page') this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'Browser created, new page')
this.bot.mainDesktopPage = await session.context.newPage() this.bot.mainDesktopPage = await session.context.newPage()
@@ -377,7 +377,7 @@ export class SearchManager {
this.bot.logger.info('main', 'SEARCH-DESKTOP-LOGIN', 'Login start') this.bot.logger.info('main', 'SEARCH-DESKTOP-LOGIN', 'Login start')
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'Calling login handler') this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'Calling login handler')
await this.bot['login'].login(this.bot.mainDesktopPage, accountEmail, account.password, account.totp) await this.bot['login'].login(this.bot.mainDesktopPage, account)
this.bot.logger.info('main', 'SEARCH-DESKTOP-LOGIN', 'Login passed, verifying') this.bot.logger.info('main', 'SEARCH-DESKTOP-LOGIN', 'Login passed, verifying')
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'verifyBingSession') this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'verifyBingSession')

View File

@@ -1,6 +1,12 @@
import type { Page } from 'patchright' import type { Page } from 'patchright'
import type { MicrosoftRewardsBot } from '../index' import type { MicrosoftRewardsBot } from '../index'
import type { DashboardData, PunchCard, BasePromotion, FindClippyPromotion } from '../interface/DashboardData' import type {
DashboardData,
PunchCard,
BasePromotion,
FindClippyPromotion,
PurplePromotionalItem
} from '../interface/DashboardData'
import type { AppDashboardData } from '../interface/AppDashBoardData' import type { AppDashboardData } from '../interface/AppDashBoardData'
export class Workers { export class Workers {
@@ -38,13 +44,14 @@ export class Workers {
] ]
const activitiesUncompleted: BasePromotion[] = const activitiesUncompleted: BasePromotion[] =
morePromotions?.filter( morePromotions?.filter(x => {
x => if (x.complete) return false
!x.complete && if (x.pointProgressMax <= 0) return false
x.pointProgressMax > 0 && if (x.exclusiveLockedFeatureStatus === 'locked') return false
x.exclusiveLockedFeatureStatus !== 'locked' && if (!x.promotionType) return false
x.promotionType
) ?? [] return true
}) ?? []
if (!activitiesUncompleted.length) { if (!activitiesUncompleted.length) {
this.bot.logger.info( this.bot.logger.info(
@@ -67,13 +74,14 @@ export class Workers {
} }
public async doAppPromotions(data: AppDashboardData) { public async doAppPromotions(data: AppDashboardData) {
const appRewards = data.response.promotions.filter( const appRewards = data.response.promotions.filter(x => {
x => if (x.attributes['complete']?.toLowerCase() !== 'false') return false
x.attributes['complete']?.toLowerCase() === 'false' && if (!x.attributes['offerid']) return false
x.attributes['offerid'] && if (!x.attributes['type']) return false
x.attributes['type'] && if (x.attributes['type'] !== 'sapphire') return false
x.attributes['type'] === 'sapphire'
) return true
})
if (!appRewards.length) { if (!appRewards.length) {
this.bot.logger.info( this.bot.logger.info(
@@ -93,6 +101,77 @@ export class Workers {
this.bot.logger.info(this.bot.isMobile, 'APP-PROMOTIONS', 'All "App Promotions" items have been completed') this.bot.logger.info(this.bot.isMobile, 'APP-PROMOTIONS', 'All "App Promotions" items have been completed')
} }
public async doSpecialPromotions(data: DashboardData) {
const specialPromotions: PurplePromotionalItem[] = [
...new Map(
[...(data.promotionalItems ?? [])]
.filter(Boolean)
.map(p => [p.offerId, p as PurplePromotionalItem] as const)
).values()
]
const supportedPromotions = ['ww_banner_optin_2x']
const specialPromotionsUncompleted: PurplePromotionalItem[] =
specialPromotions?.filter(x => {
if (x.complete) return false
if (x.exclusiveLockedFeatureStatus === 'locked') return false
if (!x.promotionType) return false
const offerId = (x.offerId ?? '').toLowerCase()
return supportedPromotions.some(s => offerId.includes(s))
}) ?? []
for (const activity of specialPromotionsUncompleted) {
try {
const type = activity.promotionType?.toLowerCase() ?? ''
const name = activity.name?.toLowerCase() ?? ''
const offerId = (activity as PurplePromotionalItem).offerId
this.bot.logger.debug(
this.bot.isMobile,
'SPECIAL-ACTIVITY',
`Processing activity | title="${activity.title}" | offerId=${offerId} | type=${type}"`
)
switch (type) {
// UrlReward
case 'urlreward': {
// Special "Double Search Points" activation
if (name.includes('ww_banner_optin_2x')) {
this.bot.logger.info(
this.bot.isMobile,
'ACTIVITY',
`Found activity type "Double Search Points" | title="${activity.title}" | offerId=${offerId}`
)
await this.bot.activities.doDoubleSearchPoints(activity)
}
break
}
// Unsupported types
default: {
this.bot.logger.warn(
this.bot.isMobile,
'SPECIAL-ACTIVITY',
`Skipped activity "${activity.title}" | offerId=${offerId} | Reason: Unsupported type "${activity.promotionType}"`
)
break
}
}
} catch (error) {
this.bot.logger.error(
this.bot.isMobile,
'SPECIAL-ACTIVITY',
`Error while solving activity "${activity.title}" | message=${error instanceof Error ? error.message : String(error)}`
)
}
}
this.bot.logger.info(this.bot.isMobile, 'SPECIAL-ACTIVITY', 'All "Special Activites" items have been completed')
}
private async solveActivities(activities: BasePromotion[], page: Page, punchCard?: PunchCard) { private async solveActivities(activities: BasePromotion[], page: Page, punchCard?: PunchCard) {
for (const activity of activities) { for (const activity of activities) {
try { try {

View File

@@ -0,0 +1,124 @@
import type { AxiosRequestConfig } from 'axios'
import { Workers } from '../../Workers'
import { PromotionalItem } from '../../../interface/DashboardData'
export class DoubleSearchPoints extends Workers {
private cookieHeader: string = ''
private fingerprintHeader: { [x: string]: string } = {}
public async doDoubleSearchPoints(promotion: PromotionalItem) {
const offerId = promotion.offerId
const activityType = promotion.activityType
try {
if (!this.bot.requestToken) {
this.bot.logger.warn(
this.bot.isMobile,
'DOUBLE-SEARCH-POINTS',
'Skipping: Request token not available, this activity requires it!'
)
return
}
this.cookieHeader = (this.bot.isMobile ? this.bot.cookies.mobile : this.bot.cookies.desktop)
.map((c: { name: string; value: string }) => `${c.name}=${c.value}`)
.join('; ')
const fingerprintHeaders = { ...this.bot.fingerprint.headers }
delete fingerprintHeaders['Cookie']
delete fingerprintHeaders['cookie']
this.fingerprintHeader = fingerprintHeaders
this.bot.logger.info(
this.bot.isMobile,
'DOUBLE-SEARCH-POINTS',
`Starting Double Search Points | offerId=${offerId}`
)
this.bot.logger.debug(
this.bot.isMobile,
'DOUBLE-SEARCH-POINTS',
`Prepared headers | cookieLength=${this.cookieHeader.length} | fingerprintHeaderKeys=${Object.keys(this.fingerprintHeader).length}`
)
const formData = new URLSearchParams({
id: offerId,
hash: promotion.hash,
timeZone: '60',
activityAmount: '1',
dbs: '0',
form: '',
type: activityType,
__RequestVerificationToken: this.bot.requestToken
})
this.bot.logger.debug(
this.bot.isMobile,
'DOUBLE-SEARCH-POINTS',
`Prepared Double Search Points form data | offerId=${offerId} | hash=${promotion.hash} | timeZone=60 | activityAmount=1 | type=${activityType}`
)
const request: AxiosRequestConfig = {
url: 'https://rewards.bing.com/api/reportactivity?X-Requested-With=XMLHttpRequest',
method: 'POST',
headers: {
...(this.bot.fingerprint?.headers ?? {}),
Cookie: this.cookieHeader,
Referer: 'https://rewards.bing.com/',
Origin: 'https://rewards.bing.com'
},
data: formData
}
this.bot.logger.debug(
this.bot.isMobile,
'DOUBLE-SEARCH-POINTS',
`Sending Double Search Points request | offerId=${offerId} | url=${request.url}`
)
const response = await this.bot.axios.request(request)
this.bot.logger.debug(
this.bot.isMobile,
'DOUBLE-SEARCH-POINTS',
`Received Double Search Points response | offerId=${offerId} | status=${response.status}`
)
const data = await this.bot.browser.func.getDashboardData()
const promotionalItem = data.promotionalItems.find(item =>
item.name.toLowerCase().includes('ww_banner_optin_2x')
)
// If OK, should no longer be presernt in promotionalItems
if (promotionalItem) {
this.bot.logger.warn(
this.bot.isMobile,
'DOUBLE-SEARCH-POINTS',
`Unable to find or activate Double Search Points | offerId=${offerId} | status=${response.status}`
)
} else {
this.bot.logger.info(
this.bot.isMobile,
'DOUBLE-SEARCH-POINTS',
`Activated Double Search Points | offerId=${offerId} | status=${response.status}`,
'green'
)
}
this.bot.logger.debug(
this.bot.isMobile,
'DOUBLE-SEARCH-POINTS',
`Waiting after Double Search Points | offerId=${offerId}`
)
await this.bot.utils.wait(this.bot.utils.randomDelay(5000, 10000))
} catch (error) {
this.bot.logger.error(
this.bot.isMobile,
'DOUBLE-SEARCH-POINTS',
`Error in doDoubleSearchPoints | offerId=${offerId} | message=${error instanceof Error ? error.message : String(error)}`
)
}
}
}

View File

@@ -33,38 +33,34 @@ export class Search extends Workers {
`Search points remaining | Edge=${missingPoints.edgePoints} | Desktop=${missingPoints.desktopPoints} | Mobile=${missingPoints.mobilePoints}` `Search points remaining | Edge=${missingPoints.edgePoints} | Desktop=${missingPoints.desktopPoints} | Mobile=${missingPoints.mobilePoints}`
) )
let queries: string[] = []
const queryCore = new QueryCore(this.bot) const queryCore = new QueryCore(this.bot)
const locale = (this.bot.userData.geoLocale ?? 'US').toUpperCase()
const langCode = (this.bot.userData.langCode ?? 'en').toLowerCase()
const locale = this.bot.userData.geoLocale.toUpperCase() this.bot.logger.debug(
isMobile,
'SEARCH-BING',
`Resolving search queries via QueryCore | locale=${locale} | lang=${langCode} | related=true`
)
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Resolving search queries | locale=${locale}`) let queries = await queryCore.queryManager({
shuffle: true,
related: true,
langCode,
geoLocale: locale,
sourceOrder: ['google', 'wikipedia', 'reddit', 'local']
})
// Set Google search queries queries = [...new Set(queries.map(q => q.trim()).filter(Boolean))]
queries = await queryCore.getGoogleTrends(locale)
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Fetched base queries | count=${queries.length}`) this.bot.logger.debug(isMobile, 'SEARCH-BING', `Query pool ready | count=${queries.length}`)
// Deduplicate queries
queries = [...new Set(queries)]
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Deduplicated queries | count=${queries.length}`)
// Shuffle
queries = this.bot.utils.shuffleArray(queries)
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Shuffled queries | count=${queries.length}`)
// Go to bing // Go to bing
const targetUrl = this.searchPageURL ? this.searchPageURL : this.bingHome const targetUrl = this.searchPageURL ? this.searchPageURL : this.bingHome
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Navigating to search page | url=${targetUrl}`) this.bot.logger.debug(isMobile, 'SEARCH-BING', `Navigating to search page | url=${targetUrl}`)
await page.goto(targetUrl) await page.goto(targetUrl)
// Wait until page loaded
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {}) await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
await this.bot.browser.utils.tryDismissAllMessages(page) await this.bot.browser.utils.tryDismissAllMessages(page)
let stagnantLoop = 0 let stagnantLoop = 0
@@ -77,7 +73,6 @@ export class Search extends Workers {
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile) const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
const newMissingPointsTotal = newMissingPoints.totalPoints const newMissingPointsTotal = newMissingPoints.totalPoints
// Points gained for THIS query only
const rawGained = missingPointsTotal - newMissingPointsTotal const rawGained = missingPointsTotal - newMissingPointsTotal
const gainedPoints = Math.max(0, rawGained) const gainedPoints = Math.max(0, rawGained)
@@ -91,12 +86,10 @@ export class Search extends Workers {
} else { } else {
stagnantLoop = 0 stagnantLoop = 0
// Update global user data
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
this.bot.userData.currentPoints = newBalance this.bot.userData.currentPoints = newBalance
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
// Track for return value
totalGainedPoints += gainedPoints totalGainedPoints += gainedPoints
this.bot.logger.info( this.bot.logger.info(
@@ -107,10 +100,8 @@ export class Search extends Workers {
) )
} }
// Update loop state
missingPointsTotal = newMissingPointsTotal missingPointsTotal = newMissingPointsTotal
// Completed
if (missingPointsTotal === 0) { if (missingPointsTotal === 0) {
this.bot.logger.info( this.bot.logger.info(
isMobile, isMobile,
@@ -120,7 +111,6 @@ export class Search extends Workers {
break break
} }
// Stuck
if (stagnantLoop > stagnantLoopMax) { if (stagnantLoop > stagnantLoopMax) {
this.bot.logger.warn( this.bot.logger.warn(
isMobile, isMobile,
@@ -130,48 +120,72 @@ export class Search extends Workers {
stagnantLoop = 0 stagnantLoop = 0
break break
} }
const remainingQueries = queries.length - (i + 1)
const minBuffer = 20
if (missingPointsTotal > 0 && remainingQueries < minBuffer) {
this.bot.logger.warn(
isMobile,
'SEARCH-BING',
`Low query buffer while still missing points, regenerating | remainingQueries=${remainingQueries} | missing=${missingPointsTotal}`
)
const extra = await queryCore.queryManager({
shuffle: true,
related: true,
langCode,
geoLocale: locale,
sourceOrder: this.bot.config.searchSettings.queryEngines
})
const merged = [...queries, ...extra].map(q => q.trim()).filter(Boolean)
queries = [...new Set(merged)]
queries = this.bot.utils.shuffleArray(queries)
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Query pool regenerated | count=${queries.length}`)
}
} }
if (missingPointsTotal > 0) { if (missingPointsTotal > 0) {
this.bot.logger.info( this.bot.logger.info(
isMobile, isMobile,
'SEARCH-BING', 'SEARCH-BING',
`Search completed but still missing points, generating extra searches | remaining=${missingPointsTotal}` `Search completed but still missing points, continuing with regenerated queries | remaining=${missingPointsTotal}`
) )
let i = 0
let stagnantLoop = 0 let stagnantLoop = 0
const stagnantLoopMax = 5 const stagnantLoopMax = 5
while (missingPointsTotal > 0) { while (missingPointsTotal > 0) {
const query = queries[i++] as string const extra = await queryCore.queryManager({
shuffle: true,
related: true,
langCode,
geoLocale: locale,
sourceOrder: this.bot.config.searchSettings.queryEngines
})
const merged = [...queries, ...extra].map(q => q.trim()).filter(Boolean)
const newPool = [...new Set(merged)]
queries = this.bot.utils.shuffleArray(newPool)
this.bot.logger.debug( this.bot.logger.debug(
isMobile, isMobile,
'SEARCH-BING-EXTRA', 'SEARCH-BING-EXTRA',
`Fetching related terms for extra searches | baseQuery="${query}"` `New query pool generated | count=${queries.length}`
) )
const relatedTerms = await queryCore.getBingRelatedTerms(query) for (const query of queries) {
this.bot.logger.debug(
isMobile,
'SEARCH-BING-EXTRA',
`Related terms resolved | baseQuery="${query}" | count=${relatedTerms.length}`
)
if (relatedTerms.length > 3) {
for (const term of relatedTerms.slice(1, 3)) {
this.bot.logger.info( this.bot.logger.info(
isMobile, isMobile,
'SEARCH-BING-EXTRA', 'SEARCH-BING-EXTRA',
`Extra search | remaining=${missingPointsTotal} | query="${term}"` `Extra search | remaining=${missingPointsTotal} | query="${query}"`
) )
searchCounters = await this.bingSearch(page, term, isMobile) searchCounters = await this.bingSearch(page, query, isMobile)
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile) const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
const newMissingPointsTotal = newMissingPoints.totalPoints const newMissingPointsTotal = newMissingPoints.totalPoints
// Points gained for THIS extra query only
const rawGained = missingPointsTotal - newMissingPointsTotal const rawGained = missingPointsTotal - newMissingPointsTotal
const gainedPoints = Math.max(0, rawGained) const gainedPoints = Math.max(0, rawGained)
@@ -180,31 +194,27 @@ export class Search extends Workers {
this.bot.logger.info( this.bot.logger.info(
isMobile, isMobile,
'SEARCH-BING-EXTRA', 'SEARCH-BING-EXTRA',
`No points gained for extra query ${stagnantLoop}/${stagnantLoopMax} | query="${term}" | remaining=${newMissingPointsTotal}` `No points gained ${stagnantLoop}/${stagnantLoopMax} | query="${query}" | remaining=${newMissingPointsTotal}`
) )
} else { } else {
stagnantLoop = 0 stagnantLoop = 0
// Update global user data
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
this.bot.userData.currentPoints = newBalance this.bot.userData.currentPoints = newBalance
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
// Track for return value
totalGainedPoints += gainedPoints totalGainedPoints += gainedPoints
this.bot.logger.info( this.bot.logger.info(
isMobile, isMobile,
'SEARCH-BING-EXTRA', 'SEARCH-BING-EXTRA',
`gainedPoints=${gainedPoints} points | query="${term}" | remaining=${newMissingPointsTotal}`, `gainedPoints=${gainedPoints} points | query="${query}" | remaining=${newMissingPointsTotal}`,
'green' 'green'
) )
} }
// Update loop state
missingPointsTotal = newMissingPointsTotal missingPointsTotal = newMissingPointsTotal
// Completed
if (missingPointsTotal === 0) { if (missingPointsTotal === 0) {
this.bot.logger.info( this.bot.logger.info(
isMobile, isMobile,
@@ -214,12 +224,11 @@ export class Search extends Workers {
break break
} }
// Stuck again
if (stagnantLoop > stagnantLoopMax) { if (stagnantLoop > stagnantLoopMax) {
this.bot.logger.warn( this.bot.logger.warn(
isMobile, isMobile,
'SEARCH-BING-EXTRA', 'SEARCH-BING-EXTRA',
`Search did not gain points for ${stagnantLoopMax} extra iterations, aborting extra searches` `Search did not gain points for ${stagnantLoopMax} iterations, aborting extra searches`
) )
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance) const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
this.bot.logger.info( this.bot.logger.info(
@@ -232,7 +241,6 @@ export class Search extends Workers {
} }
} }
} }
}
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance) const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
@@ -259,7 +267,6 @@ export class Search extends Workers {
this.searchCount++ this.searchCount++
// Page fill seems to get more sluggish over time
if (this.searchCount % refreshThreshold === 0) { if (this.searchCount % refreshThreshold === 0) {
this.bot.logger.info( this.bot.logger.info(
isMobile, isMobile,
@@ -271,7 +278,7 @@ export class Search extends Workers {
await searchPage.goto(this.bingHome) await searchPage.goto(this.bingHome)
await searchPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {}) await searchPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
await this.bot.browser.utils.tryDismissAllMessages(searchPage) // Not always the case but possible for new cookie headers await this.bot.browser.utils.tryDismissAllMessages(searchPage)
} }
this.bot.logger.debug( this.bot.logger.debug(
@@ -402,11 +409,9 @@ export class Search extends Workers {
await this.bot.utils.wait(this.bot.config.searchSettings.searchResultVisitTime) await this.bot.utils.wait(this.bot.config.searchSettings.searchResultVisitTime)
if (isMobile) { if (isMobile) {
// Mobile
await page.goto(searchPageUrl) await page.goto(searchPageUrl)
this.bot.logger.debug(isMobile, 'SEARCH-RANDOM-CLICK', 'Navigated back to search page') this.bot.logger.debug(isMobile, 'SEARCH-RANDOM-CLICK', 'Navigated back to search page')
} else { } else {
// Desktop
const newTab = await this.bot.browser.utils.getLatestTab(page) const newTab = await this.bot.browser.utils.getLatestTab(page)
const newTabUrl = newTab.url() const newTabUrl = newTab.url()

View File

@@ -232,7 +232,7 @@ export class SearchOnBing extends Workers {
if (this.bot.config.searchOnBingLocalQueries) { if (this.bot.config.searchOnBingLocalQueries) {
this.bot.logger.debug(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', 'Using local queries config file') this.bot.logger.debug(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', 'Using local queries config file')
const data = fs.readFileSync(path.join(__dirname, '../queries.json'), 'utf8') const data = fs.readFileSync(path.join(__dirname, '../bing-search-activity-queries.json'), 'utf8')
queries = JSON.parse(data) queries = JSON.parse(data)
this.bot.logger.debug( this.bot.logger.debug(
@@ -250,7 +250,7 @@ export class SearchOnBing extends Workers {
// Fetch from the repo directly so the user doesn't need to redownload the script for the new activities // Fetch from the repo directly so the user doesn't need to redownload the script for the new activities
const response = await this.bot.axios.request({ const response = await this.bot.axios.request({
method: 'GET', method: 'GET',
url: 'https://raw.githubusercontent.com/TheNetsky/Microsoft-Rewards-Script/refs/heads/v3/src/functions/queries.json' url: 'https://raw.githubusercontent.com/TheNetsky/Microsoft-Rewards-Script/refs/heads/v3/src/functions/bing-search-activity-queries.json'
}) })
queries = response.data queries = response.data

View File

@@ -0,0 +1,582 @@
[
{
"title": "Houses near you",
"queries": [
"Houses near me",
"Homes for sale near me",
"Apartments near me",
"Real estate listings near me",
"Zillow homes near me",
"houses for rent near me"
]
},
{
"title": "Feeling symptoms?",
"queries": [
"Rash on forearm",
"Stuffy nose",
"Tickling cough",
"sore throat remedies",
"headache and nausea causes",
"fever symptoms adults"
]
},
{
"title": "Get your shopping done faster",
"queries": [
"Buy PS5",
"Buy Xbox",
"Chair deals",
"wireless mouse deals",
"best gaming headset price",
"laptop deals",
"buy office chair",
"SSD deals"
]
},
{
"title": "Translate anything",
"queries": [
"Translate welcome home to Korean",
"Translate welcome home to Japanese",
"Translate goodbye to Japanese",
"Translate good morning to Spanish",
"Translate thank you to French",
"Translate see you later to Italian"
]
},
{
"title": "Search the lyrics of a song",
"queries": [
"Debarge rhythm of the night lyrics",
"bohemian rhapsody lyrics",
"hotel california lyrics",
"blinding lights lyrics",
"lose yourself lyrics",
"smells like teen spirit lyrics"
]
},
{
"title": "Let's watch that movie again!",
"queries": [
"Alien movie",
"Aliens movie",
"Alien 3 movie",
"Predator movie",
"Terminator movie",
"John Wick movie",
"Interstellar movie",
"The Matrix movie"
]
},
{
"title": "Plan a quick getaway",
"queries": [
"Flights Amsterdam to Tokyo",
"Flights New York to Tokyo",
"cheap flights to paris",
"flights amsterdam to rome",
"last minute flight deals",
"direct flights from amsterdam",
"weekend getaway europe",
"best time to visit tokyo"
]
},
{
"title": "Discover open job roles",
"queries": [
"jobs at Microsoft",
"Microsoft Job Openings",
"Jobs near me",
"jobs at Boeing worked",
"software engineer jobs near me",
"remote developer jobs",
"IT jobs netherlands",
"customer support jobs near me"
]
},
{
"title": "You can track your package",
"queries": [
"USPS tracking",
"UPS tracking",
"DHL tracking",
"FedEx tracking",
"track my package",
"international package tracking"
]
},
{
"title": "Find somewhere new to explore",
"queries": [
"Directions to Berlin",
"Directions to Tokyo",
"Directions to New York",
"things to do in berlin",
"tourist attractions tokyo",
"best places to visit in new york",
"hidden gems near me",
"day trips near me"
]
},
{
"title": "Too tired to cook tonight?",
"queries": [
"KFC near me",
"Burger King near me",
"McDonalds near me",
"pizza delivery near me",
"restaurants open now",
"best takeout near me",
"quick dinner ideas",
"easy dinner recipes"
]
},
{
"title": "Quickly convert your money",
"queries": [
"convert 250 USD to yen",
"convert 500 USD to yen",
"usd to eur",
"gbp to eur",
"eur to jpy",
"currency converter",
"exchange rate today",
"1000 yen to euro"
]
},
{
"title": "Learn to cook a new recipe",
"queries": [
"How to cook ratatouille",
"How to cook lasagna",
"easy pasta recipe",
"how to make pancakes",
"how to make fried rice",
"simple chicken recipe"
]
},
{
"title": "Find places to stay!",
"queries": [
"Hotels Berlin Germany",
"Hotels Amsterdam Netherlands",
"hotels in paris",
"best hotels in tokyo",
"cheap hotels london",
"places to stay in barcelona",
"hotel deals",
"booking hotels near me"
]
},
{
"title": "How's the economy?",
"queries": [
"sp 500",
"nasdaq",
"dow jones today",
"inflation rate europe",
"interest rates today",
"stock market today",
"economic news",
"recession forecast"
]
},
{
"title": "Who won?",
"queries": [
"braves score",
"champions league results",
"premier league results",
"nba score",
"formula 1 winner",
"latest football scores",
"ucl final winner",
"world cup final result"
]
},
{
"title": "Gaming time",
"queries": [
"Overwatch video game",
"Call of duty video game",
"best games 2025",
"top xbox games",
"popular steam games",
"new pc games",
"game reviews",
"best co-op games"
]
},
{
"title": "Expand your vocabulary",
"queries": [
"definition definition",
"meaning of serendipity",
"define nostalgia",
"synonym for happy",
"define eloquent",
"what does epiphany mean",
"word of the day",
"define immaculate"
]
},
{
"title": "What time is it?",
"queries": [
"Japan time",
"New York time",
"time in london",
"time in tokyo",
"current time in amsterdam",
"time in los angeles"
]
},
{
"title": "Find deals on Bing",
"queries": [
"best laptop deals",
"tech deals today",
"wireless earbuds deals",
"gaming chair deals",
"discount codes electronics",
"best amazon deals today",
"smartphone deals",
"ssd deals"
]
},
{
"title": "Prepare for the weather",
"queries": [
"weather tomorrow",
"weekly weather forecast",
"rain forecast today",
"weather in amsterdam",
"storm forecast europe",
"uv index today",
"temperature this weekend",
"snow forecast"
]
},
{
"title": "Track your delivery",
"queries": [
"track my package",
"postnl track and trace",
"dhl parcel tracking",
"ups tracking",
"fedex tracking",
"usps tracking",
"parcel tracking",
"international package tracking"
]
},
{
"title": "Explore a new spot today",
"queries": [
"places to visit near me",
"things to do near me",
"hidden gems netherlands",
"best museums near me",
"parks near me",
"tourist attractions nearby",
"best cafes near me",
"day trip ideas"
]
},
{
"title": "Maisons près de chez vous",
"queries": [
"Maisons près de chez moi",
"Maisons à vendre près de chez moi",
"Appartements près de chez moi",
"Annonces immobilières près de chez moi",
"Maisons à louer près de chez moi"
]
},
{
"title": "Vous ressentez des symptômes ?",
"queries": [
"Éruption cutanée sur l'avant-bras",
"Nez bouché",
"Toux chatouilleuse",
"mal de gorge remèdes",
"maux de tête causes",
"symptômes de la grippe"
]
},
{
"title": "Faites vos achats plus vite",
"queries": [
"Acheter une PS5",
"Acheter une Xbox",
"Offres sur les chaises",
"offres ordinateur portable",
"meilleures offres casque",
"acheter souris sans fil",
"promotions ssd",
"bons plans tech"
]
},
{
"title": "Traduisez tout !",
"queries": [
"Traduction bienvenue à la maison en coréen",
"Traduction bienvenue à la maison en japonais",
"Traduction au revoir en japonais",
"Traduire bonjour en espagnol",
"Traduire merci en anglais",
"Traduire à plus tard en italien"
]
},
{
"title": "Rechercher paroles de chanson",
"queries": [
"Paroles de Debarge rhythm of the night",
"paroles bohemian rhapsody",
"paroles hotel california",
"paroles blinding lights",
"paroles lose yourself",
"paroles smells like teen spirit"
]
},
{
"title": "Et si nous regardions ce film une nouvelle fois?",
"queries": [
"Alien film",
"Film Aliens",
"Film Alien 3",
"Film Predator",
"Film Terminator",
"Film John Wick",
"Film Interstellar",
"Film Matrix"
]
},
{
"title": "Planifiez une petite escapade",
"queries": [
"Vols Amsterdam-Tokyo",
"Vols New York-Tokyo",
"vols pas chers paris",
"vols amsterdam rome",
"offres vols dernière minute",
"week-end en europe",
"vols directs depuis amsterdam"
]
},
{
"title": "Consulter postes à pourvoir",
"queries": [
"emplois chez Microsoft",
"Offres d'emploi Microsoft",
"Emplois près de chez moi",
"emplois chez Boeing",
"emplois développeur à distance",
"emplois informatique pays-bas",
"offres d'emploi près de chez moi"
]
},
{
"title": "Vous pouvez suivre votre colis",
"queries": [
"Suivi Chronopost",
"suivi colis",
"suivi DHL",
"suivi UPS",
"suivi FedEx",
"suivi international colis"
]
},
{
"title": "Trouver un endroit à découvrir",
"queries": [
"Itinéraire vers Berlin",
"Itinéraire vers Tokyo",
"Itinéraire vers New York",
"que faire à berlin",
"attractions tokyo",
"meilleurs endroits à visiter à new york",
"endroits à visiter près de chez moi"
]
},
{
"title": "Trop fatigué pour cuisiner ce soir ?",
"queries": [
"KFC près de chez moi",
"Burger King près de chez moi",
"McDonalds près de chez moi",
"livraison pizza près de chez moi",
"restaurants ouverts maintenant",
"idées dîner rapide",
"quoi manger ce soir"
]
},
{
"title": "Convertissez rapidement votre argent",
"queries": [
"convertir 250 EUR en yen",
"convertir 500 EUR en yen",
"usd en eur",
"gbp en eur",
"eur en jpy",
"convertisseur de devises",
"taux de change aujourd'hui",
"1000 yen en euro"
]
},
{
"title": "Apprenez à cuisiner une nouvelle recette",
"queries": [
"Comment faire cuire la ratatouille",
"Comment faire cuire les lasagnes",
"recette pâtes facile",
"comment faire des crêpes",
"recette riz sauté",
"recette poulet simple"
]
},
{
"title": "Trouvez des emplacements pour rester!",
"queries": [
"Hôtels Berlin Allemagne",
"Hôtels Amsterdam Pays-Bas",
"hôtels paris",
"meilleurs hôtels tokyo",
"hôtels pas chers londres",
"hébergement barcelone",
"offres hôtels",
"hôtels près de chez moi"
]
},
{
"title": "Comment se porte l'économie ?",
"queries": [
"CAC 40",
"indice dax",
"dow jones aujourd'hui",
"inflation europe",
"taux d'intérêt aujourd'hui",
"marché boursier aujourd'hui",
"actualités économie",
"prévisions récession"
]
},
{
"title": "Qui a gagné ?",
"queries": [
"score du Paris Saint-Germain",
"résultats ligue des champions",
"résultats premier league",
"score nba",
"vainqueur formule 1",
"derniers scores football",
"vainqueur finale ldc"
]
},
{
"title": "Temps de jeu",
"queries": [
"Jeu vidéo Overwatch",
"Jeu vidéo Call of Duty",
"meilleurs jeux 2025",
"top jeux xbox",
"jeux steam populaires",
"nouveaux jeux pc",
"avis jeux vidéo",
"meilleurs jeux coop"
]
},
{
"title": "Enrichissez votre vocabulaire",
"queries": [
"definition definition",
"signification sérendipité",
"définir nostalgie",
"synonyme heureux",
"définir éloquent",
"mot du jour",
"que veut dire épiphanie"
]
},
{
"title": "Quelle heure est-il ?",
"queries": [
"Heure du Japon",
"Heure de New York",
"heure de londres",
"heure de tokyo",
"heure actuelle amsterdam",
"heure de los angeles"
]
},
{
"title": "Vérifier la météo",
"queries": [
"Météo de Paris",
"Météo de la France",
"météo demain",
"prévisions météo semaine",
"météo amsterdam",
"risque de pluie aujourd'hui"
]
},
{
"title": "Tenez-vous informé des sujets d'actualité",
"queries": [
"Augmentation Impots",
"Mort célébrité",
"actualités france",
"actualité internationale",
"dernières nouvelles économie",
"news technologie"
]
},
{
"title": "Préparez-vous pour la météo",
"queries": [
"météo demain",
"prévisions météo semaine",
"météo amsterdam",
"risque de pluie aujourd'hui",
"indice uv aujourd'hui",
"température ce week-end",
"alerte tempête"
]
},
{
"title": "Suivez votre livraison",
"queries": [
"suivi colis",
"postnl suivi colis",
"suivi DHL colis",
"suivi UPS",
"suivi FedEx",
"suivi international colis",
"suivre ma livraison"
]
},
{
"title": "Trouvez des offres sur Bing",
"queries": [
"meilleures offres ordinateur portable",
"bons plans tech",
"promotions écouteurs",
"offres chaise gamer",
"codes promo électronique",
"meilleures offres amazon aujourd'hui"
]
},
{
"title": "Explorez un nouvel endroit aujourd'hui",
"queries": [
"endroits à visiter près de chez moi",
"que faire près de chez moi",
"endroits insolites pays-bas",
"meilleurs musées près de chez moi",
"parcs près de chez moi",
"attractions touristiques à proximité",
"meilleurs cafés près de chez moi"
]
}
]

582
src/functions/queries.json Normal file
View File

@@ -0,0 +1,582 @@
[
{
"title": "Houses near you",
"queries": [
"Houses near me",
"Homes for sale near me",
"Apartments near me",
"Real estate listings near me",
"Zillow homes near me",
"houses for rent near me"
]
},
{
"title": "Feeling symptoms?",
"queries": [
"Rash on forearm",
"Stuffy nose",
"Tickling cough",
"sore throat remedies",
"headache and nausea causes",
"fever symptoms adults"
]
},
{
"title": "Get your shopping done faster",
"queries": [
"Buy PS5",
"Buy Xbox",
"Chair deals",
"wireless mouse deals",
"best gaming headset price",
"laptop deals",
"buy office chair",
"SSD deals"
]
},
{
"title": "Translate anything",
"queries": [
"Translate welcome home to Korean",
"Translate welcome home to Japanese",
"Translate goodbye to Japanese",
"Translate good morning to Spanish",
"Translate thank you to French",
"Translate see you later to Italian"
]
},
{
"title": "Search the lyrics of a song",
"queries": [
"Debarge rhythm of the night lyrics",
"bohemian rhapsody lyrics",
"hotel california lyrics",
"blinding lights lyrics",
"lose yourself lyrics",
"smells like teen spirit lyrics"
]
},
{
"title": "Let's watch that movie again!",
"queries": [
"Alien movie",
"Aliens movie",
"Alien 3 movie",
"Predator movie",
"Terminator movie",
"John Wick movie",
"Interstellar movie",
"The Matrix movie"
]
},
{
"title": "Plan a quick getaway",
"queries": [
"Flights Amsterdam to Tokyo",
"Flights New York to Tokyo",
"cheap flights to paris",
"flights amsterdam to rome",
"last minute flight deals",
"direct flights from amsterdam",
"weekend getaway europe",
"best time to visit tokyo"
]
},
{
"title": "Discover open job roles",
"queries": [
"jobs at Microsoft",
"Microsoft Job Openings",
"Jobs near me",
"jobs at Boeing worked",
"software engineer jobs near me",
"remote developer jobs",
"IT jobs netherlands",
"customer support jobs near me"
]
},
{
"title": "You can track your package",
"queries": [
"USPS tracking",
"UPS tracking",
"DHL tracking",
"FedEx tracking",
"track my package",
"international package tracking"
]
},
{
"title": "Find somewhere new to explore",
"queries": [
"Directions to Berlin",
"Directions to Tokyo",
"Directions to New York",
"things to do in berlin",
"tourist attractions tokyo",
"best places to visit in new york",
"hidden gems near me",
"day trips near me"
]
},
{
"title": "Too tired to cook tonight?",
"queries": [
"KFC near me",
"Burger King near me",
"McDonalds near me",
"pizza delivery near me",
"restaurants open now",
"best takeout near me",
"quick dinner ideas",
"easy dinner recipes"
]
},
{
"title": "Quickly convert your money",
"queries": [
"convert 250 USD to yen",
"convert 500 USD to yen",
"usd to eur",
"gbp to eur",
"eur to jpy",
"currency converter",
"exchange rate today",
"1000 yen to euro"
]
},
{
"title": "Learn to cook a new recipe",
"queries": [
"How to cook ratatouille",
"How to cook lasagna",
"easy pasta recipe",
"how to make pancakes",
"how to make fried rice",
"simple chicken recipe"
]
},
{
"title": "Find places to stay!",
"queries": [
"Hotels Berlin Germany",
"Hotels Amsterdam Netherlands",
"hotels in paris",
"best hotels in tokyo",
"cheap hotels london",
"places to stay in barcelona",
"hotel deals",
"booking hotels near me"
]
},
{
"title": "How's the economy?",
"queries": [
"sp 500",
"nasdaq",
"dow jones today",
"inflation rate europe",
"interest rates today",
"stock market today",
"economic news",
"recession forecast"
]
},
{
"title": "Who won?",
"queries": [
"braves score",
"champions league results",
"premier league results",
"nba score",
"formula 1 winner",
"latest football scores",
"ucl final winner",
"world cup final result"
]
},
{
"title": "Gaming time",
"queries": [
"Overwatch video game",
"Call of duty video game",
"best games 2025",
"top xbox games",
"popular steam games",
"new pc games",
"game reviews",
"best co-op games"
]
},
{
"title": "Expand your vocabulary",
"queries": [
"definition definition",
"meaning of serendipity",
"define nostalgia",
"synonym for happy",
"define eloquent",
"what does epiphany mean",
"word of the day",
"define immaculate"
]
},
{
"title": "What time is it?",
"queries": [
"Japan time",
"New York time",
"time in london",
"time in tokyo",
"current time in amsterdam",
"time in los angeles"
]
},
{
"title": "Find deals on Bing",
"queries": [
"best laptop deals",
"tech deals today",
"wireless earbuds deals",
"gaming chair deals",
"discount codes electronics",
"best amazon deals today",
"smartphone deals",
"ssd deals"
]
},
{
"title": "Prepare for the weather",
"queries": [
"weather tomorrow",
"weekly weather forecast",
"rain forecast today",
"weather in amsterdam",
"storm forecast europe",
"uv index today",
"temperature this weekend",
"snow forecast"
]
},
{
"title": "Track your delivery",
"queries": [
"track my package",
"postnl track and trace",
"dhl parcel tracking",
"ups tracking",
"fedex tracking",
"usps tracking",
"parcel tracking",
"international package tracking"
]
},
{
"title": "Explore a new spot today",
"queries": [
"places to visit near me",
"things to do near me",
"hidden gems netherlands",
"best museums near me",
"parks near me",
"tourist attractions nearby",
"best cafes near me",
"day trip ideas"
]
},
{
"title": "Maisons près de chez vous",
"queries": [
"Maisons près de chez moi",
"Maisons à vendre près de chez moi",
"Appartements près de chez moi",
"Annonces immobilières près de chez moi",
"Maisons à louer près de chez moi"
]
},
{
"title": "Vous ressentez des symptômes ?",
"queries": [
"Éruption cutanée sur l'avant-bras",
"Nez bouché",
"Toux chatouilleuse",
"mal de gorge remèdes",
"maux de tête causes",
"symptômes de la grippe"
]
},
{
"title": "Faites vos achats plus vite",
"queries": [
"Acheter une PS5",
"Acheter une Xbox",
"Offres sur les chaises",
"offres ordinateur portable",
"meilleures offres casque",
"acheter souris sans fil",
"promotions ssd",
"bons plans tech"
]
},
{
"title": "Traduisez tout !",
"queries": [
"Traduction bienvenue à la maison en coréen",
"Traduction bienvenue à la maison en japonais",
"Traduction au revoir en japonais",
"Traduire bonjour en espagnol",
"Traduire merci en anglais",
"Traduire à plus tard en italien"
]
},
{
"title": "Rechercher paroles de chanson",
"queries": [
"Paroles de Debarge rhythm of the night",
"paroles bohemian rhapsody",
"paroles hotel california",
"paroles blinding lights",
"paroles lose yourself",
"paroles smells like teen spirit"
]
},
{
"title": "Et si nous regardions ce film une nouvelle fois?",
"queries": [
"Alien film",
"Film Aliens",
"Film Alien 3",
"Film Predator",
"Film Terminator",
"Film John Wick",
"Film Interstellar",
"Film Matrix"
]
},
{
"title": "Planifiez une petite escapade",
"queries": [
"Vols Amsterdam-Tokyo",
"Vols New York-Tokyo",
"vols pas chers paris",
"vols amsterdam rome",
"offres vols dernière minute",
"week-end en europe",
"vols directs depuis amsterdam"
]
},
{
"title": "Consulter postes à pourvoir",
"queries": [
"emplois chez Microsoft",
"Offres d'emploi Microsoft",
"Emplois près de chez moi",
"emplois chez Boeing",
"emplois développeur à distance",
"emplois informatique pays-bas",
"offres d'emploi près de chez moi"
]
},
{
"title": "Vous pouvez suivre votre colis",
"queries": [
"Suivi Chronopost",
"suivi colis",
"suivi DHL",
"suivi UPS",
"suivi FedEx",
"suivi international colis"
]
},
{
"title": "Trouver un endroit à découvrir",
"queries": [
"Itinéraire vers Berlin",
"Itinéraire vers Tokyo",
"Itinéraire vers New York",
"que faire à berlin",
"attractions tokyo",
"meilleurs endroits à visiter à new york",
"endroits à visiter près de chez moi"
]
},
{
"title": "Trop fatigué pour cuisiner ce soir ?",
"queries": [
"KFC près de chez moi",
"Burger King près de chez moi",
"McDonalds près de chez moi",
"livraison pizza près de chez moi",
"restaurants ouverts maintenant",
"idées dîner rapide",
"quoi manger ce soir"
]
},
{
"title": "Convertissez rapidement votre argent",
"queries": [
"convertir 250 EUR en yen",
"convertir 500 EUR en yen",
"usd en eur",
"gbp en eur",
"eur en jpy",
"convertisseur de devises",
"taux de change aujourd'hui",
"1000 yen en euro"
]
},
{
"title": "Apprenez à cuisiner une nouvelle recette",
"queries": [
"Comment faire cuire la ratatouille",
"Comment faire cuire les lasagnes",
"recette pâtes facile",
"comment faire des crêpes",
"recette riz sauté",
"recette poulet simple"
]
},
{
"title": "Trouvez des emplacements pour rester!",
"queries": [
"Hôtels Berlin Allemagne",
"Hôtels Amsterdam Pays-Bas",
"hôtels paris",
"meilleurs hôtels tokyo",
"hôtels pas chers londres",
"hébergement barcelone",
"offres hôtels",
"hôtels près de chez moi"
]
},
{
"title": "Comment se porte l'économie ?",
"queries": [
"CAC 40",
"indice dax",
"dow jones aujourd'hui",
"inflation europe",
"taux d'intérêt aujourd'hui",
"marché boursier aujourd'hui",
"actualités économie",
"prévisions récession"
]
},
{
"title": "Qui a gagné ?",
"queries": [
"score du Paris Saint-Germain",
"résultats ligue des champions",
"résultats premier league",
"score nba",
"vainqueur formule 1",
"derniers scores football",
"vainqueur finale ldc"
]
},
{
"title": "Temps de jeu",
"queries": [
"Jeu vidéo Overwatch",
"Jeu vidéo Call of Duty",
"meilleurs jeux 2025",
"top jeux xbox",
"jeux steam populaires",
"nouveaux jeux pc",
"avis jeux vidéo",
"meilleurs jeux coop"
]
},
{
"title": "Enrichissez votre vocabulaire",
"queries": [
"definition definition",
"signification sérendipité",
"définir nostalgie",
"synonyme heureux",
"définir éloquent",
"mot du jour",
"que veut dire épiphanie"
]
},
{
"title": "Quelle heure est-il ?",
"queries": [
"Heure du Japon",
"Heure de New York",
"heure de londres",
"heure de tokyo",
"heure actuelle amsterdam",
"heure de los angeles"
]
},
{
"title": "Vérifier la météo",
"queries": [
"Météo de Paris",
"Météo de la France",
"météo demain",
"prévisions météo semaine",
"météo amsterdam",
"risque de pluie aujourd'hui"
]
},
{
"title": "Tenez-vous informé des sujets d'actualité",
"queries": [
"Augmentation Impots",
"Mort célébrité",
"actualités france",
"actualité internationale",
"dernières nouvelles économie",
"news technologie"
]
},
{
"title": "Préparez-vous pour la météo",
"queries": [
"météo demain",
"prévisions météo semaine",
"météo amsterdam",
"risque de pluie aujourd'hui",
"indice uv aujourd'hui",
"température ce week-end",
"alerte tempête"
]
},
{
"title": "Suivez votre livraison",
"queries": [
"suivi colis",
"postnl suivi colis",
"suivi DHL colis",
"suivi UPS",
"suivi FedEx",
"suivi international colis",
"suivre ma livraison"
]
},
{
"title": "Trouvez des offres sur Bing",
"queries": [
"meilleures offres ordinateur portable",
"bons plans tech",
"promotions écouteurs",
"offres chaise gamer",
"codes promo électronique",
"meilleures offres amazon aujourd'hui"
]
},
{
"title": "Explorez un nouvel endroit aujourd'hui",
"queries": [
"endroits à visiter près de chez moi",
"que faire près de chez moi",
"endroits insolites pays-bas",
"meilleurs musées près de chez moi",
"parcs près de chez moi",
"attractions touristiques à proximité",
"meilleurs cafés près de chez moi"
]
}
]

View File

@@ -0,0 +1,116 @@
[
"weather tomorrow",
"how to cook pasta",
"best movies 2024",
"latest tech news",
"how tall is the eiffel tower",
"easy dinner recipes",
"what time is it in japan",
"how does photosynthesis work",
"best budget smartphones",
"coffee vs espresso difference",
"how to improve wifi signal",
"popular netflix series",
"how many calories in an apple",
"world population today",
"best free pc games",
"how to clean a keyboard",
"what is artificial intelligence",
"simple home workouts",
"how long do cats live",
"famous paintings in museums",
"how to boil eggs",
"latest windows updates",
"how to screenshot on windows",
"best travel destinations europe",
"what is cloud computing",
"how to save money monthly",
"best youtube channels",
"how fast is light",
"how to learn programming",
"popular board games",
"how to make pancakes",
"capital cities of europe",
"how does a vpn work",
"best productivity apps",
"how to grow plants indoors",
"difference between hdd and ssd",
"how to fix slow computer",
"most streamed songs",
"how to tie a tie",
"what causes rain",
"best laptops for students",
"how to reset router",
"healthy breakfast ideas",
"how many continents are there",
"latest smartphone features",
"how to meditate beginners",
"what is renewable energy",
"best pc accessories",
"how to clean glasses",
"famous landmarks worldwide",
"how to make coffee at home",
"what is machine learning",
"best programming languages",
"how to backup files",
"how does bluetooth work",
"top video games right now",
"how to improve sleep quality",
"what is cryptocurrency",
"easy lunch ideas",
"how to check internet speed",
"best noise cancelling headphones",
"how to take screenshots on mac",
"what is the milky way",
"how to organize files",
"popular mobile apps",
"how to learn faster",
"how does gps work",
"best free antivirus",
"how to clean a monitor",
"what is an electric car",
"simple math tricks",
"how to update drivers",
"famous scientists",
"how to cook rice",
"what is the tallest mountain",
"best tv shows all time",
"how to improve typing speed",
"how does solar power work",
"easy dessert recipes",
"how to fix bluetooth issues",
"what is the internet",
"best pc keyboards",
"how to stay focused",
"popular science facts",
"how to convert files to pdf",
"how long does it take to sleep",
"best travel tips",
"how to clean headphones",
"what is open source software",
"how to manage time better",
"latest gaming news",
"how to check laptop temperature",
"what is a firewall",
"easy meal prep ideas",
"how to reduce eye strain",
"best budget headphones",
"how does email work",
"what is virtual reality",
"how to compress files",
"popular programming tools",
"how to improve concentration",
"how to make smoothies",
"best desk setup ideas",
"how to block ads",
"what is 5g technology",
"how to clean a mouse",
"famous world wonders",
"how to improve battery life",
"best cloud storage services",
"how to learn a new language",
"what is dark mode",
"how to clear browser cache",
"popular tech podcasts",
"how to stay motivated"
]

View File

@@ -12,6 +12,7 @@ import BrowserUtils from './browser/BrowserUtils'
import { IpcLog, Logger } from './logging/Logger' import { IpcLog, Logger } from './logging/Logger'
import Utils from './util/Utils' import Utils from './util/Utils'
import { loadAccounts, loadConfig } from './util/Load' import { loadAccounts, loadConfig } from './util/Load'
import { checkNodeVersion } from './util/Validator'
import { Login } from './browser/auth/Login' import { Login } from './browser/auth/Login'
import { Workers } from './functions/Workers' import { Workers } from './functions/Workers'
@@ -27,7 +28,7 @@ import type { AppDashboardData } from './interface/AppDashBoardData'
interface ExecutionContext { interface ExecutionContext {
isMobile: boolean isMobile: boolean
accountEmail: string account: Account
} }
interface BrowserSession { interface BrowserSession {
@@ -50,7 +51,7 @@ const executionContext = new AsyncLocalStorage<ExecutionContext>()
export function getCurrentContext(): ExecutionContext { export function getCurrentContext(): ExecutionContext {
const context = executionContext.getStore() const context = executionContext.getStore()
if (!context) { if (!context) {
return { isMobile: false, accountEmail: 'unknown' } return { isMobile: false, account: {} as any }
} }
return context return context
} }
@@ -62,6 +63,7 @@ async function flushAllWebhooks(timeoutMs = 5000): Promise<void> {
interface UserData { interface UserData {
userName: string userName: string
geoLocale: string geoLocale: string
langCode: string
initialPoints: number initialPoints: number
currentPoints: number currentPoints: number
gainedPoints: number gainedPoints: number
@@ -87,6 +89,7 @@ export class MicrosoftRewardsBot {
private pointsCanCollect = 0 private pointsCanCollect = 0
private activeWorkers: number private activeWorkers: number
private exitedWorkers: number[]
private browserFactory: Browser = new Browser(this) private browserFactory: Browser = new Browser(this)
private accounts: Account[] private accounts: Account[]
private workers: Workers private workers: Workers
@@ -98,7 +101,8 @@ export class MicrosoftRewardsBot {
constructor() { constructor() {
this.userData = { this.userData = {
userName: '', userName: '',
geoLocale: '', geoLocale: 'US',
langCode: 'en',
initialPoints: 0, initialPoints: 0,
currentPoints: 0, currentPoints: 0,
gainedPoints: 0 gainedPoints: 0
@@ -115,6 +119,7 @@ export class MicrosoftRewardsBot {
} }
this.config = loadConfig() this.config = loadConfig()
this.activeWorkers = this.config.clusters this.activeWorkers = this.config.clusters
this.exitedWorkers = []
} }
get isMobile(): boolean { get isMobile(): boolean {
@@ -132,7 +137,7 @@ export class MicrosoftRewardsBot {
this.logger.info( this.logger.info(
'main', 'main',
'RUN-START', 'RUN-START',
`Starting Microsoft Rewards bot| v${pkg.version} | Accounts: ${totalAccounts} | Clusters: ${this.config.clusters}` `Starting Microsoft Rewards Script | v${pkg.version} | Accounts: ${totalAccounts} | Clusters: ${this.config.clusters}`
) )
if (this.config.clusters > 1) { if (this.config.clusters > 1) {
@@ -182,7 +187,15 @@ export class MicrosoftRewardsBot {
} }
const onWorkerDone = async (label: 'exit' | 'disconnect', worker: Worker, code?: number): Promise<void> => { const onWorkerDone = async (label: 'exit' | 'disconnect', worker: Worker, code?: number): Promise<void> => {
const { pid } = worker.process
this.activeWorkers -= 1 this.activeWorkers -= 1
if (!pid || this.exitedWorkers.includes(pid)) {
return
} else {
this.exitedWorkers.push(pid)
}
this.logger.warn( this.logger.warn(
'main', 'main',
`CLUSTER-WORKER-${label.toUpperCase()}`, `CLUSTER-WORKER-${label.toUpperCase()}`,
@@ -226,6 +239,8 @@ export class MicrosoftRewardsBot {
if (process.send) { if (process.send) {
process.send({ __stats: stats }) process.send({ __stats: stats })
} }
process.disconnect()
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
'main', 'main',
@@ -347,14 +362,14 @@ export class MicrosoftRewardsBot {
let mobileContextClosed = false let mobileContextClosed = false
try { try {
return await executionContext.run({ isMobile: true, accountEmail }, async () => { return await executionContext.run({ isMobile: true, account }, async () => {
mobileSession = await this.browserFactory.createBrowser(account.proxy, accountEmail) mobileSession = await this.browserFactory.createBrowser(account)
const initialContext: BrowserContext = mobileSession.context const initialContext: BrowserContext = mobileSession.context
this.mainMobilePage = await initialContext.newPage() this.mainMobilePage = await initialContext.newPage()
this.logger.info('main', 'BROWSER', `Mobile Browser started | ${accountEmail}`) this.logger.info('main', 'BROWSER', `Mobile Browser started | ${accountEmail}`)
await this.login.login(this.mainMobilePage, accountEmail, account.password, account.totp) await this.login.login(this.mainMobilePage, account)
try { try {
this.accessToken = await this.login.getAppAccessToken(this.mainMobilePage, accountEmail) this.accessToken = await this.login.getAppAccessToken(this.mainMobilePage, accountEmail)
@@ -402,6 +417,7 @@ export class MicrosoftRewardsBot {
if (this.config.workers.doAppPromotions) await this.workers.doAppPromotions(appData) if (this.config.workers.doAppPromotions) await this.workers.doAppPromotions(appData)
if (this.config.workers.doDailySet) await this.workers.doDailySet(data, this.mainMobilePage) if (this.config.workers.doDailySet) await this.workers.doDailySet(data, this.mainMobilePage)
if (this.config.workers.doSpecialPromotions) await this.workers.doSpecialPromotions(data)
if (this.config.workers.doMorePromotions) await this.workers.doMorePromotions(data, this.mainMobilePage) if (this.config.workers.doMorePromotions) await this.workers.doMorePromotions(data, this.mainMobilePage)
if (this.config.workers.doDailyCheckIn) await this.activities.doDailyCheckIn() if (this.config.workers.doDailyCheckIn) await this.activities.doDailyCheckIn()
if (this.config.workers.doReadToEarn) await this.activities.doReadToEarn() if (this.config.workers.doReadToEarn) await this.activities.doReadToEarn()
@@ -440,7 +456,7 @@ export class MicrosoftRewardsBot {
} finally { } finally {
if (mobileSession && !mobileContextClosed) { if (mobileSession && !mobileContextClosed) {
try { try {
await executionContext.run({ isMobile: true, accountEmail }, async () => { await executionContext.run({ isMobile: true, account }, async () => {
await this.browser.func.closeBrowser(mobileSession!.context, accountEmail) await this.browser.func.closeBrowser(mobileSession!.context, accountEmail)
}) })
} catch {} } catch {}
@@ -452,6 +468,8 @@ export class MicrosoftRewardsBot {
export { executionContext } export { executionContext }
async function main(): Promise<void> { async function main(): Promise<void> {
// Check before doing anything
checkNodeVersion()
const rewardsBot = new MicrosoftRewardsBot() const rewardsBot = new MicrosoftRewardsBot()
process.on('beforeExit', () => { process.on('beforeExit', () => {

View File

@@ -1,9 +1,12 @@
export interface Account { export interface Account {
email: string email: string
password: string password: string
totp?: string totpSecret?: string
recoveryEmail: string
geoLocale: 'auto' | string geoLocale: 'auto' | string
langCode: 'en' | string
proxy: AccountProxy proxy: AccountProxy
saveFingerprint: ConfigSaveFingerprint
} }
export interface AccountProxy { export interface AccountProxy {
@@ -13,3 +16,8 @@ export interface AccountProxy {
password: string password: string
username: string username: string
} }
export interface ConfigSaveFingerprint {
mobile: boolean
desktop: boolean
}

View File

@@ -5,7 +5,6 @@ export interface Config {
runOnZeroPoints: boolean runOnZeroPoints: boolean
clusters: number clusters: number
errorDiagnostics: boolean errorDiagnostics: boolean
saveFingerprint: ConfigSaveFingerprint
workers: ConfigWorkers workers: ConfigWorkers
searchOnBingLocalQueries: boolean searchOnBingLocalQueries: boolean
globalTimeout: number | string globalTimeout: number | string
@@ -16,15 +15,13 @@ export interface Config {
webhook: ConfigWebhook webhook: ConfigWebhook
} }
export interface ConfigSaveFingerprint { export type QueryEngine = 'google' | 'wikipedia' | 'reddit' | 'local'
mobile: boolean
desktop: boolean
}
export interface ConfigSearchSettings { export interface ConfigSearchSettings {
scrollRandomResults: boolean scrollRandomResults: boolean
clickRandomResults: boolean clickRandomResults: boolean
parallelSearching: boolean parallelSearching: boolean
queryEngines: QueryEngine[]
searchResultVisitTime: number | string searchResultVisitTime: number | string
searchDelay: ConfigDelay searchDelay: ConfigDelay
readDelay: ConfigDelay readDelay: ConfigDelay
@@ -41,6 +38,7 @@ export interface ConfigProxy {
export interface ConfigWorkers { export interface ConfigWorkers {
doDailySet: boolean doDailySet: boolean
doSpecialPromotions: boolean
doMorePromotions: boolean doMorePromotions: boolean
doPunchCards: boolean doPunchCards: boolean
doAppPromotions: boolean doAppPromotions: boolean

View File

@@ -94,3 +94,23 @@ export interface BingTrendingImage {
export interface BingTrendingQuery { export interface BingTrendingQuery {
text: string text: string
} }
export interface WikipediaTopResponse {
items: Array<{
articles: Array<{
article: string
views: number
}>
}>
}
export interface RedditListing {
data: {
children: Array<{
data: {
title: string
over_18: boolean
}
}>
}
}

View File

@@ -73,10 +73,10 @@ export class Logger {
const now = new Date().toLocaleString() const now = new Date().toLocaleString()
const formatted = formatMessage(message) const formatted = formatMessage(message)
const userName = this.bot.userData.userName ? this.bot.userData.userName : 'MAIN'
const levelTag = level.toUpperCase() const levelTag = level.toUpperCase()
const cleanMsg = `[${now}] [${this.bot.userData.userName}] [${levelTag}] ${platformText( const cleanMsg = `[${now}] [${userName}] [${levelTag}] ${platformText(isMobile)} [${title}] ${formatted}`
isMobile
)} [${title}] ${formatted}`
const config = this.bot.config const config = this.bot.config
@@ -85,7 +85,7 @@ export class Logger {
} }
const badge = platformBadge(isMobile) const badge = platformBadge(isMobile)
const consoleStr = `[${now}] [${this.bot.userData.userName}] [${levelTag}] ${badge} [${title}] ${formatted}` const consoleStr = `[${now}] [${userName}] [${levelTag}] ${badge} [${title}] ${formatted}`
let logColor: ColorKey | undefined = color let logColor: ColorKey | undefined = color

View File

@@ -2,6 +2,7 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axiosRetry from 'axios-retry' import axiosRetry from 'axios-retry'
import { HttpProxyAgent } from 'http-proxy-agent' import { HttpProxyAgent } from 'http-proxy-agent'
import { HttpsProxyAgent } from 'https-proxy-agent' import { HttpsProxyAgent } from 'https-proxy-agent'
import { SocksProxyAgent } from 'socks-proxy-agent'
import { URL } from 'url' import { URL } from 'url'
import type { AccountProxy } from '../interface/Account' import type { AccountProxy } from '../interface/Account'
@@ -36,7 +37,9 @@ class AxiosClient {
}) })
} }
private getAgentForProxy(proxyConfig: AccountProxy): HttpProxyAgent<string> | HttpsProxyAgent<string> { private getAgentForProxy(
proxyConfig: AccountProxy
): HttpProxyAgent<string> | HttpsProxyAgent<string> | SocksProxyAgent {
const { url: baseUrl, port, username, password } = proxyConfig const { url: baseUrl, port, username, password } = proxyConfig
let urlObj: URL let urlObj: URL
@@ -67,8 +70,11 @@ class AxiosClient {
return new HttpProxyAgent(proxyUrl) return new HttpProxyAgent(proxyUrl)
case 'https:': case 'https:':
return new HttpsProxyAgent(proxyUrl) return new HttpsProxyAgent(proxyUrl)
case 'socks4:':
case 'socks5:':
return new SocksProxyAgent(proxyUrl)
default: default:
throw new Error(`Unsupported proxy protocol: ${protocol}. Only HTTP(S) is supported!`) throw new Error(`Unsupported proxy protocol: ${protocol}. Only HTTP(S) and SOCKS4/5 are supported!`)
} }
} }

View File

@@ -3,8 +3,9 @@ import type { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import type { Account } from '../interface/Account' import type { Account, ConfigSaveFingerprint } from '../interface/Account'
import type { Config, ConfigSaveFingerprint } from '../interface/Config' import type { Config } from '../interface/Config'
import { validateAccounts, validateConfig } from './Validator'
let configCache: Config let configCache: Config
@@ -18,8 +19,11 @@ export function loadAccounts(): Account[] {
const accountDir = path.join(__dirname, '../', file) const accountDir = path.join(__dirname, '../', file)
const accounts = fs.readFileSync(accountDir, 'utf-8') const accounts = fs.readFileSync(accountDir, 'utf-8')
const accountsData = JSON.parse(accounts)
return JSON.parse(accounts) validateAccounts(accountsData)
return accountsData
} catch (error) { } catch (error) {
throw new Error(error as string) throw new Error(error as string)
} }
@@ -35,6 +39,8 @@ export function loadConfig(): Config {
const config = fs.readFileSync(configDir, 'utf-8') const config = fs.readFileSync(configDir, 'utf-8')
const configData = JSON.parse(config) const configData = JSON.parse(config)
validateConfig(configData)
configCache = configData configCache = configData
return configData return configData

View File

@@ -21,10 +21,19 @@ export default class Util {
} }
shuffleArray<T>(array: T[]): T[] { shuffleArray<T>(array: T[]): T[] {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const a = array[i]
const b = array[j]
if (a === undefined || b === undefined) continue
array[i] = b
array[j] = a
}
return array return array
.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
} }
randomNumber(min: number, max: number): number { randomNumber(min: number, max: number): number {

131
src/util/Validator.ts Normal file
View File

@@ -0,0 +1,131 @@
import { z } from 'zod'
import semver from 'semver'
import pkg from '../../package.json'
import { Config } from '../interface/Config'
import { Account } from '../interface/Account'
const NumberOrString = z.union([z.number(), z.string()])
const LogFilterSchema = z.object({
enabled: z.boolean(),
mode: z.enum(['whitelist', 'blacklist']),
levels: z.array(z.enum(['debug', 'info', 'warn', 'error'])).optional(),
keywords: z.array(z.string()).optional(),
regexPatterns: z.array(z.string()).optional()
})
const DelaySchema = z.object({
min: NumberOrString,
max: NumberOrString
})
const QueryEngineSchema = z.enum(['google', 'wikipedia', 'reddit', 'local'])
// Webhook
const WebhookSchema = z.object({
discord: z
.object({
enabled: z.boolean(),
url: z.string()
})
.optional(),
ntfy: z
.object({
enabled: z.boolean().optional(),
url: z.string(),
topic: z.string().optional(),
token: z.string().optional(),
title: z.string().optional(),
tags: z.array(z.string()).optional(),
priority: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)]).optional()
})
.optional(),
webhookLogFilter: LogFilterSchema
})
// Config
export const ConfigSchema = z.object({
baseURL: z.string(),
sessionPath: z.string(),
headless: z.boolean(),
runOnZeroPoints: z.boolean(),
clusters: z.number().int().nonnegative(),
errorDiagnostics: z.boolean(),
workers: z.object({
doDailySet: z.boolean(),
doSpecialPromotions: z.boolean(),
doMorePromotions: z.boolean(),
doPunchCards: z.boolean(),
doAppPromotions: z.boolean(),
doDesktopSearch: z.boolean(),
doMobileSearch: z.boolean(),
doDailyCheckIn: z.boolean(),
doReadToEarn: z.boolean()
}),
searchOnBingLocalQueries: z.boolean(),
globalTimeout: NumberOrString,
searchSettings: z.object({
scrollRandomResults: z.boolean(),
clickRandomResults: z.boolean(),
parallelSearching: z.boolean(),
queryEngines: z.array(QueryEngineSchema),
searchResultVisitTime: NumberOrString,
searchDelay: DelaySchema,
readDelay: DelaySchema
}),
debugLogs: z.boolean(),
proxy: z.object({
queryEngine: z.boolean()
}),
consoleLogFilter: LogFilterSchema,
webhook: WebhookSchema
})
// Account
export const AccountSchema = z.object({
email: z.string(),
password: z.string(),
totpSecret: z.string().optional(),
recoveryEmail: z.string(),
geoLocale: z.string(),
langCode: z.string(),
proxy: z.object({
proxyAxios: z.boolean(),
url: z.string(),
port: z.number(),
password: z.string(),
username: z.string()
}),
saveFingerprint: z.object({
mobile: z.boolean(),
desktop: z.boolean()
})
})
export function validateConfig(data: unknown): Config {
return ConfigSchema.parse(data) as Config
}
export function validateAccounts(data: unknown): Account[] {
return z.array(AccountSchema).parse(data)
}
export function checkNodeVersion(): void {
try {
const requiredVersion = pkg.engines?.node
if (!requiredVersion) {
console.warn('No Node.js version requirement found in package.json "engines" field.')
return
}
if (!semver.satisfies(process.version, requiredVersion)) {
console.error(`Current Node.js version ${process.version} does not satisfy requirement: ${requiredVersion}`)
process.exit(1)
}
} catch (error) {
console.error('Failed to validate Node.js version:', error)
process.exit(1)
}
}

View File

@@ -40,8 +40,12 @@
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"types": ["node"], "types": [
"typeRoots": ["./node_modules/@types"], "node"
],
"typeRoots": [
"./node_modules/@types"
],
// Keep explicit typeRoots to ensure resolution in environments that don't auto-detect before full install. // Keep explicit typeRoots to ensure resolution in environments that don't auto-detect before full install.
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
@@ -65,6 +69,14 @@
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": ["src/**/*.ts", "src/accounts.json", "src/config.json", "src/functions/queries.json"], "include": [
"exclude": ["node_modules"] "src/**/*.ts",
"src/accounts.json",
"src/config.json",
"src/functions/bing-search-activity-queries.json",
"src/functions/search-queries.json"
],
"exclude": [
"node_modules"
]
} }