diff --git a/index.js b/index.js index c96c7f5..214d4b5 100644 --- a/index.js +++ b/index.js @@ -131,6 +131,15 @@ const patchApp = async () => { process.exit(1) } + // Remove @prisma/instrumentation - it has a broken nested node_modules reference + // to @opentelemetry/instrumentation that causes a crash after repacking. + // It's only telemetry and not needed for the app to function. + const prismaInstrPath = path.join(tempPath, 'node_modules', '@prisma', 'instrumentation') + if (fs.existsSync(prismaInstrPath)) { + rm(prismaInstrPath) + console.log(chalk.yellowBright`Removed broken @prisma/instrumentation package`) + } + const indexPath = path.join(tempPath, 'build', 'index.js') if (!fs.existsSync(indexPath)) { console.error(chalk.redBright`Couldn't find index.js file`) diff --git a/package-lock.json b/package-lock.json index 2e12632..380de82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "httptoolkit-pro-patcher", + "name": "httptoolkit-patcher", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "httptoolkit-pro-patcher", + "name": "httptoolkit-patcher", "version": "1.0.0", "license": "MIT", "dependencies": { @@ -53,9 +53,9 @@ } }, "node_modules/@types/node": { - "version": "20.19.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.28.tgz", - "integrity": "sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==", + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", "dev": true, "license": "MIT", "dependencies": { @@ -251,7 +251,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -511,4 +511,4 @@ } } } -} \ No newline at end of file +} diff --git a/patch.js b/patch.js index 29e685e..fc3c684 100644 --- a/patch.js +++ b/patch.js @@ -1,16 +1,24 @@ -// Patcher imports - need these for ESM compatibility -import { createRequire as __patcherCreateRequire } from 'module'; -import { fileURLToPath as __patcherFileURLToPath } from 'url'; -import { dirname as __patcherDirname, join as __patcherJoin } from 'path'; +// Patcher imports - dynamic import works in both ESM and CJS +const __patcherHttpsProxyAgent = (await import('https-proxy-agent')).HttpsProxyAgent; +const __patcherExpress = (await import('express')).default; +const __patcherHttps = (await import('https')).default; +const __patcherFs = (await import('fs')).default; +const __patcherOs = (await import('os')).default; +const __patcherPath = (await import('path')).default; -// Load modules using require (works better with the app's setup) -const __patcherRequire = __patcherCreateRequire(import.meta.url); -const __patcherHttpsProxyAgent = __patcherRequire('https-proxy-agent').HttpsProxyAgent; -const __patcherExpress = __patcherRequire('express'); -const __patcherHttps = __patcherRequire('https'); -const __patcherFs = __patcherRequire('fs'); -const __patcherOs = __patcherRequire('os'); -const __patcherPath = __patcherRequire('path'); +// The fake pro user we inject everywhere +const __patcherProUser = { + email, + subscription: { + status: 'active', + expiry: new Date('6767-06-07').toISOString(), + sku: 'pro-annual', + }, + userId: 'patcher', + banned: false, + featureFlags: [], + teamSubscription: null, +}; // Simple HTTP request helper with redirect support const __patcherRequest = (method, url, redirectCount = 0) => new Promise((resolve, reject) => { @@ -46,6 +54,17 @@ console.log(`[Patcher] Temp path: ${__patcherTempPath}`); const __patcherApp = __patcherExpress(); __patcherApp.disable('x-powered-by'); +// LAYER 2: Intercept account API calls directly +// Handles any endpoint the account store calls, so even if JS regex fails, app still gets pro user +__patcherApp.all(/^\/__patcher_api\/(.*)/, (req, res) => { + console.log(`[Patcher] API intercept: ${req.url}`); + const user = JSON.parse(JSON.stringify(__patcherProUser)); + user.subscription.expiry = new Date(user.subscription.expiry); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', '*'); + res.json(user); +}); + // Catch all requests and proxy them through our server __patcherApp.all(/(.*)/, async (req, res) => { console.log(`[Patcher] ${req.url}`); @@ -100,44 +119,39 @@ __patcherApp.all(/(.*)/, async (req, res) => { let data = remoteFile.data; - // This is where the magic happens - patch main.js to inject pro subscription + // LAYER 1: Patch main.js JS directly (with multiple fallback patterns) if (new URL(req.url, process.env.APP_URL).pathname === '/main.js') { console.log(`[Patcher] Patching main.js...`); res.setHeader('Cache-Control', 'no-store'); data = data.toString(); - // Find the account store class and module names (they're minified so we gotta search for them) - const accStoreName = data.match(/class ([0-9A-Za-z_]+){constructor\(e\){this\.goToSettings=e/)?.[1]; - const modName = data.match(/([0-9A-Za-z_]+).(getLatestUserData|getLastUserData)/)?.[1]; + // Try multiple regex patterns for different minification versions + const accStoreName = + data.match(/class ([0-9A-Za-z_$]+)\s*\{\s*constructor\s*\(\s*e\s*\)\s*\{\s*this\.goToSettings\s*=\s*e/)?.[1] || + data.match(/class ([0-9A-Za-z_$]+)[^{]*\{[^}]{0,200}goToSettings/)?.[1]; - if (!accStoreName) console.error(`[Patcher] Couldn't find account store class`); - else if (!modName) console.error(`[Patcher] Couldn't find user data module`); - else { - // Override the user data functions to return our fake pro user + const modName = + data.match(/([0-9A-Za-z_$]+)\.(getLatestUserData|getLastUserData)/)?.[1]; + + const userJson = JSON.stringify(__patcherProUser); + const userInit = `const __pUser=${userJson};__pUser.subscription.expiry=new Date(__pUser.subscription.expiry);`; + + if (!accStoreName) { + console.error(`[Patcher] Couldn't find account store class - relying on API interception`); + } else if (!modName) { + console.error(`[Patcher] Couldn't find user data module - relying on API interception`); + } else { + const classPattern = new RegExp(`class ${accStoreName}\\s*\\{`); let patched = data.replace( - `class ${accStoreName}{`, - `["getLatestUserData","getLastUserData"].forEach(p=>Object.defineProperty(${modName},p,{value:()=>user}));class ${accStoreName}{` + classPattern, + `["getLatestUserData","getLastUserData"].forEach(p=>Object.defineProperty(${modName},p,{value:()=>__pUser}));class ${accStoreName}{` ); if (patched === data) { - console.error(`[Patcher] Patch failed - couldn't find injection point`); + console.error(`[Patcher] JS inject failed - relying on API interception`); } else { - // Inject our pro user at the top - patched = `const user=${JSON.stringify({ - email, - subscription: { - status: 'active', - expiry: new Date('6767-06-07').toISOString(), - sku: 'pro-annual', - }, - userId: 'patcher', - banned: false, - featureFlags: [], - teamSubscription: null, - })};user.subscription.expiry=new Date(user.subscription.expiry);` + patched; - - data = patched; + data = userInit + patched; console.log(`[Patcher] main.js patched successfully!`); } } @@ -153,7 +167,7 @@ __patcherApp.all(/(.*)/, async (req, res) => { __patcherApp.listen(__patcherPort, () => console.log(`[Patcher] Running on port ${__patcherPort}`)); -// Fix CORS so the app can talk to our local server +// Fix CORS and intercept account API at the Electron session level app.on('ready', () => { session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { // Block telemetry @@ -169,6 +183,21 @@ app.on('ready', () => { callback({ requestHeaders: details.requestHeaders }); }); + // LAYER 2: Redirect any account/auth API calls to our fake endpoint + session.defaultSession.webRequest.onBeforeRequest( + { urls: ['https://api.httptoolkit.tech/*', 'https://accounts.httptoolkit.tech/*'] }, + (details, callback) => { + try { + const url = new URL(details.url); + const redirectURL = `http://localhost:${__patcherPort}/__patcher_api${url.pathname}${url.search}`; + console.log(`[Patcher] Redirecting API: ${details.url} -> ${redirectURL}`); + callback({ redirectURL }); + } catch (e) { + callback({}); + } + } + ); + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { if (details.responseHeaders) { details.responseHeaders['Access-Control-Allow-Origin'] = [`http://localhost:${__patcherPort}`];