Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
306 changes: 220 additions & 86 deletions scripts/patch-fast-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,87 +5,175 @@
* The speed selector is gated by authMethod === "chatgpt" checks.
* API-key users never see it because their authMethod differs.
*
* This patch locates BinaryExpression nodes matching:
* X.authMethod !== "chatgpt"
* inside functions that also reference "fast_mode", and replaces
* the comparison with !1 (always false), removing the auth gate.
* This patch locates auth gates inside functions that also reference
* "fast_mode", and either removes legacy negative gates or extends
* positive ChatGPT-only gates to include API-key auth.
*
* Target: permissions-mode-helpers-*.js (or any chunk with the pattern)
* Target: service-tier / permissions chunks that contain the pattern.
*/
const fs = require("fs");
const path = require("path");
const { parse } = require("acorn");
const { locateBundles, relPath, SRC_DIR } = require("./patch-util");
const { relPath, SRC_DIR } = require("./patch-util");

function walk(node, visitor) {
const CHATGPT_AUTH = "chatgpt";
const APIKEY_AUTH = "apikey";
const ALWAYS_FALSE = "!1";

function walk(node, visitor, parent = null) {
if (!node || typeof node !== "object") return;
if (node.type) visitor(node);
if (node.type) visitor(node, parent);
for (const key of Object.keys(node)) {
if (key === "type" || key === "start" || key === "end") continue;
const child = node[key];
if (Array.isArray(child)) {
for (const item of child) {
if (item && typeof item === "object" && item.type) walk(item, visitor);
if (item && typeof item === "object" && item.type) {
walk(item, visitor, node);
}
}
} else if (child && typeof child === "object" && child.type) {
walk(child, visitor);
walk(child, visitor, node);
}
}
}

function sourceFor(source, node) {
return source.slice(node.start, node.end);
}

function isFunctionNode(node) {
return (
node.type === "FunctionDeclaration" ||
node.type === "FunctionExpression" ||
node.type === "ArrowFunctionExpression"
);
}

function isStringLiteral(node, value) {
if (node.type === "Literal") return node.value === value;
if (node.type !== "TemplateLiteral") return false;
return (
node.expressions.length === 0 &&
node.quasis.length === 1 &&
node.quasis[0].value.cooked === value
);
}

function isFalseLiteral(node, source) {
if (node.type === "Literal") return node.value === false;
return sourceFor(source, node) === ALWAYS_FALSE;
}

function getChatGptEqualityOperand(node, source) {
if (node.type !== "BinaryExpression" || node.operator !== "===") return null;
if (isStringLiteral(node.right, CHATGPT_AUTH)) return sourceFor(source, node.left);
if (isStringLiteral(node.left, CHATGPT_AUTH)) return sourceFor(source, node.right);
return null;
}

function buildApiKeyAuthExpression(operand) {
return `${operand}===\`${CHATGPT_AUTH}\`||${operand}===\`${APIKEY_AUTH}\``;
}

function hasApiKeyEquality(node, source, operand) {
let found = false;
walk(node, (child) => {
if (found || child.type !== "BinaryExpression" || child.operator !== "===") {
return;
}
const left = sourceFor(source, child.left);
const right = sourceFor(source, child.right);
found =
(left === operand && isStringLiteral(child.right, APIKEY_AUTH)) ||
(right === operand && isStringLiteral(child.left, APIKEY_AUTH));
});
return found;
}

function isAlreadyApiKeyAuthGate(node, parent, source, operand) {
if (parent?.type !== "LogicalExpression" || parent.operator !== "||") {
return false;
}
return hasApiKeyEquality(parent, source, operand);
}

function addPatch(patches, patch) {
if (patches.some((p) => p.start === patch.start)) return;
patches.push(patch);
}

function collectLegacyNegativeGatePatches(node, source, patches) {
if (node.type !== "BinaryExpression" || node.operator !== "!==") return;

const original = sourceFor(source, node);
if (!original.includes("authMethod") || !original.includes(CHATGPT_AUTH)) return;
if (original === ALWAYS_FALSE) return;

addPatch(patches, {
id: "fast_mode_legacy_auth_gate",
start: node.start,
end: node.end,
replacement: ALWAYS_FALSE,
original,
});
}

function collectPositiveAuthGatePatches(node, parent, source, patches) {
const original = sourceFor(source, node);
const operand = getChatGptEqualityOperand(node, source);
if (operand == null || !original.includes("authMethod")) return;
if (isAlreadyApiKeyAuthGate(node, parent, source, operand)) return;

addPatch(patches, {
id: "fast_mode_positive_auth_gate",
start: node.start,
end: node.end,
replacement: buildApiKeyAuthExpression(operand),
original,
});
}

function collectConditionalAuthGatePatches(node, source, patches) {
if (node.type !== "ConditionalExpression") return;
if (!sourceFor(source, node.consequent).includes("fast_mode")) return;
if (!sourceFor(source, node).includes("authMethod")) return;
if (!isFalseLiteral(node.alternate, source)) return;

const operand = getChatGptEqualityOperand(node.test, source);
if (operand == null) return;

addPatch(patches, {
id: "fast_mode_conditional_auth_gate",
start: node.test.start,
end: node.test.end,
replacement: buildApiKeyAuthExpression(operand),
original: sourceFor(source, node.test),
});
}

function collectPatches(ast, source) {
const patches = [];

walk(ast, (node) => {
// Match function bodies containing both authMethod and fast_mode
const isFn =
node.type === "FunctionDeclaration" ||
node.type === "FunctionExpression" ||
node.type === "ArrowFunctionExpression";
if (!isFn) return;
if (!isFunctionNode(node)) return;

const fnSrc = source.slice(node.start, node.end);
const fnSrc = sourceFor(source, node);
if (!fnSrc.includes("authMethod") || !fnSrc.includes("fast_mode")) return;

// Inside this function, find: X.authMethod !== `chatgpt`
walk(node, (child) => {
if (child.type !== "BinaryExpression" || child.operator !== "!==") return;

const childSrc = source.slice(child.start, child.end);
if (!childSrc.includes("authMethod") || !childSrc.includes("chatgpt"))
return;

if (childSrc === "!1") return;

// Avoid duplicate patches at same offset
if (patches.some((p) => p.start === child.start)) return;

patches.push({
id: "fast_mode_auth_gate",
start: child.start,
end: child.end,
replacement: "!1",
original: childSrc,
});
// Inside this function, find auth gates tied to Fast mode.
walk(node, (child, parent) => {
collectLegacyNegativeGatePatches(child, source, patches);
collectPositiveAuthGatePatches(child, parent, source, patches);
collectConditionalAuthGatePatches(child, source, patches);
});
});

return patches;
}

function main() {
const args = process.argv.slice(2);
const isCheck = args.includes("--check");
const platform = args.find((a) =>
["mac-arm64", "mac-x64", "win"].includes(a),
);

const platforms = platform
? [platform]
: ["mac-arm64", "mac-x64", "win"].filter((p) =>
fs.existsSync(path.join(SRC_DIR, p, "_asar", "webview", "assets")),
);

function findTargets(platforms) {
const targets = [];
for (const plat of platforms) {
const assetsDir = path.join(SRC_DIR, plat, "_asar", "webview", "assets");
Expand All @@ -99,57 +187,103 @@ function main() {
}
}
}
return targets;
}

if (targets.length === 0) {
console.log(" [skip] No chunk contains fast_mode gate logic");
return;
function parseBundle(bundle, source) {
try {
return parse(source, { ecmaVersion: "latest", sourceType: "module" });
} catch (error) {
throw new Error(`${relPath(bundle.path)} parse failed: ${error.message}`);
}
}

let totalPatched = 0;
function applyPatches(source, patches) {
let code = source;
for (const patch of patches.sort((a, b) => b.start - a.start)) {
console.log(` * ${patch.original} -> ${patch.replacement}`);
code =
code.slice(0, patch.start) +
patch.replacement +
code.slice(patch.end);
}
return code;
}

for (const bundle of targets) {
const source = fs.readFileSync(bundle.path, "utf-8");

const t0 = Date.now();
let ast;
try {
ast = parse(source, { ecmaVersion: "latest", sourceType: "module" });
} catch {
continue;
}
function getPlatforms(platform) {
if (platform) return [platform];
return ["mac-arm64", "mac-x64", "win"].filter((p) =>
fs.existsSync(path.join(SRC_DIR, p, "_asar", "webview", "assets")),
);
}

const patches = collectPatches(ast, source);
function processBundle(bundle, isCheck) {
const source = fs.readFileSync(bundle.path, "utf-8");
const t0 = Date.now();
const ast = parseBundle(bundle, source);
const patches = collectPatches(ast, source);

if (patches.length === 0) continue;
if (patches.length === 0) return { candidates: 0, patched: 0 };

console.log(
` [${bundle.platform}] ${relPath(bundle.path)} (parse ${Date.now() - t0}ms)`,
);
console.log(
` [${bundle.platform}] ${relPath(bundle.path)} (parse ${Date.now() - t0}ms)`,
);

if (isCheck) {
for (const p of patches) {
console.log(` [?] offset ${p.start}: ${p.original} -> ${p.replacement}`);
}
continue;
if (isCheck) {
for (const patch of patches) {
console.log(` [?] offset ${patch.start}: ${patch.original} -> ${patch.replacement}`);
}
return { candidates: patches.length, patched: 0 };
}

patches.sort((a, b) => b.start - a.start);
fs.writeFileSync(bundle.path, applyPatches(source, patches), "utf-8");
return { candidates: patches.length, patched: patches.length };
}

let code = source;
for (const p of patches) {
console.log(` * ${p.original} -> ${p.replacement}`);
code = code.slice(0, p.start) + p.replacement + code.slice(p.end);
}
function printSummary({ isCheck, totalCandidates, totalPatched }) {
if (isCheck && totalCandidates > 0) {
console.log(` [ok] ${totalCandidates} auth gate(s) would be patched`);
} else if (isCheck) {
console.log(" [ok] no fast_mode auth gates need patching");
} else if (totalPatched > 0) {
const candidateSuffix =
totalCandidates && totalCandidates !== totalPatched
? ` (out of ${totalCandidates} candidate auth gate(s))`
: "";
console.log(` [ok] ${totalPatched} auth gate(s) patched${candidateSuffix}`);
} else {
console.log(" [ok] fast_mode auth gates already patched or absent");
}
}
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

function main() {
const args = process.argv.slice(2);
const isCheck = args.includes("--check");
const platform = args.find((a) =>
["mac-arm64", "mac-x64", "win"].includes(a),
);

const targets = findTargets(getPlatforms(platform));

fs.writeFileSync(bundle.path, code, "utf-8");
totalPatched += patches.length;
if (targets.length === 0) {
console.log(" [skip] No chunk contains fast_mode gate logic");
return;
}

if (totalPatched > 0) {
console.log(` [ok] ${totalPatched} auth gate(s) removed`);
} else {
console.log(" [ok] fast_mode auth gates already patched or absent");
let totalPatched = 0;
let totalCandidates = 0;

for (const bundle of targets) {
const result = processBundle(bundle, isCheck);
totalCandidates += result.candidates;
totalPatched += result.patched;
}

printSummary({ isCheck, totalCandidates, totalPatched });
}

if (require.main === module) {
main();
}

main();
module.exports = { collectPatches };