v3.1.0 initial

This commit is contained in:
TheNetsky
2026-01-05 16:26:47 +01:00
parent a8ddb65b21
commit 576899f39d
37 changed files with 3391 additions and 865 deletions

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

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

419
package-lock.json generated
View File

@@ -7,14 +7,14 @@
"": {
"name": "microsoft-rewards-script",
"version": "3.0.0",
"license": "ISC",
"license": "GPL-3.0-or-later",
"dependencies": {
"axios": "^1.13.2",
"axios-retry": "^4.5.0",
"chalk": "^4.1.2",
"cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.77",
"fingerprint-injector": "^2.1.77",
"fingerprint-generator": "^2.1.79",
"fingerprint-injector": "^2.1.79",
"ghost-cursor-playwright-port": "^1.4.3",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
@@ -22,11 +22,15 @@
"otpauth": "^9.4.1",
"p-queue": "^9.0.1",
"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": {
"@types/ms": "^2.1.0",
"@types/node": "^24.10.1",
"@types/semver": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^8.48.0",
"eslint": "^9.39.1",
"eslint-plugin-modules-newline": "^0.0.6",
@@ -35,7 +39,7 @@
"typescript": "^5.9.3"
},
"engines": {
"node": ">=18.0.0"
"node": ">=22.0.0"
}
},
"node_modules/@cspotcode/source-map-support": {
@@ -51,9 +55,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -145,9 +149,9 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -157,7 +161,7 @@
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
@@ -179,6 +183,16 @@
"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": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -193,9 +207,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -342,13 +356,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"playwright": "1.56.1"
"playwright": "1.57.0"
},
"bin": {
"playwright": "cli.js"
@@ -357,25 +371,6 @@
"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": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -389,9 +384,9 @@
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
@@ -440,30 +435,36 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"version": "24.10.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
"license": "MIT",
"dependencies": {
"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": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
"integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz",
"integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.48.0",
"@typescript-eslint/type-utils": "8.48.0",
"@typescript-eslint/utils": "8.48.0",
"@typescript-eslint/visitor-keys": "8.48.0",
"graphemer": "^1.4.0",
"@typescript-eslint/scope-manager": "8.51.0",
"@typescript-eslint/type-utils": "8.51.0",
"@typescript-eslint/utils": "8.51.0",
"@typescript-eslint/visitor-keys": "8.51.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
"ts-api-utils": "^2.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -473,33 +474,23 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.48.0",
"@typescript-eslint/parser": "^8.51.0",
"eslint": "^8.57.0 || ^9.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": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz",
"integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz",
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.48.0",
"@typescript-eslint/types": "8.48.0",
"@typescript-eslint/typescript-estree": "8.48.0",
"@typescript-eslint/visitor-keys": "8.48.0",
"@typescript-eslint/scope-manager": "8.51.0",
"@typescript-eslint/types": "8.51.0",
"@typescript-eslint/typescript-estree": "8.51.0",
"@typescript-eslint/visitor-keys": "8.51.0",
"debug": "^4.3.4"
},
"engines": {
@@ -515,14 +506,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz",
"integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz",
"integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.48.0",
"@typescript-eslint/types": "^8.48.0",
"@typescript-eslint/tsconfig-utils": "^8.51.0",
"@typescript-eslint/types": "^8.51.0",
"debug": "^4.3.4"
},
"engines": {
@@ -537,14 +528,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz",
"integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz",
"integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.48.0",
"@typescript-eslint/visitor-keys": "8.48.0"
"@typescript-eslint/types": "8.51.0",
"@typescript-eslint/visitor-keys": "8.51.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -555,9 +546,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz",
"integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz",
"integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -572,17 +563,17 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz",
"integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz",
"integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.48.0",
"@typescript-eslint/typescript-estree": "8.48.0",
"@typescript-eslint/utils": "8.48.0",
"@typescript-eslint/types": "8.51.0",
"@typescript-eslint/typescript-estree": "8.51.0",
"@typescript-eslint/utils": "8.51.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
"ts-api-utils": "^2.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -597,9 +588,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz",
"integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz",
"integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==",
"dev": true,
"license": "MIT",
"engines": {
@@ -611,21 +602,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz",
"integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz",
"integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.48.0",
"@typescript-eslint/tsconfig-utils": "8.48.0",
"@typescript-eslint/types": "8.48.0",
"@typescript-eslint/visitor-keys": "8.48.0",
"@typescript-eslint/project-service": "8.51.0",
"@typescript-eslint/tsconfig-utils": "8.51.0",
"@typescript-eslint/types": "8.51.0",
"@typescript-eslint/visitor-keys": "8.51.0",
"debug": "^4.3.4",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.1.0"
"ts-api-utils": "^2.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -639,16 +630,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz",
"integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz",
"integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.48.0",
"@typescript-eslint/types": "8.48.0",
"@typescript-eslint/typescript-estree": "8.48.0"
"@typescript-eslint/scope-manager": "8.51.0",
"@typescript-eslint/types": "8.51.0",
"@typescript-eslint/typescript-estree": "8.51.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -663,13 +654,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz",
"integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz",
"integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.48.0",
"@typescript-eslint/types": "8.51.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -827,9 +818,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.31",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
"integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
"version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -862,9 +853,9 @@
}
},
"node_modules/browserslist": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
"integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"funding": [
{
"type": "opencollective",
@@ -881,11 +872,11 @@
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
"electron-to-chromium": "^1.5.249",
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.1.4"
"update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
@@ -917,9 +908,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001757",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
"version": "1.0.30001762",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
"funding": [
{
"type": "opencollective",
@@ -1207,9 +1198,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.262",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz",
"integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
"version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
"license": "ISC"
},
"node_modules/encoding-sniffer": {
@@ -1305,9 +1296,9 @@
}
},
"node_modules/eslint": {
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1317,7 +1308,7 @@
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.39.1",
"@eslint/js": "9.39.2",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -1431,6 +1422,16 @@
"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": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -1476,9 +1477,9 @@
}
},
"node_modules/esquery": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -1597,13 +1598,13 @@
}
},
"node_modules/fingerprint-generator": {
"version": "2.1.77",
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.77.tgz",
"integrity": "sha512-wR15VUEZnwozFiSDRV+40zxlEt3ZV3JNYvLx0CSF9D9smov4pUC6MJZJnlxtDr+Ir4oppU8vn1JXApLk/Qr5Uw==",
"version": "2.1.79",
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.79.tgz",
"integrity": "sha512-0dr3kTgvRYHleRPp6OBDcPb8amJmOyFr9aOuwnpN6ooWJ5XyT+/aL/SZ6CU4ZrEtzV26EyJ2Lg7PT32a0NdrRA==",
"license": "Apache-2.0",
"dependencies": {
"generative-bayesian-network": "^2.1.77",
"header-generator": "^2.1.77",
"generative-bayesian-network": "^2.1.79",
"header-generator": "^2.1.79",
"tslib": "^2.4.0"
},
"engines": {
@@ -1611,12 +1612,12 @@
}
},
"node_modules/fingerprint-injector": {
"version": "2.1.77",
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.77.tgz",
"integrity": "sha512-R778SIyrqgWO0P+UWKzIFWUWZz13EGu6UmV7CX3vuFDbsYIL1xiH+s+/nzPSOqFdhXyLo7d8aTOjbGbRLULoQQ==",
"version": "2.1.79",
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.79.tgz",
"integrity": "sha512-0tKb3wCQ92ZlLLbxRfhoduCI+rIgpsUdb3Jp66TrwoMlSeVDlmoBZfkQW4FheQFau1kg0lIg4Zk4xetBubetQQ==",
"license": "Apache-2.0",
"dependencies": {
"fingerprint-generator": "^2.1.77",
"fingerprint-generator": "^2.1.79",
"tslib": "^2.4.0"
},
"engines": {
@@ -1677,9 +1678,9 @@
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -1716,9 +1717,9 @@
}
},
"node_modules/generative-bayesian-network": {
"version": "2.1.77",
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.77.tgz",
"integrity": "sha512-viU4CRPsmgiklR94LhvdMndaY73BkCH1pGjmOjWbLR/ZwcUd06gKF3TCcsS3npRl74o33YSInSixxm16wIukcA==",
"version": "2.1.79",
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.79.tgz",
"integrity": "sha512-aPH+V2wO+HE0BUX1LbsM8Ak99gmV43lgh+D7GDteM0zgnPqiAwcK9JZPxMPZa3aJUleFtFaL1lAei8g9zNrDIA==",
"license": "Apache-2.0",
"dependencies": {
"adm-zip": "^0.5.9",
@@ -1850,13 +1851,6 @@
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -1906,13 +1900,13 @@
}
},
"node_modules/header-generator": {
"version": "2.1.77",
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.77.tgz",
"integrity": "sha512-ggSG/mfkFMu8CO7xP591G8kp1IJCBvgXu7M8oxTjC9u914JsIzE6zIfoFsXzA+pf0utWJhUsdqU0oV/DtQ4DFQ==",
"version": "2.1.79",
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.79.tgz",
"integrity": "sha512-YvHx8teq4QmV5mz7wdPMsj9n1OZBPnZxA4QE+EOrtx7xbmGvd1gBvDNKCb5XqS4GR/TL75MU5hqMqqqANdILRg==",
"license": "Apache-2.0",
"dependencies": {
"browserslist": "^4.21.1",
"generative-bayesian-network": "^2.1.77",
"generative-bayesian-network": "^2.1.79",
"ow": "^0.28.1",
"tslib": "^2.4.0"
},
@@ -1990,9 +1984,9 @@
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2026,6 +2020,15 @@
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2166,11 +2169,11 @@
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
"integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
"version": "11.2.4",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
@@ -2552,19 +2555,6 @@
}
},
"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",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
@@ -2588,9 +2578,9 @@
}
},
"node_modules/prettier": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.1.tgz",
"integrity": "sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==",
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -2669,7 +2659,6 @@
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -2701,6 +2690,44 @@
"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": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -2744,9 +2771,9 @@
}
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2847,9 +2874,9 @@
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"funding": [
{
"type": "opencollective",
@@ -2905,6 +2932,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
@@ -2969,6 +2997,15 @@
"funding": {
"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",
"version": "3.0.1",
"version": "3.0.2",
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
"author": "Netsky",
"license": "GPL-3.0-or-later",
"main": "dist/index.js",
"engines": {
"node": ">=18.0.0"
"node": ">=22.0.0"
},
"scripts": {
"pre-build": "npm i && rimraf dist && npx patchright install chromium",
@@ -18,8 +17,10 @@
"create-docker": "docker build -t microsoft-rewards-script-docker .",
"format": "prettier --write .",
"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": [
"Bing Rewards",
@@ -33,6 +34,7 @@
"devDependencies": {
"@types/ms": "^2.1.0",
"@types/node": "^24.10.1",
"@types/semver": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^8.48.0",
"eslint": "^9.39.1",
"eslint-plugin-modules-newline": "^0.0.6",
@@ -45,8 +47,8 @@
"axios-retry": "^4.5.0",
"chalk": "^4.1.2",
"cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.77",
"fingerprint-injector": "^2.1.77",
"fingerprint-generator": "^2.1.79",
"fingerprint-injector": "^2.1.79",
"ghost-cursor-playwright-port": "^1.4.3",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
@@ -54,6 +56,9 @@
"otpauth": "^9.4.1",
"p-queue": "^9.0.1",
"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",
"password": "password_1",
"totp": "",
"totpSecret": "",
"recoveryEmail": "",
"geoLocale": "auto",
"langCode": "en",
"proxy": {
"proxyAxios": true,
"proxyAxios": false,
"url": "",
"port": 0,
"username": "",
"password": ""
},
"saveFingerprint": {
"mobile": false,
"desktop": false
}
},
{
"email": "email_2",
"password": "password_2",
"totp": "",
"totpSecret": "",
"recoveryEmail": "",
"geoLocale": "auto",
"langCode": "en",
"proxy": {
"proxyAxios": true,
"proxyAxios": false,
"url": "",
"port": 0,
"username": "",
"password": ""
},
"saveFingerprint": {
"mobile": false,
"desktop": false
}
}
]
]

View File

@@ -1,5 +1,4 @@
import rebrowser, { BrowserContext } from 'patchright'
import { newInjectedContext } from 'fingerprint-injector'
import { BrowserFingerprintWithHeaders, FingerprintGenerator } from 'fingerprint-generator'
@@ -7,7 +6,7 @@ import type { MicrosoftRewardsBot } from '../index'
import { loadSessionData, saveFingerprintData } from '../util/Load'
import { UserAgentManager } from './UserAgent'
import type { AccountProxy } from '../interface/Account'
import type { Account, AccountProxy } from '../interface/Account'
/* Test Stuff
https://abrahamjuliot.github.io/creepjs/
@@ -17,92 +16,110 @@ https://pixelscan.net/
https://www.browserscan.net/
*/
interface BrowserCreationResult {
context: BrowserContext
fingerprint: BrowserFingerprintWithHeaders
}
class Browser {
private bot: MicrosoftRewardsBot
private readonly bot: MicrosoftRewardsBot
private static readonly BROWSER_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'
] as const
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
async createBrowser(
proxy: AccountProxy,
email: string
): Promise<{
context: BrowserContext
fingerprint: BrowserFingerprintWithHeaders
}> {
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,
...(proxy.url && {
proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` }
}),
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'
]
...(proxyConfig && { proxy: proxyConfig }),
args: [...Browser.BROWSER_ARGS]
})
} catch (error) {
this.bot.logger.error(
this.bot.isMobile,
'BROWSER',
`Launch failed: ${error instanceof Error ? error.message : String(error)}`
)
const errorMessage = error instanceof Error ? error.message : String(error)
this.bot.logger.error(this.bot.isMobile, 'BROWSER', `Launch failed: ${errorMessage}`)
throw error
}
const sessionData = await loadSessionData(
this.bot.config.sessionPath,
email,
this.bot.config.saveFingerprint,
this.bot.isMobile
)
try {
const sessionData = await loadSessionData(
this.bot.config.sessionPath,
account.email,
account.saveFingerprint,
this.bot.isMobile
)
const fingerprint = sessionData.fingerprint
? sessionData.fingerprint
: await this.generateFingerprint(this.bot.isMobile)
const fingerprint = 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(() => {
Object.defineProperty(navigator, 'credentials', {
value: {
create: () => Promise.reject(new Error('WebAuthn disabled')),
get: () => Promise.reject(new Error('WebAuthn disabled'))
}
await context.addInitScript(() => {
Object.defineProperty(navigator, 'credentials', {
value: {
create: () => Promise.reject(new Error('WebAuthn disabled')),
get: () => Promise.reject(new Error('WebAuthn disabled'))
}
})
})
})
context.setDefaultTimeout(this.bot.utils.stringToNumber(this.bot.config?.globalTimeout ?? 30000))
context.setDefaultTimeout(this.bot.utils.stringToNumber(this.bot.config?.globalTimeout ?? 30000))
await context.addCookies(sessionData.cookies)
await context.addCookies(sessionData.cookies)
if (this.bot.config.saveFingerprint) {
await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerprint)
if (
(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.isMobile,
'BROWSER',
`Created browser with User-Agent: "${fingerprint.fingerprint.navigator.userAgent}"`
)
this.bot.logger.debug(this.bot.isMobile, 'BROWSER-FINGERPRINT', JSON.stringify(fingerprint))
return { context: context as unknown as BrowserContext, fingerprint }
} catch (error) {
await browser.close().catch(() => {})
throw error
}
}
this.bot.logger.info(
this.bot.isMobile,
'BROWSER',
`Created browser with User-Agent: "${fingerprint.fingerprint.navigator.userAgent}"`
)
this.bot.logger.debug(this.bot.isMobile, 'BROWSER-FINGERPRINT', JSON.stringify(fingerprint))
return {
context: context as unknown as BrowserContext,
fingerprint: fingerprint
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

@@ -28,7 +28,7 @@ export default class BrowserFunc {
.join('; ')
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',
headers: {
...(this.bot.fingerprint?.headers ?? {}),

View File

@@ -2,25 +2,31 @@ import type { Page } from 'patchright'
import type { MicrosoftRewardsBot } from '../../index'
import { saveSessionData } from '../../util/Load'
// Methods
import { MobileAccessLogin } from './methods/MobileAccessLogin'
import { EmailLogin } from './methods/EmailLogin'
import { PasswordlessLogin } from './methods/PasswordlessLogin'
import { TotpLogin } from './methods/Totp2FALogin'
import { CodeLogin } from './methods/GetACodeLogin'
import { RecoveryLogin } from './methods/RecoveryEmailLogin'
import type { Account } from '../../interface/Account'
type LoginState =
| 'EMAIL_INPUT'
| 'PASSWORD_INPUT'
| 'SIGN_IN_ANOTHER_WAY'
| 'SIGN_IN_ANOTHER_WAY_EMAIL'
| 'PASSKEY_ERROR'
| 'PASSKEY_VIDEO'
| 'KMSI_PROMPT'
| 'LOGGED_IN'
| 'RECOVERY_EMAIL_INPUT'
| 'ACCOUNT_LOCKED'
| 'ERROR_ALERT'
| '2FA_TOTP'
| 'LOGIN_PASSWORDLESS'
| 'GET_A_CODE'
| 'GET_A_CODE_2'
| 'UNKNOWN'
| 'CHROMEWEBDATA_ERROR'
@@ -28,28 +34,52 @@ export class Login {
emailLogin: EmailLogin
passwordlessLogin: PasswordlessLogin
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"] span[role="button"]',
bingProfile: '#id_n',
requestToken: 'input[name="__RequestVerificationToken"]',
requestTokenMeta: 'meta[name="__RequestVerificationToken"]'
} as const
constructor(private bot: MicrosoftRewardsBot) {
this.emailLogin = new EmailLogin(this.bot)
this.passwordlessLogin = new PasswordlessLogin(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"]'
private readonly secondaryButtonSelector = 'button[data-testid="secondaryButton"]'
async login(page: Page, email: string, password: string, totpSecret?: string) {
async login(page: Page, account: Account) {
try {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting login process')
await page.goto('https://www.bing.com/rewards/dashboard', { waitUntil: 'domcontentloaded' }).catch(() => {})
await this.bot.utils.wait(2000)
await this.bot.browser.utils.reloadBadPage(page)
await this.bot.browser.utils.disableFido(page)
const maxIterations = 25
let iteration = 0
let previousState: LoginState = 'UNKNOWN'
let sameStateCount = 0
@@ -59,7 +89,7 @@ export class Login {
iteration++
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}`)
if (state !== previousState && previousState !== 'UNKNOWN') {
@@ -68,11 +98,16 @@ export class Login {
if (state === previousState && state !== 'LOGGED_IN' && state !== 'UNKNOWN') {
sameStateCount++
this.bot.logger.debug(
this.bot.isMobile,
'LOGIN',
`Same state count: ${sameStateCount}/4 for state "${state}"`
)
if (sameStateCount >= 4) {
this.bot.logger.warn(
this.bot.isMobile,
'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 this.bot.utils.wait(3000)
@@ -90,8 +125,7 @@ export class Login {
break
}
const shouldContinue = await this.handleState(state, page, email, password, totpSecret)
const shouldContinue = await this.handleState(state, page, account)
if (!shouldContinue) {
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')
}
await this.finalizeLogin(page, email)
await this.finalizeLogin(page, account.email)
} catch (error) {
this.bot.logger.error(
this.bot.isMobile,
'LOGIN',
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
`Fatal error: ${error instanceof Error ? error.message : String(error)}`
)
throw error
}
}
private async detectCurrentState(page: Page): Promise<LoginState> {
// Make sure we settled before getting a URL
private async detectCurrentState(page: Page, account?: Account): Promise<LoginState> {
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
const url = new URL(page.url())
this.bot.logger.debug(this.bot.isMobile, 'DETECT-CURRENT-STATE', `Current URL: ${url}`)
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Current URL: ${url.hostname}${url.pathname}`)
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'
}
const isLocked = await page
.waitForSelector('#serviceAbuseLandingTitle', { state: 'visible', timeout: 200 })
.then(() => true)
.catch(() => false)
const isLocked = await this.checkSelector(page, this.selectors.accountLocked)
if (isLocked) {
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'Account locked selector found')
return 'ACCOUNT_LOCKED'
}
// If instantly loading rewards dash, logged in
if (url.hostname === 'rewards.bing.com') {
if (url.hostname === 'rewards.bing.com' || url.hostname === 'account.microsoft.com') {
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'On rewards/account page, assuming logged in')
return 'LOGGED_IN'
}
// If account dash, logged in
if (url.hostname === 'account.microsoft.com') {
return 'LOGGED_IN'
const stateChecks: Array<[string, LoginState]> = [
[this.selectors.errorAlert, 'ERROR_ALERT'],
[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> => {
return page
.waitForSelector(selector, { state: 'visible', timeout: 200 })
.then(visible => (visible ? state : null))
.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')
const [identityBanner, primaryButton, passwordEntry] = await Promise.all([
this.checkSelector(page, this.selectors.identityBanner),
this.checkSelector(page, this.selectors.primaryButton),
this.checkSelector(page, this.selectors.passwordEntry)
])
// 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')) {
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)
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')) {
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') {
// Remove ERROR_ALERT if not on login.live.com
foundStates = foundStates.filter(s => s !== 'ERROR_ALERT')
}
if (foundStates.includes('2FA_TOTP')) {
// Don't throw on TOTP if expired code is entered
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 priorities: LoginState[] = [
'ACCOUNT_LOCKED',
'PASSKEY_VIDEO',
'PASSKEY_ERROR',
'KMSI_PROMPT',
'PASSWORD_INPUT',
'EMAIL_INPUT',
'SIGN_IN_ANOTHER_WAY_EMAIL',
'SIGN_IN_ANOTHER_WAY',
'LOGIN_PASSWORDLESS',
'2FA_TOTP'
]
const mainState = foundStates[0] as LoginState
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
}
}
return mainState
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Returning first found state: ${foundStates[0]}`)
return foundStates[0] as LoginState
}
private async handleState(
state: LoginState,
page: Page,
email: string,
password: string,
totpSecret?: string
): Promise<boolean> {
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) {
case 'ACCOUNT_LOCKED': {
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)
}
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')
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':
@@ -246,96 +289,161 @@ export class Login {
case 'EMAIL_INPUT': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering email')
await this.emailLogin.enterEmail(page, email)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
await this.emailLogin.enterEmail(page, account.email)
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
}
case 'PASSWORD_INPUT': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering password')
await this.emailLogin.enterPassword(page, password)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
await this.emailLogin.enterPassword(page, account.password)
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
}
case 'GET_A_CODE': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code"')
// Select sign in other way
await this.bot.browser.utils.ghostClick(page, '[data-testid="viewFooter"] span[role="button"]')
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code" via footer')
await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
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
}
case 'CHROMEWEBDATA_ERROR': {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN',
'chromewebdata error page detected, attempting to recover to Rewards home'
)
// Try go to Rewards dashboard
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'chromewebdata error detected, attempting recovery')
try {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', `Navigating to ${this.bot.config.baseURL}`)
await page
.goto(this.bot.config.baseURL, {
waitUntil: 'domcontentloaded',
timeout: 10000
})
.catch(() => {})
await this.bot.utils.wait(3000)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery navigation successful')
return true
} catch {
// If even that fails, fall back to login.live.com
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN',
'Failed to navigate to baseURL from chromewebdata, retrying login.live.com'
)
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Fallback to login.live.com')
await page
.goto('https://login.live.com/', {
waitUntil: 'domcontentloaded',
timeout: 10000
})
.catch(() => {})
await this.bot.utils.wait(3000)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Fallback navigation successful')
return true
}
}
case '2FA_TOTP': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA required')
await this.totp2FALogin.handle(page, totpSecret)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA authentication required')
await this.totp2FALogin.handle(page, account.totpSecret)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA handler completed successfully')
return true
}
case 'SIGN_IN_ANOTHER_WAY': {
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, passwordOption)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
await this.bot.browser.utils.ghostClick(page, this.selectors.passwordIcon)
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
}
case 'KMSI_PROMPT': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Accepting KMSI prompt')
await this.bot.browser.utils.ghostClick(page, this.primaryButtonSelector)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
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 KMSI acceptance')
})
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'KMSI prompt accepted')
return true
}
case 'PASSKEY_VIDEO':
case 'PASSKEY_ERROR': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Skipping Passkey prompt')
await this.bot.browser.utils.ghostClick(page, this.secondaryButtonSelector)
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
await this.bot.browser.utils.ghostClick(page, this.selectors.secondaryButton)
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
}
case 'LOGIN_PASSWORDLESS': {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Handling passwordless authentication')
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
}
@@ -344,12 +452,13 @@ export class Login {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN',
`Unknown state at host:${url.hostname} path:${url.pathname}. Waiting...`
`Unknown state at ${url.hostname}${url.pathname}, waiting`
)
return true
}
default:
this.bot.logger.debug(this.bot.isMobile, 'HANDLE-STATE', `Unhandled state: ${state}, continuing`)
return true
}
}
@@ -363,21 +472,21 @@ export class Login {
if (loginRewardsSuccess) {
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Logged into Microsoft Rewards successfully')
} else {
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN',
'Could not verify Rewards Dashboard. Assuming login valid anyway.'
)
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Could not verify Rewards Dashboard, assuming login valid')
}
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting Bing session verification')
await this.verifyBingSession(page)
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting rewards session verification')
await this.getRewardsSession(page)
const browser = page.context()
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)
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) {
@@ -393,30 +502,34 @@ export class Login {
for (let i = 0; i < loopMax; i++) {
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)
if (state === 'PASSKEY_ERROR') {
this.bot.logger.debug(
this.bot.isMobile,
'LOGIN-BING',
'Verification landed on Passkey error state! Trying to dismiss.'
)
await this.bot.browser.utils.ghostClick(page, this.secondaryButtonSelector)
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Dismissing Passkey error state')
await this.bot.browser.utils.ghostClick(page, this.selectors.secondaryButton)
}
const u = new URL(page.url())
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) {
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => {})
const signedIn = await page
.waitForSelector('#id_n', { timeout: 3000 })
.waitForSelector(this.selectors.bingProfile, { timeout: 3000 })
.then(() => true)
.catch(() => false)
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-BING', `Profile element found: ${signedIn}`)
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
}
}
@@ -424,16 +537,12 @@ export class Login {
await this.bot.utils.wait(1000)
}
this.bot.logger.warn(
this.bot.isMobile,
'LOGIN-BING',
'Could not confirm Bing session after retries; continuing'
)
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-BING', 'Could not verify Bing session, continuing anyway')
} catch (error) {
this.bot.logger.warn(
this.bot.isMobile,
'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) {
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 {
await page
@@ -451,11 +560,7 @@ export class Login {
for (let i = 0; i < loopMax; i++) {
if (page.isClosed()) break
this.bot.logger.debug(
this.bot.isMobile,
'GET-REWARD-SESSION',
`Loop ${i + 1}/${loopMax} | URL=${page.url()}`
)
this.bot.logger.debug(this.bot.isMobile, 'GET-REWARD-SESSION', `Token fetch loop ${i + 1}/${loopMax}`)
const u = new URL(page.url())
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 token =
$('input[name="__RequestVerificationToken"]').attr('value') ??
$('meta[name="__RequestVerificationToken"]').attr('content') ??
$(this.selectors.requestToken).attr('value') ??
$(this.selectors.requestTokenMeta).attr('content') ??
null
if (token) {
this.bot.requestToken = token
this.bot.logger.info(this.bot.isMobile, 'GET-REQUEST-TOKEN', 'Request token has been set!')
this.bot.logger.debug(
this.bot.logger.info(
this.bot.isMobile,
'GET-REWARD-SESSION',
`Token extracted: ${token.substring(0, 10)}...`
`Request token retrieved: ${token.substring(0, 10)}...`
)
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)
@@ -491,19 +600,20 @@ export class Login {
this.bot.logger.warn(
this.bot.isMobile,
'GET-REQUEST-TOKEN',
'No RequestVerificationToken found some activities may not work'
'GET-REWARD-SESSION',
'No RequestVerificationToken found, some activities may not work'
)
} catch (error) {
throw this.bot.logger.error(
this.bot.isMobile,
'GET-REQUEST-TOKEN',
`Reward session error: ${error instanceof Error ? error.message : String(error)}`
'GET-REWARD-SESSION',
`Fatal error: ${error instanceof Error ? error.message : String(error)}`
)
}
}
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)
}
}

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 { randomBytes } from 'crypto'
import { URLSearchParams } from 'url'
import type { AxiosRequestConfig } from 'axios'
import type { MicrosoftRewardsBot } from '../../../index'
@@ -29,25 +28,70 @@ export class MobileAccessLogin {
authorizeUrl.searchParams.append('access_type', 'offline_access')
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.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...')
const start = Date.now()
let code = ''
let lastUrl = ''
while (Date.now() - start < this.maxTimeout) {
const url = new URL(this.page.url())
if (url.hostname === 'login.live.com' && url.pathname === '/oauth20_desktop.srf') {
code = url.searchParams.get('code') || ''
if (code) break
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') {
code = url.searchParams.get('code') || ''
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)
}
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 ''
}
@@ -57,18 +101,24 @@ export class MobileAccessLogin {
data.append('code', code)
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,
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: data.toString()
}
})
const response = await this.bot.axios.request(request)
const token = (response?.data?.access_token as string) ?? ''
if (!token) {
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 ''
}
@@ -78,10 +128,11 @@ export class MobileAccessLogin {
this.bot.logger.error(
this.bot.isMobile,
'LOGIN-APP',
`MobileAccess error: ${error instanceof Error ? error.message : String(error)}`
`MobileAccess error: ${error instanceof Error ? error.stack || error.message : String(error)}`
)
return ''
} 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(() => {})
}
}

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 * as OTPAuth from 'otpauth'
import readline from 'readline'
import type { MicrosoftRewardsBot } from '../../../index'
import { getErrorMessage, promptInput } from './LoginUtils'
export class TotpLogin {
private readonly textInputSelector =
'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 maxManualSeconds = 60
private readonly maxManualAttempts = 5
@@ -17,31 +17,6 @@ export class TotpLogin {
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> {
try {
const visibleInput = await page
@@ -50,19 +25,18 @@ export class TotpLogin {
if (visibleInput) {
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
}
const hiddenInput = await page.$(this.hiddenInputSelector)
if (hiddenInput) {
await hiddenInput.fill(code)
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled hidden TOTP input')
const secondairyInput = await page.$(this.secondairyInputSelector)
if (secondairyInput) {
await secondairyInput.fill(code)
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled TOTP input')
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
} catch (error) {
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')
const filled = await this.fillCode(page, code)
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')
}
@@ -93,6 +66,12 @@ export class TotpLogin {
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
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')
return
}
@@ -100,45 +79,36 @@ export class TotpLogin {
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP secret provided, awaiting manual input')
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)) {
this.bot.logger.warn(
this.bot.isMobile,
'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) {
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
}
const filled = await this.fillCode(page, code)
if (!filled) {
this.bot.logger.error(
this.bot.isMobile,
'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) {
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
}
@@ -146,16 +116,31 @@ export class TotpLogin {
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
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')
return
}
throw new Error(`Manual TOTP input failed after ${this.maxManualAttempts} attempts`)
throw new Error(`TOTP input failed after ${this.maxManualAttempts} attempts`)
} catch (error) {
this.bot.logger.error(
this.bot.isMobile,
'LOGIN-TOTP',
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
`Error occurred: ${error instanceof Error ? error.message : String(error)}`
)
throw error
}

View File

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

View File

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

View File

@@ -1,22 +1,207 @@
import type { AxiosRequestConfig } from 'axios'
import type {
BingSuggestionResponse,
BingTrendingTopicsResponse,
GoogleSearch,
GoogleTrendsResponse
} from '../interface/Search'
import * as fs from 'fs'
import path from 'path'
import type { GoogleSearch, GoogleTrendsResponse, RedditListing, WikipediaTopResponse } from '../interface/Search'
import type { MicrosoftRewardsBot } from '../index'
import { QueryEngine } from '../interface/Config'
export class QueryCore {
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[]> {
const queryTerms: GoogleSearch[] = []
this.bot.logger.info(
this.bot.isMobile,
'SEARCH-GOOGLE-TRENDS',
`Generating search queries, can take a while! | GeoLocale: ${geoLocale}`
)
try {
const request: AxiosRequestConfig = {
@@ -29,163 +214,287 @@ export class QueryCore {
}
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
const rawData = response.data
const trendsData = this.extractJsonFromResponse(rawData)
const trendsData = this.extractJsonFromResponse(response.data)
if (!trendsData) {
throw this.bot.logger.error(
this.bot.isMobile,
'SEARCH-GOOGLE-TRENDS',
'Failed to parse Google Trends response'
)
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No queries')
this.bot.logger.debug(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No trendsData parsed from response')
return []
}
const mappedTrendsData = trendsData.map(query => [query[0], query[9]!.slice(1)])
if (mappedTrendsData.length < 90) {
this.bot.logger.warn(
this.bot.isMobile,
'SEARCH-GOOGLE-TRENDS',
'Insufficient search queries, falling back to US'
)
const mapped = trendsData.map(q => [q[0], q[9]!.slice(1)])
if (mapped.length < 90 && geoLocale !== 'US') {
return this.getGoogleTrends('US')
}
for (const [topic, relatedQueries] of mappedTrendsData) {
for (const [topic, related] of mapped) {
queryTerms.push({
topic: topic as string,
related: relatedQueries as string[]
related: related as string[]
})
}
} 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,
'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 queries
return queryTerms.flatMap(x => [x.topic, ...x.related])
}
private extractJsonFromResponse(text: string): GoogleTrendsResponse[1] | null {
const lines = text.split('\n')
for (const line of lines) {
for (const line of text.split('\n')) {
const trimmed = line.trim()
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
return JSON.parse(JSON.parse(trimmed)[0][2])[1]
} catch {
continue
}
}
if (!trimmed.startsWith('[')) continue
try {
return JSON.parse(JSON.parse(trimmed)[0][2])[1]
} catch {}
}
return null
}
async getBingSuggestions(query: string = '', langCode: string = 'en'): Promise<string[]> {
this.bot.logger.info(
this.bot.isMobile,
'SEARCH-BING-SUGGESTIONS',
`Generating bing suggestions! | LangCode: ${langCode}`
)
async getBingSuggestions(query = '', langCode = 'en'): Promise<string[]> {
try {
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',
headers: {
...(this.bot.fingerprint?.headers ?? {}),
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
}
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 (!searchSuggestions?.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'API returned no results')
return []
if (!suggestions.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-BING-SUGGESTIONS',
`empty suggestions | query="${query}" | lang=${langCode}`
)
}
return searchSuggestions.map(x => x.query)
return suggestions
} catch (error) {
this.bot.logger.error(
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-GOOGLE-TRENDS',
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
'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 {
const request = {
url: `https://api.bing.com/osjson.aspx?query=${term}`,
const request: AxiosRequestConfig = {
url: `https://api.bing.com/osjson.aspx?query=${encodeURIComponent(query)}`,
method: 'GET',
headers: {
'Content-Type': 'application/json'
...(this.bot.fingerprint?.headers ?? {})
}
}
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 (!relatedTerms?.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'API returned no results')
return []
if (!out.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-BING-RELATED',
`empty related terms | query="${query}"`
)
}
return relatedTerms
return out
} catch (error) {
this.bot.logger.error(
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-BING-RELATED',
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
`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 {
const request = {
const request: AxiosRequestConfig = {
url: `https://www.bing.com/api/v7/news/trendingtopics?appid=91B36E34F9D1B900E54E85A77CF11FB3BE5279E6&cc=xl&setlang=${langCode}`,
method: 'GET',
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 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 (!trendingTopics?.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'API returned no results')
return []
if (!topics.length) {
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-BING-TRENDING',
`empty trending topics | lang=${langCode}`
)
}
const queries = trendingTopics.map(x => x.query?.text?.trim() || x.name.trim())
return queries
return topics
} catch (error) {
this.bot.logger.error(
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'No queries')
this.bot.logger.debug(
this.bot.isMobile,
'SEARCH-BING-TRENDING',
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
`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')
try {
await executionContext.run({ isMobile: true, accountEmail }, async () => {
await executionContext.run({ isMobile: true, account }, async () => {
await this.bot.browser.func.closeBrowser(mobileSession.context, accountEmail)
})
this.bot.logger.info('main', 'SEARCH-MANAGER', 'Mobile session closed')
@@ -368,7 +368,7 @@ export class SearchManager {
`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.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.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.debug('main', 'SEARCH-DESKTOP-LOGIN', 'verifyBingSession')

View File

@@ -1,6 +1,12 @@
import type { Page } from 'patchright'
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'
export class Workers {
@@ -38,13 +44,14 @@ export class Workers {
]
const activitiesUncompleted: BasePromotion[] =
morePromotions?.filter(
x =>
!x.complete &&
x.pointProgressMax > 0 &&
x.exclusiveLockedFeatureStatus !== 'locked' &&
x.promotionType
) ?? []
morePromotions?.filter(x => {
if (x.complete) return false
if (x.pointProgressMax <= 0) return false
if (x.exclusiveLockedFeatureStatus === 'locked') return false
if (!x.promotionType) return false
return true
}) ?? []
if (!activitiesUncompleted.length) {
this.bot.logger.info(
@@ -67,13 +74,14 @@ export class Workers {
}
public async doAppPromotions(data: AppDashboardData) {
const appRewards = data.response.promotions.filter(
x =>
x.attributes['complete']?.toLowerCase() === 'false' &&
x.attributes['offerid'] &&
x.attributes['type'] &&
x.attributes['type'] === 'sapphire'
)
const appRewards = data.response.promotions.filter(x => {
if (x.attributes['complete']?.toLowerCase() !== 'false') return false
if (!x.attributes['offerid']) return false
if (!x.attributes['type']) return false
if (x.attributes['type'] !== 'sapphire') return false
return true
})
if (!appRewards.length) {
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')
}
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) {
for (const activity of activities) {
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}`
)
let queries: string[] = []
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 = await queryCore.getGoogleTrends(locale)
queries = [...new Set(queries.map(q => q.trim()).filter(Boolean))]
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Fetched base queries | 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}`)
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Query pool ready | count=${queries.length}`)
// Go to bing
const targetUrl = this.searchPageURL ? this.searchPageURL : this.bingHome
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Navigating to search page | url=${targetUrl}`)
await page.goto(targetUrl)
// Wait until page loaded
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
await this.bot.browser.utils.tryDismissAllMessages(page)
let stagnantLoop = 0
@@ -77,7 +73,6 @@ export class Search extends Workers {
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
const newMissingPointsTotal = newMissingPoints.totalPoints
// Points gained for THIS query only
const rawGained = missingPointsTotal - newMissingPointsTotal
const gainedPoints = Math.max(0, rawGained)
@@ -91,12 +86,10 @@ export class Search extends Workers {
} else {
stagnantLoop = 0
// Update global user data
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
this.bot.userData.currentPoints = newBalance
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
// Track for return value
totalGainedPoints += gainedPoints
this.bot.logger.info(
@@ -107,10 +100,8 @@ export class Search extends Workers {
)
}
// Update loop state
missingPointsTotal = newMissingPointsTotal
// Completed
if (missingPointsTotal === 0) {
this.bot.logger.info(
isMobile,
@@ -120,7 +111,6 @@ export class Search extends Workers {
break
}
// Stuck
if (stagnantLoop > stagnantLoopMax) {
this.bot.logger.warn(
isMobile,
@@ -130,105 +120,123 @@ export class Search extends Workers {
stagnantLoop = 0
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) {
this.bot.logger.info(
isMobile,
'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
const stagnantLoopMax = 5
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(
isMobile,
'SEARCH-BING-EXTRA',
`Fetching related terms for extra searches | baseQuery="${query}"`
`New query pool generated | count=${queries.length}`
)
const relatedTerms = await queryCore.getBingRelatedTerms(query)
this.bot.logger.debug(
isMobile,
'SEARCH-BING-EXTRA',
`Related terms resolved | baseQuery="${query}" | count=${relatedTerms.length}`
)
for (const query of queries) {
this.bot.logger.info(
isMobile,
'SEARCH-BING-EXTRA',
`Extra search | remaining=${missingPointsTotal} | query="${query}"`
)
if (relatedTerms.length > 3) {
for (const term of relatedTerms.slice(1, 3)) {
searchCounters = await this.bingSearch(page, query, isMobile)
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
const newMissingPointsTotal = newMissingPoints.totalPoints
const rawGained = missingPointsTotal - newMissingPointsTotal
const gainedPoints = Math.max(0, rawGained)
if (gainedPoints === 0) {
stagnantLoop++
this.bot.logger.info(
isMobile,
'SEARCH-BING-EXTRA',
`Extra search | remaining=${missingPointsTotal} | query="${term}"`
`No points gained ${stagnantLoop}/${stagnantLoopMax} | query="${query}" | remaining=${newMissingPointsTotal}`
)
} else {
stagnantLoop = 0
searchCounters = await this.bingSearch(page, term, isMobile)
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
const newMissingPointsTotal = newMissingPoints.totalPoints
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
this.bot.userData.currentPoints = newBalance
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
// Points gained for THIS extra query only
const rawGained = missingPointsTotal - newMissingPointsTotal
const gainedPoints = Math.max(0, rawGained)
totalGainedPoints += gainedPoints
if (gainedPoints === 0) {
stagnantLoop++
this.bot.logger.info(
isMobile,
'SEARCH-BING-EXTRA',
`No points gained for extra query ${stagnantLoop}/${stagnantLoopMax} | query="${term}" | remaining=${newMissingPointsTotal}`
)
} else {
stagnantLoop = 0
this.bot.logger.info(
isMobile,
'SEARCH-BING-EXTRA',
`gainedPoints=${gainedPoints} points | query="${query}" | remaining=${newMissingPointsTotal}`,
'green'
)
}
// Update global user data
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
this.bot.userData.currentPoints = newBalance
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
missingPointsTotal = newMissingPointsTotal
// Track for return value
totalGainedPoints += gainedPoints
if (missingPointsTotal === 0) {
this.bot.logger.info(
isMobile,
'SEARCH-BING-EXTRA',
'All required search points earned during extra searches'
)
break
}
this.bot.logger.info(
isMobile,
'SEARCH-BING-EXTRA',
`gainedPoints=${gainedPoints} points | query="${term}" | remaining=${newMissingPointsTotal}`,
'green'
)
}
// Update loop state
missingPointsTotal = newMissingPointsTotal
// Completed
if (missingPointsTotal === 0) {
this.bot.logger.info(
isMobile,
'SEARCH-BING-EXTRA',
'All required search points earned during extra searches'
)
break
}
// Stuck again
if (stagnantLoop > stagnantLoopMax) {
this.bot.logger.warn(
isMobile,
'SEARCH-BING-EXTRA',
`Search did not gain points for ${stagnantLoopMax} extra iterations, aborting extra searches`
)
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
this.bot.logger.info(
isMobile,
'SEARCH-BING',
`Aborted extra searches | startBalance=${startBalance} | finalBalance=${finalBalance}`
)
return totalGainedPoints
}
if (stagnantLoop > stagnantLoopMax) {
this.bot.logger.warn(
isMobile,
'SEARCH-BING-EXTRA',
`Search did not gain points for ${stagnantLoopMax} iterations, aborting extra searches`
)
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
this.bot.logger.info(
isMobile,
'SEARCH-BING',
`Aborted extra searches | startBalance=${startBalance} | finalBalance=${finalBalance}`
)
return totalGainedPoints
}
}
}
@@ -259,7 +267,6 @@ export class Search extends Workers {
this.searchCount++
// Page fill seems to get more sluggish over time
if (this.searchCount % refreshThreshold === 0) {
this.bot.logger.info(
isMobile,
@@ -271,7 +278,7 @@ export class Search extends Workers {
await searchPage.goto(this.bingHome)
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(
@@ -402,11 +409,9 @@ export class Search extends Workers {
await this.bot.utils.wait(this.bot.config.searchSettings.searchResultVisitTime)
if (isMobile) {
// Mobile
await page.goto(searchPageUrl)
this.bot.logger.debug(isMobile, 'SEARCH-RANDOM-CLICK', 'Navigated back to search page')
} else {
// Desktop
const newTab = await this.bot.browser.utils.getLatestTab(page)
const newTabUrl = newTab.url()

View File

@@ -232,7 +232,7 @@ export class SearchOnBing extends Workers {
if (this.bot.config.searchOnBingLocalQueries) {
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)
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
const response = await this.bot.axios.request({
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

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 Utils from './util/Utils'
import { loadAccounts, loadConfig } from './util/Load'
import { checkNodeVersion } from './util/Validator'
import { Login } from './browser/auth/Login'
import { Workers } from './functions/Workers'
@@ -27,7 +28,7 @@ import type { AppDashboardData } from './interface/AppDashBoardData'
interface ExecutionContext {
isMobile: boolean
accountEmail: string
account: Account
}
interface BrowserSession {
@@ -50,7 +51,7 @@ const executionContext = new AsyncLocalStorage<ExecutionContext>()
export function getCurrentContext(): ExecutionContext {
const context = executionContext.getStore()
if (!context) {
return { isMobile: false, accountEmail: 'unknown' }
return { isMobile: false, account: {} as any }
}
return context
}
@@ -62,6 +63,7 @@ async function flushAllWebhooks(timeoutMs = 5000): Promise<void> {
interface UserData {
userName: string
geoLocale: string
langCode: string
initialPoints: number
currentPoints: number
gainedPoints: number
@@ -99,7 +101,8 @@ export class MicrosoftRewardsBot {
constructor() {
this.userData = {
userName: '',
geoLocale: '',
geoLocale: 'US',
langCode: 'en',
initialPoints: 0,
currentPoints: 0,
gainedPoints: 0
@@ -134,7 +137,7 @@ export class MicrosoftRewardsBot {
this.logger.info(
'main',
'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) {
@@ -185,11 +188,14 @@ export class MicrosoftRewardsBot {
const onWorkerDone = async (label: 'exit' | 'disconnect', worker: Worker, code?: number): Promise<void> => {
const { pid } = worker.process
if (!pid || this.exitedWorkers.includes(pid)) return
else this.exitedWorkers.push(pid)
this.activeWorkers -= 1
if (!pid || this.exitedWorkers.includes(pid)) {
return
} else {
this.exitedWorkers.push(pid)
}
this.logger.warn(
'main',
`CLUSTER-WORKER-${label.toUpperCase()}`,
@@ -233,6 +239,7 @@ export class MicrosoftRewardsBot {
if (process.send) {
process.send({ __stats: stats })
}
process.disconnect()
} catch (error) {
this.logger.error(
@@ -355,14 +362,14 @@ export class MicrosoftRewardsBot {
let mobileContextClosed = false
try {
return await executionContext.run({ isMobile: true, accountEmail }, async () => {
mobileSession = await this.browserFactory.createBrowser(account.proxy, accountEmail)
return await executionContext.run({ isMobile: true, account }, async () => {
mobileSession = await this.browserFactory.createBrowser(account)
const initialContext: BrowserContext = mobileSession.context
this.mainMobilePage = await initialContext.newPage()
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 {
this.accessToken = await this.login.getAppAccessToken(this.mainMobilePage, accountEmail)
@@ -410,6 +417,7 @@ export class MicrosoftRewardsBot {
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.doSpecialPromotions) await this.workers.doSpecialPromotions(data)
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.doReadToEarn) await this.activities.doReadToEarn()
@@ -448,7 +456,7 @@ export class MicrosoftRewardsBot {
} finally {
if (mobileSession && !mobileContextClosed) {
try {
await executionContext.run({ isMobile: true, accountEmail }, async () => {
await executionContext.run({ isMobile: true, account }, async () => {
await this.browser.func.closeBrowser(mobileSession!.context, accountEmail)
})
} catch {}
@@ -460,6 +468,8 @@ export class MicrosoftRewardsBot {
export { executionContext }
async function main(): Promise<void> {
// Check before doing anything
checkNodeVersion()
const rewardsBot = new MicrosoftRewardsBot()
process.on('beforeExit', () => {

View File

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

View File

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

View File

@@ -94,3 +94,23 @@ export interface BingTrendingImage {
export interface BingTrendingQuery {
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 formatted = formatMessage(message)
const userName = this.bot.userData.userName ? this.bot.userData.userName : 'MAIN'
const levelTag = level.toUpperCase()
const cleanMsg = `[${now}] [${this.bot.userData.userName}] [${levelTag}] ${platformText(
isMobile
)} [${title}] ${formatted}`
const cleanMsg = `[${now}] [${userName}] [${levelTag}] ${platformText(isMobile)} [${title}] ${formatted}`
const config = this.bot.config
@@ -85,7 +85,7 @@ export class Logger {
}
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

View File

@@ -2,6 +2,7 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import axiosRetry from 'axios-retry'
import { HttpProxyAgent } from 'http-proxy-agent'
import { HttpsProxyAgent } from 'https-proxy-agent'
import { SocksProxyAgent } from 'socks-proxy-agent'
import { URL } from 'url'
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
let urlObj: URL
@@ -67,8 +70,11 @@ class AxiosClient {
return new HttpProxyAgent(proxyUrl)
case 'https:':
return new HttpsProxyAgent(proxyUrl)
case 'socks4:':
case 'socks5:':
return new SocksProxyAgent(proxyUrl)
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 path from 'path'
import type { Account } from '../interface/Account'
import type { Config, ConfigSaveFingerprint } from '../interface/Config'
import type { Account, ConfigSaveFingerprint } from '../interface/Account'
import type { Config } from '../interface/Config'
import { validateAccounts, validateConfig } from './Validator'
let configCache: Config
@@ -18,8 +19,11 @@ export function loadAccounts(): Account[] {
const accountDir = path.join(__dirname, '../', file)
const accounts = fs.readFileSync(accountDir, 'utf-8')
const accountsData = JSON.parse(accounts)
return JSON.parse(accounts)
validateAccounts(accountsData)
return accountsData
} catch (error) {
throw new Error(error as string)
}
@@ -35,6 +39,8 @@ export function loadConfig(): Config {
const config = fs.readFileSync(configDir, 'utf-8')
const configData = JSON.parse(config)
validateConfig(configData)
configCache = configData
return configData

View File

@@ -21,10 +21,19 @@ export default class Util {
}
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
.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
}
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. */
/* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"types": ["node"],
"typeRoots": ["./node_modules/@types"],
"types": [
"node"
],
"typeRoots": [
"./node_modules/@types"
],
// Keep explicit typeRoots to ensure resolution in environments that don't auto-detect before full install.
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
@@ -65,6 +69,14 @@
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"resolveJsonModule": true
},
"include": ["src/**/*.ts", "src/accounts.json", "src/config.json", "src/functions/queries.json"],
"exclude": ["node_modules"]
}
"include": [
"src/**/*.ts",
"src/accounts.json",
"src/config.json",
"src/functions/bing-search-activity-queries.json",
"src/functions/search-queries.json"
],
"exclude": [
"node_modules"
]
}