Exocore-gateway / src /core /gemini.js
Johnsteve Costanos
Update Exocore Gateway
6a989d5
Raw
History Blame Contribute Delete
72.6 kB
import * as crypto from 'crypto';
import { randomUUID } from 'crypto';
import { URLSearchParams, fileURLToPath } from 'url';
import axios from 'axios';
import https from 'https';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { z } from 'zod';
import { logger } from '@exocore/multi';
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 1 — Constants
// ═══════════════════════════════════════════════════════════════════════════
export const Endpoint = {
GOOGLE: 'https://www.google.com',
INIT: 'https://gemini.google.com/app',
GENERATE: 'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate',
BATCH_EXEC: 'https://gemini.google.com/_/BardChatUi/data/batchexecute',
UPLOAD: 'https://content-push.googleapis.com/upload',
ROTATE_COOKIES: 'https://accounts.google.com/RotateCookies',
PROCESS_GEM_FILE: 'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/ProcessFile',
};
export const RPC = {
LIST_CHATS: 'MaZiqc',
READ_CHAT: 'hNvQHb',
DELETE_CHAT: 'GzXR5e',
DELETE_CHAT_2: 'qWymEb',
LIST_GEMS: 'CNgdBe',
CREATE_GEM: 'oMH3Zd',
UPDATE_GEM: 'kHv0Vd',
DELETE_GEM: 'UXcSJb',
LIST_NLM_NOTEBOOKS: 'NXpLKc',
BARD_ACTIVITY: 'ESY5D',
STATUS_POLL: 'aPya6c',
ASYNC_POLL: 'kwDCne',
POST_PROCESS: 'PCck7e',
INIT_CONFIG: 'otAQ7b',
INIT_FEATURES: 'GPRiHf',
INIT_SETTINGS: 'maGuAc',
INIT_STATE: 'cYRIkd',
INIT_CAPABILITIES: 'ozz5Z',
SYNC_STATE: 'L5adhe',
USER_PREFS: 'K4WWud',
USAGE_INFO: 'qpEbW',
ACCOUNT_INFO: 'o30O0e',
HISTORY_STATE: 'DYBcR',
NOTIFICATION_STATE: 'ku4Jyf',
};
export const BATCH_EXEC_HEADERS = {
'x-goog-ext-525001261-jspb': '[1,null,null,null,null,null,null,null,[4]]',
'x-goog-ext-73010989-jspb': '[0]',
};
export const MODEL_HEADER_KEY = 'x-goog-ext-525001261-jspb';
export const TRACKING_HEADER_KEY = 'x-goog-ext-73010989-jspb';
export const SESSION_HEADER_KEY = 'x-goog-ext-525005358-jspb';
export const SAFETY_HEADER_KEY = 'x-goog-ext-73010990-jspb';
export const TEMP_MODEL_HASH = 'fbb127bbb056c959';
export const SYNC_STATE_NULL_COUNT = 87;
export class Model {
constructor(name, hash = null) {
this.name = name;
this.hash = hash;
}
get header() {
if (!this.hash) return {};
return { [MODEL_HEADER_KEY]: `[1,null,null,null,"${this.hash}",null,null,0,[4],null,null,2]` };
}
toString() { return `Model(name=${JSON.stringify(this.name)}, hash=${JSON.stringify(this.hash)})`; }
equals(other) { if (other instanceof Model) return this.name === other.name; return false; }
}
export const MODELS = {
unspecified: new Model('unspecified', null),
fast: new Model('fast', '56fdd199312815e2'),
thinking: new Model('thinking', 'e051ce1aa80aa576'),
pro: new Model('pro', 'e6fa609c3fa255c0'),
};
export const MODEL_ALIASES = { flash: 'fast', preview: 'fast', default: 'fast' };
export const DEFAULT_MODEL = MODELS.fast;
export const TOKEN_FACTORY_MODELS = new Set(['pro', 'thinking']);
export const CHROME_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';
export const XBV_API_KEY = 'AIzaSyDr2UxVnv_U85AbhhY8XSHSIavUW0DC-sY';
export const DRIVE_PICKER_API_KEY = 'AIzaSyAw-cTyp9Xotzvu3vNDWhDU3E9NConkKxQ';
export const CHROME_HEADERS = {
Accept: '*/*', 'Accept-Language': 'en-US,en;q=0.9',
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
Origin: 'https://gemini.google.com', Referer: 'https://gemini.google.com/',
'User-Agent': CHROME_USER_AGENT, 'X-Same-Domain': '1',
'x-browser-channel': 'stable', 'x-browser-year': '2026',
'x-client-data': 'CJWJywE=',
'Sec-Ch-Ua': '"Chromium";v="145", "Google Chrome";v="145", "Not-A.Brand";v="24"',
'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"macOS"',
'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin',
};
export function computeXbv(userAgent) {
return crypto.createHash('sha1').update(XBV_API_KEY + userAgent).digest('base64');
}
export function getModel(name) {
if (name in MODELS) return MODELS[name];
if (name in MODEL_ALIASES) return MODELS[MODEL_ALIASES[name]];
const all = Array.from(new Set([...Object.keys(MODELS), ...Object.keys(MODEL_ALIASES)])).sort();
throw new Error(`Unknown model: ${JSON.stringify(name)}. Available: ${all.join(', ')}`);
}
export const ErrorCode = {
TEMPORARY_ERROR: 1013, USAGE_LIMIT_EXCEEDED: 1037,
MODEL_INCONSISTENT: 1050, MODEL_HEADER_INVALID: 1052, IP_TEMPORARILY_BLOCKED: 1060,
};
export const GEMINI_HEADERS = {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
Host: 'gemini.google.com', Origin: 'https://gemini.google.com',
Referer: 'https://gemini.google.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
'X-Same-Domain': '1',
};
export const ROTATE_COOKIES_HEADERS = { 'Content-Type': 'application/json' };
export const UPLOAD_HEADERS = { 'Push-ID': 'feeds/mcudyrk2a4khkz' };
export const TOKEN_PATTERNS = {
snlm0e: /"SNlM0e":\s*"(.*?)"/,
cfb2h: /"cfb2h":\s*"(.*?)"/,
fdrfje: /"FdrFJe":\s*"(.*?)"/,
};
export const EMAIL_PATTERN = /"oPEP7c":\s*"([^"]+@[^"]+)"/;
export const COOKIE_1PSID = '__Secure-1PSID';
export const COOKIE_1PSIDTS = '__Secure-1PSIDTS';
export const COOKIE_DOMAIN = '.google.com';
export const COOKIE_REFRESH_INTERVAL = 540;
export const COOKIE_REFRESH_COOLDOWN = 60;
export const ROTATE_COOKIES_BODY = '[000,"-0000000000000000000"]';
export const CONFIG_DIR_NAME = '.gemini-web-mcp-cli';
export const NLM_CONFIG_DIR_NAME = '.notebooklm-mcp-cli';
export const PROFILES_DIR = 'profiles';
export const CONFIG_FILE = 'config.json';
export const AUTH_FILE = 'auth.json';
export const ENV_PROFILE = 'GEMCLI_PROFILE';
export const DEFAULT_PROFILE_NAME = 'default';
export const REQID_INCREMENT = 100000;
export const INNER_REQ_LIST_SIZE = 70;
export const VIDEO_TOOL_ID = 11;
export const IMAGE_TOOL_ID = 14;
export const MUSIC_TOOL_ID = 21;
export const RESPONSE_PATHS = {
inner_json: [2], metadata: [1], candidates: [4], completion: [25],
error_code: [5, 2, 0, 1, 0], candidate_rcid: [0], candidate_text: [1, 0],
candidate_thoughts: [37, 0, 0], candidate_web_images: [12, 1],
candidate_generated_images: [12, 7, 0], candidate_generated_music: [12, 86],
candidate_generated_video: [12, 59],
};
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 2 — Exceptions
// ═══════════════════════════════════════════════════════════════════════════
export class GeminiError extends Error {
constructor(message) { super(message); this.name = new.target.name; Object.setPrototypeOf(this, new.target.prototype); }
}
export class AuthError extends GeminiError {}
export class APIError extends GeminiError { constructor(message, errorCode = null) { super(message); this.errorCode = errorCode; } }
export class UsageLimitExceeded extends GeminiError {}
export class ModelInvalid extends GeminiError {}
export class TemporarilyBlocked extends GeminiError {}
export class GeminiTimeoutError extends GeminiError {}
export class ImageGenerationError extends APIError {}
export class VideoGenerationError extends APIError {}
export class MusicGenerationError extends APIError {}
export class ResearchError extends APIError {}
export class ProfileError extends GeminiError {}
export class TokenFactoryError extends GeminiError {}
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 3 — Profiles
// ═══════════════════════════════════════════════════════════════════════════
function configDir() { return path.join(os.homedir(), CONFIG_DIR_NAME); }
function nlmConfigDir() { return path.join(os.homedir(), NLM_CONFIG_DIR_NAME); }
function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
export class ProfileManager {
constructor({ configDir: cd, nlmConfigDir: ncd } = {}) {
this.configDir = cd ?? configDir();
this.profilesDir = path.join(this.configDir, PROFILES_DIR);
this.configFile = path.join(this.configDir, CONFIG_FILE);
this.nlmConfigDir = ncd ?? nlmConfigDir();
this.nlmProfilesDir = path.join(this.nlmConfigDir, PROFILES_DIR);
}
#ensureDirs() { fs.mkdirSync(this.configDir, { recursive: true }); fs.mkdirSync(this.profilesDir, { recursive: true }); }
#profileDir(name) { return path.join(this.profilesDir, name); }
#authFilePath(name) { return path.join(this.#profileDir(name), AUTH_FILE); }
#nlmProfileDir(name) { return path.join(this.nlmProfilesDir, name); }
#readConfig() {
if (fs.existsSync(this.configFile)) { try { return JSON.parse(fs.readFileSync(this.configFile, 'utf-8')); } catch { return {}; } }
return {};
}
#writeConfig(config) { this.#ensureDirs(); fs.writeFileSync(this.configFile, JSON.stringify(config, null, 2)); }
getActiveProfile() {
const env = process.env[ENV_PROFILE];
if (env) return env;
const cfg = this.#readConfig();
return cfg.active_profile ?? DEFAULT_PROFILE_NAME;
}
setActiveProfile(name) {
if (!this.profileExists(name)) throw new ProfileError(`Profile '${name}' does not exist.`);
const cfg = this.#readConfig();
cfg.active_profile = name; cfg.version = 1;
this.#writeConfig(cfg);
logger.info(`Switched to profile: ${name}`);
}
profileExists(name) { return isDir(this.#profileDir(name)) || isDir(this.#nlmProfileDir(name)); }
#listGemcliProfiles() {
if (!fs.existsSync(this.profilesDir)) return [];
return fs.readdirSync(this.profilesDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name).sort();
}
#listNlmProfiles() {
if (!fs.existsSync(this.nlmProfilesDir)) return [];
return fs.readdirSync(this.nlmProfilesDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name).sort();
}
listProfiles() { return Array.from(new Set([...this.#listGemcliProfiles(), ...this.#listNlmProfiles()])).sort(); }
listProfilesDetailed() {
const profiles = new Map();
for (const name of this.#listNlmProfiles()) profiles.set(name, { name, source: 'nlm', path: this.#nlmProfileDir(name), displayName: `${name} (from NotebookLM)` });
for (const name of this.#listGemcliProfiles()) profiles.set(name, { name, source: 'gemcli', path: this.#profileDir(name), displayName: name });
return Array.from(profiles.values()).sort((a, b) => a.name.localeCompare(b.name));
}
getProfileSource(name) {
if (isDir(this.#profileDir(name))) return 'gemcli';
if (isDir(this.#nlmProfileDir(name))) return 'nlm';
throw new ProfileError(`Profile '${name}' does not exist.`);
}
createProfile(name) {
this.#ensureDirs();
const dir = this.#profileDir(name);
if (fs.existsSync(dir)) throw new ProfileError(`Profile '${name}' already exists.`);
fs.mkdirSync(dir, { recursive: true }); logger.info(`Created profile: ${name}`); return dir;
}
deleteProfile(name) {
const dir = this.#profileDir(name);
if (!fs.existsSync(dir)) {
if (isDir(this.#nlmProfileDir(name))) throw new ProfileError(`Profile '${name}' is from NotebookLM MCP. Delete it from NotebookLM instead.`);
throw new ProfileError(`Profile '${name}' does not exist.`);
}
if (this.getActiveProfile() === name) throw new ProfileError(`Cannot delete the active profile '${name}'. Switch to another profile first.`);
fs.rmSync(dir, { recursive: true, force: true }); logger.info(`Deleted profile: ${name}`);
}
renameProfile(oldName, newName) {
const oldDir = this.#profileDir(oldName);
if (!fs.existsSync(oldDir)) {
if (isDir(this.#nlmProfileDir(oldName))) throw new ProfileError(`Profile '${oldName}' is from NotebookLM MCP. Rename it from NotebookLM instead.`);
throw new ProfileError(`Profile '${oldName}' does not exist.`);
}
const trimmed = newName.trim();
if (!trimmed) throw new ProfileError('New profile name cannot be empty.');
if (this.profileExists(trimmed)) throw new ProfileError(`Profile '${trimmed}' already exists.`);
const newDir = this.#profileDir(trimmed);
fs.renameSync(oldDir, newDir);
logger.debug(`Renamed profile dir: ${oldName} -> ${trimmed}`);
const chromeProfilesDir = path.join(this.configDir, 'chrome-profiles');
const oldChrome = path.join(chromeProfilesDir, oldName);
const newChrome = path.join(chromeProfilesDir, trimmed);
if (fs.existsSync(oldChrome)) { fs.renameSync(oldChrome, newChrome); logger.debug(`Renamed Chrome profile: ${oldName} -> ${trimmed}`); }
const oldTf = path.join(chromeProfilesDir, `${oldName}_tf`);
const newTf = path.join(chromeProfilesDir, `${trimmed}_tf`);
if (fs.existsSync(oldTf)) { fs.renameSync(oldTf, newTf); logger.debug(`Renamed TF profile: ${oldName}_tf -> ${trimmed}_tf`); }
const authFile = this.#authFilePath(trimmed);
if (fs.existsSync(authFile)) {
try {
const data = JSON.parse(fs.readFileSync(authFile, 'utf-8'));
const oldPath = data.chrome_profile_path ?? '';
if (oldPath && oldPath.includes(oldName)) { data.chrome_profile_path = oldPath.replace(`/${oldName}`, `/${trimmed}`); fs.writeFileSync(authFile, JSON.stringify(data, null, 2)); }
} catch (e) { logger.warn(`Could not update auth.json: ${e.message}`); }
}
if (this.getActiveProfile() === oldName) { const cfg = this.#readConfig(); cfg.active_profile = trimmed; this.#writeConfig(cfg); logger.debug(`Updated active profile: ${oldName} -> ${trimmed}`); }
logger.info(`Renamed profile: ${oldName} -> ${trimmed}`);
}
loadAuth(name) {
const profileName = name ?? this.getActiveProfile();
const gemcliAuth = this.#authFilePath(profileName);
if (fs.existsSync(gemcliAuth)) { try { return JSON.parse(fs.readFileSync(gemcliAuth, 'utf-8')); } catch (e) { logger.warn(`Failed to load gemcli auth for '${profileName}': ${e.message}`); } }
const nlmChromeDir = path.join(this.nlmConfigDir, 'chrome-profiles', profileName);
if (isDir(nlmChromeDir)) { logger.debug(`Using NLM Chrome profile for '${profileName}': ${nlmChromeDir}`); return { chrome_profile_path: nlmChromeDir, _source: 'nlm', _nlm_profile: profileName }; }
return {};
}
saveAuth(authData, name) {
const profileName = name ?? this.getActiveProfile();
this.#ensureDirs();
const dir = this.#profileDir(profileName);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(this.#authFilePath(profileName), JSON.stringify(authData, null, 2));
logger.debug(`Saved auth for profile: ${profileName}`);
}
getChromeProfilePath(name) {
const profileName = name ?? this.getActiveProfile();
const gemcliAuth = this.#authFilePath(profileName);
if (fs.existsSync(gemcliAuth)) { try { const data = JSON.parse(fs.readFileSync(gemcliAuth, 'utf-8')); if (data.chrome_profile_path) return data.chrome_profile_path; } catch { } }
const nlmChrome = path.join(this.nlmConfigDir, 'chrome-profiles', profileName);
if (isDir(nlmChrome)) return nlmChrome;
return null;
}
hasNlmProfiles() { return this.#listNlmProfiles().length > 0; }
isFirstRun() { return this.#listGemcliProfiles().length === 0; }
}
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 4 — Models (data classes)
// ═══════════════════════════════════════════════════════════════════════════
function stripPrefix(s, prefix) { return s.startsWith(prefix) ? s.slice(prefix.length) : s; }
function ensurePrefix(s, prefix) { if (!s) return s; return s.startsWith(prefix) ? s : prefix + s; }
export class RPCPayload {
constructor({ rpcId, payload = '[]', identifier = 'generic' }) {
this.rpcId = rpcId; this.payload = payload; this.identifier = identifier;
}
serialize() { return [this.rpcId, this.payload, null, this.identifier]; }
}
export class ConversationMetadata {
constructor({ cid = '', rid = '', rcid = '', extra } = {}) {
this.cid = stripPrefix(cid, 'c_'); this.rid = stripPrefix(rid, 'r_'); this.rcid = stripPrefix(rcid, 'rc_');
this.extra = extra ?? new Array(7).fill(null);
}
toList() {
const cid = this.cid ? ensurePrefix(this.cid, 'c_') : '';
const rid = this.rid ? ensurePrefix(this.rid, 'r_') : '';
const rcid = this.rcid ? ensurePrefix(this.rcid, 'rc_') : '';
const tail = [...this.extra];
while (tail.length < 7) tail.push(null);
if (tail.length > 7) tail.length = 7;
return [cid, rid, rcid, ...tail, ''];
}
static fromList(data) {
if (!Array.isArray(data)) return new ConversationMetadata();
return new ConversationMetadata({
cid: typeof data[0] === 'string' && data[0] ? data[0] : '',
rid: typeof data[1] === 'string' && data[1] ? data[1] : '',
rcid: typeof data[2] === 'string' && data[2] ? data[2] : '',
extra: data.length > 3 ? data.slice(3, 10) : new Array(7).fill(null),
});
}
}
export class Candidate {
constructor({ rcid = null, text = null, thoughts = null, webImages = null, generatedImages = null, generatedMusic = null, generatedVideo = null } = {}) {
this.rcid = rcid; this.text = text; this.thoughts = thoughts;
this.webImages = webImages; this.generatedImages = generatedImages;
this.generatedMusic = generatedMusic; this.generatedVideo = generatedVideo;
}
}
export class StreamResponse {
constructor({ metadata = null, candidates = [], text = null, thoughts = null, isComplete = false, serverModelHash = null, serverModelLabel = null } = {}) {
this.metadata = metadata ?? new ConversationMetadata(); this.candidates = candidates;
this.text = text; this.thoughts = thoughts; this.isComplete = isComplete;
this.serverModelHash = serverModelHash; this.serverModelLabel = serverModelLabel;
}
get hasImages() { return this.candidates.some(c => c.generatedImages?.length > 0); }
get hasMusic() { return this.candidates.some(c => c.generatedMusic?.length > 0); }
get hasVideo() { return this.candidates.some(c => c.generatedVideo?.length > 0); }
}
export class BatchResult {
constructor({ rpcId = null, data = null, raw = null } = {}) { this.rpcId = rpcId; this.data = data; this.raw = raw; }
}
export const ZRPCPayload = z.object({ rpcId: z.string(), payload: z.string().default('[]'), identifier: z.string().default('generic') });
export const ZConversationMetadata = z.object({ cid: z.string().default(''), rid: z.string().default(''), rcid: z.string().default(''), extra: z.array(z.unknown()).default(() => new Array(7).fill(null)) });
export const ZCandidate = z.object({ rcid: z.string().nullable().optional(), text: z.string().nullable().optional(), thoughts: z.string().nullable().optional(), webImages: z.array(z.unknown()).nullable().optional(), generatedImages: z.array(z.unknown()).nullable().optional(), generatedMusic: z.array(z.unknown()).nullable().optional(), generatedVideo: z.array(z.unknown()).nullable().optional() });
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 5 — Auth
// ═══════════════════════════════════════════════════════════════════════════
export class AuthTokens {
constructor(snlm0e = '', cfb2h = '', fdrfje = '') { this.snlm0e = snlm0e; this.cfb2h = cfb2h; this.fdrfje = fdrfje; }
}
export class AuthState {
constructor({ cookies = {}, tokens = new AuthTokens(), chromeProfilePath = null, lastRefreshed = 0, email = '' } = {}) {
this.cookies = cookies; this.tokens = tokens; this.chromeProfilePath = chromeProfilePath;
this.lastRefreshed = lastRefreshed; this.email = email;
}
toDict() {
const d = { cookies: this.cookies, tokens: { snlm0e: this.tokens.snlm0e, cfb2h: this.tokens.cfb2h, fdrfje: this.tokens.fdrfje }, chrome_profile_path: this.chromeProfilePath, last_refreshed: this.lastRefreshed };
if (this.email) d.email = this.email; return d;
}
static fromDict(data) {
const tokensData = data.tokens ?? {};
return new AuthState({
cookies: data.cookies ?? {}, tokens: new AuthTokens(tokensData.snlm0e ?? '', tokensData.cfb2h ?? '', tokensData.fdrfje ?? ''),
chromeProfilePath: data.chrome_profile_path ?? null, lastRefreshed: data.last_refreshed ?? 0, email: data.email ?? '',
});
}
get isValid() { return Boolean(this.cookies[COOKIE_1PSID] && this.tokens.snlm0e); }
}
export function extractTokens(html) {
const tokens = new AuthTokens();
for (const [name, pattern] of Object.entries(TOKEN_PATTERNS)) {
const m = html.match(pattern);
if (m) tokens[name] = m[1];
}
if (!tokens.snlm0e) throw new AuthError('Failed to extract SNlM0e access token from Gemini page. Cookies may be expired or invalid.');
return tokens;
}
export function extractEmail(html) { const m = html.match(EMAIL_PATTERN); return m ? m[1] : ''; }
export class CookieJar {
constructor(cookies = {}) {
this._store = new Map();
if (cookies && typeof cookies === 'object') {
for (const [k, v] of Object.entries(cookies)) {
if (typeof v === 'string') this._store.set(k, v);
}
}
}
set(name, value, attrs = {}) { this._store.set(name, String(value)); return this; }
get(name) { return this._store.get(name) ?? null; }
toObject() { const o = {}; for (const [k, v] of this._store) o[k] = v; return o; }
toHeader() { return Array.from(this._store.entries()).map(([k, v]) => `${k}=${v}`).join('; '); }
toArray() { return Array.from(this._store.entries()).map(([k, v]) => ({ name: k, value: v, domain: COOKIE_DOMAIN, path: '/', secure: true, httpOnly: true, session: false })); }
attachTo(client) {
const self = this;
client.interceptors.request.use(config => {
const existing = config.headers.cookie || config.headers.Cookie || '';
const our = self.toHeader();
config.headers.cookie = existing ? `${existing}; ${our}` : our;
return config;
});
client.interceptors.response.use(response => {
const setCookie = response.headers['set-cookie'];
if (setCookie) {
const arr = Array.isArray(setCookie) ? setCookie : [setCookie];
for (const raw of arr) {
const eq = raw.indexOf('=');
const semi = raw.indexOf(';');
if (eq > 0) {
const name = raw.slice(0, eq).trim();
const value = semi > eq ? raw.slice(eq + 1, semi).trim() : raw.slice(eq + 1).trim();
if (name && value) self._store.set(name, value);
}
}
}
return response;
});
}
}
export async function fetchTokens(cookies, proxy = null) {
const jar = new CookieJar(cookies);
const client = axios.create({ timeout: 30_000, headers: { ...GEMINI_HEADERS }, maxRedirects: 5, proxy: proxy ? parseAxiosProxy(proxy) : false, validateStatus: () => true });
jar.attachTo(client);
const res = await client.get(Endpoint.INIT);
if (res.status >= 400) throw new AuthError(`Gemini init page returned ${res.status} — cookies may be invalid.`);
const html = String(res.data ?? '');
return { tokens: extractTokens(html), cookies: jar.toObject(), email: extractEmail(html) };
}
export async function rotateCookies(cookies, proxy = null) {
const jar = new CookieJar(cookies);
const client = axios.create({ timeout: 30_000, headers: { ...ROTATE_COOKIES_HEADERS }, proxy: proxy ? parseAxiosProxy(proxy) : false, validateStatus: () => true });
jar.attachTo(client);
const res = await client.post(Endpoint.ROTATE_COOKIES, ROTATE_COOKIES_BODY);
if (res.status === 401) throw new AuthError('Cookie rotation returned 401. Full re-authentication required.');
if (res.status >= 400) throw new AuthError(`Cookie rotation failed: HTTP ${res.status}`);
const updated = jar.toObject();
if (updated[COOKIE_1PSIDTS] && updated[COOKIE_1PSIDTS] !== cookies[COOKIE_1PSIDTS]) return updated;
return null;
}
function parseAxiosProxy(proxy) {
try {
const u = new URL(proxy);
const port = u.port ? parseInt(u.port, 10) : (u.protocol === 'https:' ? 443 : 80);
const out = { protocol: u.protocol.replace(':', ''), host: u.hostname, port };
if (u.username) out.auth = { username: decodeURIComponent(u.username), password: decodeURIComponent(u.password ?? '') };
return out;
} catch { return false; }
}
export class AuthManager {
#state = null;
constructor(profileManager, profileName = null, proxy = null) { this.profileManager = profileManager; this.profileName = profileName; this.proxy = proxy; }
get state() { if (!this.#state) throw new AuthError('AuthManager not initialized. Call load() first.'); return this.#state; }
load() { const data = this.profileManager.loadAuth(this.profileName ?? undefined); this.#state = data && Object.keys(data).length > 0 ? AuthState.fromDict(data) : new AuthState(); return this.#state; }
save() { if (this.#state) this.profileManager.saveAuth(this.#state.toDict(), this.profileName ?? undefined); }
async refreshTokens() {
if (!this.state.cookies[COOKIE_1PSID] && !this.state.cookies['__Secure-3PSID']) throw new AuthError('No cookies available. Provide cookies first.');
logger.debug('auth: refreshing tokens from Gemini app page…');
const { tokens, cookies, email } = await fetchTokens(this.state.cookies, this.proxy);
this.state.tokens = tokens;
if (email) this.state.email = email;
for (const [k, v] of Object.entries(cookies)) this.state.cookies[k] = v;
this.state.lastRefreshed = Date.now() / 1000;
this.save(); logger.debug('auth: tokens refreshed.'); return tokens;
}
async refreshCookies() {
const elapsed = Date.now() / 1000 - this.state.lastRefreshed;
if (elapsed < COOKIE_REFRESH_COOLDOWN) { logger.debug(`auth: cookie-refresh cooldown ${Math.round(COOKIE_REFRESH_COOLDOWN - elapsed)}s remaining`); return false; }
try {
const updated = await rotateCookies(this.state.cookies, this.proxy);
if (updated) { this.state.cookies = updated; this.state.lastRefreshed = Date.now() / 1000; this.save(); logger.debug('auth: cookies refreshed via RotateCookies.'); return true; }
} catch (err) { if (err instanceof AuthError) { logger.warn('auth: cookie rotation failed (401). Full re-auth needed.'); throw err; } logger.warn(`auth: cookie rotation failed: ${err.message}`); }
return false;
}
async recover() {
try { await this.refreshTokens(); if (this.state.isValid) return this.state; } catch { logger.debug('auth: layer 1 (token refresh) failed.'); }
try { this.load(); if (this.state.isValid) { logger.debug('auth: layer 2 (disk reload) succeeded.'); return this.state; } } catch { logger.debug('auth: layer 2 (disk reload) failed.'); }
throw new AuthError('All recovery layers exhausted. Re-paste your Gemini cookies via POST /api/gemini/cookies.');
}
saveCookies(cookies) { if (!this.#state) this.#state = new AuthState(); this.#state.cookies = { ...cookies }; this.#state.lastRefreshed = Date.now() / 1000; this.save(); }
}
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 6 — Parser
// ═══════════════════════════════════════════════════════════════════════════
const LENGTH_MARKER_RE = /^(\d+)\n/;
function utf16CharCount(s, start, utf16Units) {
const remaining = s.length - start;
if (remaining < utf16Units) return [remaining, remaining];
return [utf16Units, utf16Units];
}
export function stripJsonpPrefix(text) {
if (text.startsWith(")]}'")) return text.slice(4).replace(/^\n+/, '');
return text;
}
export function parseFrames(content) {
let pos = 0;
const total = content.length;
const frames = [];
while (pos < total) {
while (pos < total && /\s/.test(content[pos])) pos++;
if (pos >= total) break;
const slice = content.slice(pos);
const m = slice.match(LENGTH_MARKER_RE);
if (!m) break;
const frameLength = parseInt(m[1], 10);
const contentStart = pos + m[1].length;
const [charCount] = utf16CharCount(content, contentStart, frameLength);
const endPos = contentStart + charCount;
const chunk = content.slice(contentStart, endPos).trim();
pos = endPos;
if (!chunk) continue;
try {
const parsed = JSON.parse(chunk);
if (Array.isArray(parsed)) frames.push(...parsed); else frames.push(parsed);
} catch { logger.debug(`Failed to parse frame JSON: ${chunk.slice(0, 100)}...`); }
}
return [frames, content.slice(pos)];
}
export function extractJsonFromResponse(text) {
let content = stripJsonpPrefix(text).replace(/^\s+/, '');
const [frames] = parseFrames(content);
if (frames.length > 0) return frames;
try { const parsed = JSON.parse(content.trim()); return Array.isArray(parsed) ? parsed : [parsed]; } catch { }
const collected = [];
for (const rawLine of content.trim().split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) continue;
try { const parsed = JSON.parse(line); if (Array.isArray(parsed)) collected.push(...parsed); else if (parsed !== null && typeof parsed === 'object') collected.push(parsed); } catch { continue; }
}
if (collected.length > 0) return collected;
throw new Error('Could not parse any valid JSON from the response.');
}
export function getNested(data, path, defaultValue = null) {
let current = data;
for (const key of path) {
try {
if (typeof key === 'number') {
if (Array.isArray(current)) {
if (key >= -current.length && key < current.length) { current = current[key < 0 ? current.length + key : key]; }
else if (current.length === 1 && current[0] !== null && typeof current[0] === 'object' && !Array.isArray(current[0])) { const pk = String(key + 1); if (pk in current[0]) current = current[0][pk]; else return defaultValue; }
else return defaultValue;
} else if (current !== null && typeof current === 'object' && !Array.isArray(current)) { const pk = String(key + 1); if (pk in current) current = current[pk]; else return defaultValue; }
else return defaultValue;
} else { if (current !== null && typeof current === 'object' && key in current) current = current[key]; else return defaultValue; }
} catch { return defaultValue; }
}
return (current ?? defaultValue);
}
export function parseBatchexecuteResponse(text) {
const frames = extractJsonFromResponse(text);
const results = [];
for (const frame of frames) {
if (!Array.isArray(frame)) continue;
if (frame.length >= 3 && frame[0] === 'wrb.fr') {
const rawJson = frame[2];
let data = null;
if (rawJson && typeof rawJson === 'string') { try { data = JSON.parse(rawJson); } catch { data = rawJson; } }
results.push({ rpc_id: frame[1], data, raw: rawJson });
}
}
return results;
}
const KNOWN_MODEL_HASHES = new Set(['56fdd199312815e2', 'e051ce1aa80aa576', 'e6fa609c3fa255c0', 'fbb127bbb056c959', '9d8ca3786ebdfbea', '5bf011840784117a']);
const KNOWN_MODEL_LABELS = new Set(['Fast', 'Thinking', 'Pro']);
function extractServerModel(data, depth = 0) {
if (depth > 10 || !Array.isArray(data)) return [null, null];
for (let i = 0; i < data.length; i++) {
const item = data[i];
if (typeof item === 'string' && item.length === 16 && KNOWN_MODEL_HASHES.has(item)) {
let label = null;
for (let off = 1; off <= 5; off++) {
if (i + off < data.length) { const v = data[i + off]; if (typeof v === 'string' && KNOWN_MODEL_LABELS.has(v)) { label = v; break; } }
}
return [item, label];
}
}
for (const item of data) { if (Array.isArray(item)) { const [h, l] = extractServerModel(item, depth + 1); if (h) return [h, l]; } }
return [null, null];
}
export function parseStreamResponse(text) {
const frames = extractJsonFromResponse(text);
const results = [];
for (const frame of frames) {
if (!Array.isArray(frame)) continue;
if (frame.length < 3 || frame[0] !== 'wrb.fr') continue;
const rawInner = frame[2];
if (!rawInner || typeof rawInner !== 'string') continue;
let inner;
try { inner = JSON.parse(rawInner); } catch { continue; }
if (!Array.isArray(inner)) continue;
const result = { metadata: getNested(inner, [1]), candidates: [], text: null, thoughts: null, completion: getNested(inner, [25]), server_model_hash: null, server_model_label: null };
const candidatesData = getNested(inner, [4]);
if (Array.isArray(candidatesData)) {
for (const candidate of candidatesData) {
if (!Array.isArray(candidate)) continue;
result.candidates.push({
rcid: getNested(candidate, [0]), text: getNested(candidate, [1, 0]), thoughts: getNested(candidate, [37, 0, 0]),
web_images: getNested(candidate, [12, 1]), generated_images: getNested(candidate, [12, 7, 0]),
generated_music: getNested(candidate, [12, 86]), generated_video: getNested(candidate, [12, 59]),
});
}
if (result.candidates.length > 0) { result.text = result.candidates[0].text; result.thoughts = result.candidates[0].thoughts; }
}
const [hash, label] = extractServerModel(inner);
if (hash) { result.server_model_hash = hash; result.server_model_label = label; }
results.push(result);
}
return results;
}
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 7 — RPC Transport
// ═══════════════════════════════════════════════════════════════════════════
export function buildBatchexecuteBody(payloads, accessToken) {
const serialized = [payloads.map(p => p.serialize())];
const params = new URLSearchParams();
params.set('f.req', JSON.stringify(serialized));
params.set('at', accessToken);
return params.toString() + '&';
}
export function buildBatchexecuteUrl({ rpcIds, reqid, buildLabel, sessionId, sourcePath = '/app' }) {
const params = new URLSearchParams();
params.set('rpcids', rpcIds.join(','));
params.set('source-path', sourcePath);
if (buildLabel) params.set('bl', buildLabel);
if (sessionId) params.set('f.sid', sessionId);
params.set('hl', 'en');
params.set('_reqid', String(reqid));
params.set('rt', 'c');
return `${Endpoint.BATCH_EXEC}?${params.toString()}`;
}
export function buildStreamgenerateBody({ prompt, accessToken, metadata, fileData, gemId, toolId, stylePreset, botguardToken, botguardHash }) {
const inner = new Array(INNER_REQ_LIST_SIZE).fill(null);
const innerZero = [prompt, 0, null, fileData ?? null, null, null, 0];
if (stylePreset) { try { logger.debug('musicPresets module not yet ported; ignoring stylePreset'); } catch { } }
inner[0] = innerZero; inner[1] = ['en'];
if (metadata) inner[2] = metadata.toList(); else inner[2] = ['', '', '', null, null, null, null, null, null, null];
if (botguardToken) inner[3] = botguardToken;
if (botguardHash) inner[4] = botguardHash;
inner[6] = [1]; inner[7] = 1; inner[10] = 1; inner[11] = 0; inner[17] = [[0]]; inner[18] = 0;
if (gemId) inner[19] = gemId;
inner[27] = 1; inner[30] = [4]; inner[41] = [1];
if (toolId !== null && toolId !== undefined) inner[49] = toolId;
inner[53] = 0; inner[59] = randomUUID().toUpperCase(); inner[61] = [];
const nowMs = Date.now(); inner[66] = [Math.floor(nowMs / 1000), (nowMs % 1000) * 1_000_000]; inner[68] = 2;
const params = new URLSearchParams();
params.set('f.req', JSON.stringify([null, JSON.stringify(inner)]));
params.set('at', accessToken);
return params.toString() + '&';
}
export function buildStreamgenerateUrl({ reqid, buildLabel, sessionId }) {
const params = new URLSearchParams();
if (buildLabel) params.set('bl', buildLabel);
if (sessionId) params.set('f.sid', sessionId);
params.set('hl', 'en');
params.set('_reqid', String(reqid));
params.set('rt', 'c');
return `${Endpoint.GENERATE}?${params.toString()}`;
}
export function buildHeaders(model, cid, tempSession = null) {
const headers = { ...GEMINI_HEADERS };
if (tempSession) {
const { clientUUID, sessionUUID } = tempSession;
headers[MODEL_HEADER_KEY] = `[1,null,null,null,"${TEMP_MODEL_HASH}",null,null,1,[4],null,null,1,null,null,1,null,"${clientUUID}"]`;
headers[SESSION_HEADER_KEY] = `["${sessionUUID}",1]`;
headers[TRACKING_HEADER_KEY] = '[0]'; headers[SAFETY_HEADER_KEY] = '[0,0,0]';
} else if (model && Object.keys(model.header).length > 0) { Object.assign(headers, model.header); }
if (cid) { const bare = cid.startsWith('c_') ? cid.slice(2) : cid; headers.Referer = `https://gemini.google.com/app/${bare}`; }
return headers;
}
export function buildSyncStateBody(accessToken) {
const arr = new Array(SYNC_STATE_NULL_COUNT).fill(null);
arr[SYNC_STATE_NULL_COUNT - 1] = 5;
const payload = JSON.stringify([arr, [['popup_zs_visits_cooldown']]]);
const rpc = new RPCPayload({ rpcId: RPC.SYNC_STATE, payload });
return buildBatchexecuteBody([rpc], accessToken);
}
export function buildStreamResponse(responseText) {
const parsedFrames = parseStreamResponse(responseText);
const result = new StreamResponse();
const prevMedia = new Map();
for (const frame of parsedFrames) {
if (frame.metadata) result.metadata = ConversationMetadata.fromList(frame.metadata);
if (frame.candidates?.length > 0) {
const newCandidates = [];
for (let i = 0; i < frame.candidates.length; i++) {
const c = frame.candidates[i];
const prev = prevMedia.get(i) ?? { images: null, music: null, video: null };
const candidate = new Candidate({
rcid: c.rcid, text: c.text, thoughts: c.thoughts, webImages: c.web_images,
generatedImages: c.generated_images || prev.images,
generatedMusic: c.generated_music || prev.music,
generatedVideo: c.generated_video || prev.video,
});
newCandidates.push(candidate);
prevMedia.set(i, { images: candidate.generatedImages, music: candidate.generatedMusic, video: candidate.generatedVideo });
}
result.candidates = newCandidates;
}
if (frame.text) result.text = frame.text;
if (frame.thoughts) result.thoughts = frame.thoughts;
if (frame.completion !== null && frame.completion !== undefined) result.isComplete = true;
if (frame.server_model_hash) { result.serverModelHash = frame.server_model_hash; result.serverModelLabel = frame.server_model_label; }
}
return result;
}
function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
export class RPCTransport {
#reqid;
constructor({ client, accessToken, buildLabel = null, sessionId = null }) {
this.client = client; this.accessToken = accessToken; this.buildLabel = buildLabel; this.sessionId = sessionId;
this.#reqid = randomInt(10000, 99999);
}
#nextReqid() { const c = this.#reqid; this.#reqid += REQID_INCREMENT; return c; }
async batchexecute(payloads, sourcePath = '/app') {
const rpcIds = payloads.map(p => p.rpcId);
const url = buildBatchexecuteUrl({ rpcIds, reqid: this.#nextReqid(), buildLabel: this.buildLabel, sessionId: this.sessionId, sourcePath });
const body = buildBatchexecuteBody(payloads, this.accessToken);
const cidFromPath = sourcePath.startsWith('/app/') ? sourcePath.slice('/app/'.length) : null;
const headers = { ...buildHeaders(null, cidFromPath), ...BATCH_EXEC_HEADERS };
logger.debug(`batchexecute: ${rpcIds.join(',')}`);
const response = await this.client.post(url, body, { headers, responseType: 'text', transformResponse: [d => d] });
if (response.status >= 400) throw new Error(`batchexecute HTTP ${response.status}: ${String(response.data).slice(0, 200)}`);
return parseBatchexecuteResponse(String(response.data)).map(r => new BatchResult({ rpcId: r.rpc_id, data: r.data, raw: r.raw }));
}
async syncState() {
const url = buildBatchexecuteUrl({ rpcIds: [RPC.SYNC_STATE], reqid: this.#nextReqid(), buildLabel: this.buildLabel, sessionId: this.sessionId });
const body = buildSyncStateBody(this.accessToken);
const headers = { ...GEMINI_HEADERS, [TRACKING_HEADER_KEY]: '[0]' };
logger.debug('syncState: calling L5adhe for temp chat session');
const response = await this.client.post(url, body, { headers, responseType: 'text', transformResponse: [d => d] });
if (response.status >= 400) logger.warn(`syncState: HTTP ${response.status} — continuing anyway`);
return response;
}
async streamGenerate(opts) {
const tempSession = opts.temp ?? null;
if (tempSession) await this.syncState().catch(e => logger.warn(`syncState failed: ${e.message}`));
const url = buildStreamgenerateUrl({ reqid: this.#nextReqid(), buildLabel: this.buildLabel, sessionId: this.sessionId });
const body = buildStreamgenerateBody({ prompt: opts.prompt, accessToken: this.accessToken, metadata: opts.metadata, fileData: opts.fileData, gemId: opts.gemId, toolId: opts.toolId, stylePreset: opts.stylePreset });
const cid = opts.metadata?.cid ?? null;
const headers = buildHeaders(opts.model ?? DEFAULT_MODEL, cid, tempSession);
logger.debug(`StreamGenerate${tempSession ? ' [TEMP]' : ''}: ${opts.prompt.slice(0, 50)}...`);
const { onDelta } = opts;
if (onDelta) {
const response = await this.client.post(url, body, { headers, responseType: 'stream', timeout: 120000 });
if (response.status >= 400) throw new Error(`StreamGenerate HTTP ${response.status}`);
return this._streamFrames(response.data, onDelta);
}
const response = await this.client.post(url, body, { headers, responseType: 'text', transformResponse: [d => d] });
if (response.status >= 400) throw new Error(`StreamGenerate HTTP ${response.status}: ${String(response.data).slice(0, 200)}`);
return buildStreamResponse(String(response.data));
}
async _streamFrames(stream, onDelta) {
let buffer = '';
let chunkCount = 0;
let frameCount = 0;
let totalDeltaChars = 0;
let loggedFrames = 0;
let prevText = '';
const result = { metadata: null, cid: null, rid: null, rcid: null, serverModelLabel: null };
const emitInner = (inner) => {
frameCount++;
if (loggedFrames < 3) {
const textPreview = JSON.stringify(getNested(inner, [4, 0, 1, 0])).slice(0, 80);
const candLen = Array.isArray(getNested(inner, [4])) ? getNested(inner, [4]).length : 0;
logger.debug(`_streamFrames: frame #${frameCount} inner_keys=${Object.keys(inner).length} inner[4] len=${candLen} text=${textPreview} inner[25]=${JSON.stringify(getNested(inner, [25]))}`);
loggedFrames++;
}
if (!result.metadata) {
const metaList = getNested(inner, [1]);
if (Array.isArray(metaList)) {
try { result.metadata = ConversationMetadata.fromList(metaList); } catch {}
}
}
const text = getNested(inner, [4, 0, 1, 0]);
if (text && text !== prevText) {
const delta = text.slice(prevText.length);
prevText = text;
totalDeltaChars += delta.length;
if (delta) onDelta(delta);
}
if (!result.serverModelLabel) {
const [, label] = extractServerModel(inner);
if (label) result.serverModelLabel = label;
}
};
const tryParseWrb = (frame) => {
if (!Array.isArray(frame) || frame.length < 3 || frame[0] !== 'wrb.fr') return;
const rawInner = frame[2];
if (!rawInner || typeof rawInner !== 'string') return;
let inner;
try { inner = JSON.parse(rawInner); } catch { return; }
if (!Array.isArray(inner)) return;
emitInner(inner);
};
const consume = () => {
buffer = stripJsonpPrefix(buffer);
let consumed;
// 1) Try length-prefixed frames
do {
consumed = false;
buffer = buffer.replace(/^\s+/, '');
if (!buffer) break;
const m = buffer.match(LENGTH_MARKER_RE);
if (!m) break;
const frameLength = parseInt(m[1], 10);
const contentStart = m.index + m[0].length;
const remaining = buffer.slice(contentStart);
const [charCount] = utf16CharCount(remaining, 0, frameLength);
if (remaining.length < charCount) break;
let frameJson;
try { frameJson = JSON.parse(remaining.slice(0, charCount)); } catch { break; }
buffer = remaining.slice(charCount);
consumed = true;
if (Array.isArray(frameJson)) {
for (const f of frameJson) tryParseWrb(f);
}
} while (consumed);
// 2) Try newline-delimited JSON on remainder
if (buffer.trim()) {
const lines = buffer.split(/\r?\n/);
const remaining = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const parsed = JSON.parse(trimmed);
if (Array.isArray(parsed)) {
for (const f of parsed) tryParseWrb(f);
} else {
remaining.push(line);
}
} catch {
remaining.push(line);
}
}
buffer = remaining.join('\n');
}
};
for await (const chunk of stream) {
chunkCount++;
const chunkStr = chunk.toString();
if (chunkCount <= 3) logger.debug(`_streamFrames: chunk #${chunkCount} size=${chunkStr.length} first100=${chunkStr.slice(0, 100)}`);
buffer += chunkStr;
consume();
}
consume(); // final pass
logger.debug(`_streamFrames: done — ${chunkCount} chunks, ${frameCount} frames, ${totalDeltaChars} chars streamed, prevText="${prevText.slice(0, 100)}"`);
if (result.metadata) {
result.cid = result.metadata.cid ?? null;
result.rid = result.metadata.rid ?? null;
result.rcid = result.metadata.rcid ?? null;
}
return result;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 8 — Upload
// ═══════════════════════════════════════════════════════════════════════════
const UPLOAD_ENDPOINT = 'https://push.clients6.google.com/upload/';
const PUSH_ID_UPLOAD = 'feeds/mcudyrk2a4khkz';
const X_TENANT_ID = 'bard-storage';
const X_CLIENT_DATA = 'CJvoygE=';
const X_CLIENT_PCTX = 'CgcSBWjK7pYx';
function formatCookieHeader(cookies) {
return Object.entries(cookies).filter(([, v]) => v !== undefined && v !== null).map(([k, v]) => `${k}=${v}`).join('; ');
}
function makeUploadClient() { return axios.create({ timeout: 60_000, maxRedirects: 5, validateStatus: () => true }); }
export async function uploadToContentPush(_client, cookies, content, mimeType, filename) {
const cookieHeader = formatCookieHeader(cookies);
if (!cookieHeader) { logger.warn('upload: no cookies available — cannot authenticate upload'); return null; }
const http = makeUploadClient();
const step1Headers = {
'accept': '*/*', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 'push-id': PUSH_ID_UPLOAD,
'x-goog-upload-command': 'start', 'x-goog-upload-protocol': 'resumable',
'x-goog-upload-header-content-length': content.length.toString(), 'x-tenant-id': X_TENANT_ID,
'x-client-data': X_CLIENT_DATA, 'x-client-pctx': X_CLIENT_PCTX, 'cookie': cookieHeader,
'origin': 'https://gemini.google.com', 'referer': 'https://gemini.google.com/',
};
let uploadUrl;
try {
logger.debug(`upload: step1 start — size=${content.length} mime=${mimeType} file=${filename}`);
const r1 = await http.post(UPLOAD_ENDPOINT, `File name: ${filename}`, { headers: step1Headers, responseType: 'text', maxRedirects: 0 });
if (r1.status !== 200) { logger.warn(`upload: step1 failed status=${r1.status} body="${String(r1.data).slice(0, 200)}"`); return null; }
uploadUrl = r1.headers['x-goog-upload-url'] ?? '';
if (!uploadUrl) { logger.warn('upload: step1 OK but no x-goog-upload-url header.'); return null; }
logger.debug(`upload: step1 OK — upload_url=${uploadUrl.slice(0, 80)}…`);
} catch (e) { logger.warn(`upload: step1 threw: ${e.message}`); return null; }
const step2Headers = {
'accept': '*/*', 'content-type': mimeType, 'push-id': PUSH_ID_UPLOAD,
'x-goog-upload-command': 'upload, finalize', 'x-goog-upload-offset': '0',
'x-tenant-id': X_TENANT_ID, 'x-client-data': X_CLIENT_DATA, 'x-client-pctx': X_CLIENT_PCTX,
'cookie': cookieHeader, 'origin': 'https://gemini.google.com', 'referer': 'https://gemini.google.com/',
};
try {
logger.debug(`upload: step2 upload,finalize — url=${uploadUrl.slice(0, 80)}…`);
const r2 = await http.post(uploadUrl, content, { headers: step2Headers, responseType: 'text', maxRedirects: 5 });
const body = (r2.data ?? '');
logger.debug(`upload: step2 status=${r2.status} body="${body.slice(0, 120)}"`);
if (r2.status === 200 && body.trim().startsWith('/')) { logger.info(`upload: success → ${body.trim().slice(0, 80)}`); return body.trim(); }
logger.warn(`upload: step2 failed status=${r2.status} body="${body.slice(0, 200)}"`); return null;
} catch (e) { logger.warn(`upload: step2 threw: ${e.message}`); return null; }
}
export function buildFileDataItem(storagePath, mimeType, filename) {
return [[storagePath, 1, null, mimeType], filename, null, null, null, null, null, null, [0]];
}
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 9 — Temporary (no-auth) Chat
// ═══════════════════════════════════════════════════════════════════════════
const GEMINI_HOST = 'gemini.google.com';
const APP_PATH = '/app';
const STREAM_PATH = '/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate';
const LANG = 'en-US';
function httpsGet(opts) {
return new Promise((resolve, reject) => {
const req = https.request({ method: 'GET', ...opts }, res => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
});
req.on('error', reject);
req.end();
});
}
function httpsPost(opts, bodyStr) {
return new Promise((resolve, reject) => {
const req = https.request({ method: 'POST', ...opts }, res => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
});
req.on('error', reject);
req.write(bodyStr);
req.end();
});
}
function extractWiz(html) {
const marker = 'WIZ_global_data = ';
const start = html.indexOf(marker);
if (start < 0) return {};
const jsonStart = start + marker.length;
let depth = 0, i = jsonStart, end = -1;
while (i < html.length) {
if (html[i] === '{') depth++;
else if (html[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } }
i++;
}
try { return end > 0 ? JSON.parse(html.slice(jsonStart, end)) : {}; } catch { return {}; }
}
function extractCookies(setCookieHeader) {
if (!setCookieHeader) return '';
const arr = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
return arr.map(c => c.split(';')[0]).join('; ');
}
async function fetchSession() {
logger.debug('tempChat: fetching anonymous session…');
const res = await httpsGet({
hostname: GEMINI_HOST, path: APP_PATH,
headers: { 'accept': 'text/html', 'accept-language': LANG, 'user-agent': CHROME_USER_AGENT },
});
const wiz = extractWiz(res.body);
const fsid = wiz['FdrFJe'];
const bl = wiz['cfb2h'];
const cookies = extractCookies(res.headers['set-cookie']);
if (!fsid || !bl) logger.warn('tempChat: could not extract f.sid / bl from page — proceeding without');
logger.debug(`tempChat: session acquired (f.sid=${fsid?.slice(0, 8)}…, bl=${bl})`);
return { fsid, bl, cookies };
}
let _reqid = Math.floor(Math.random() * 90000) + 10000;
function nextReqid() { const r = _reqid; _reqid += REQID_INCREMENT; return r; }
function buildNoAuthBody(prompt) {
const inner = new Array(INNER_REQ_LIST_SIZE).fill(null);
inner[0] = [prompt, 0, null, null, null, null, 0];
inner[1] = [LANG];
inner[2] = ['', '', '', null, null, null, null, null, null, ''];
inner[6] = [1]; inner[7] = 1; inner[10] = 1; inner[11] = 0; inner[17] = [[0]]; inner[18] = 0;
inner[27] = 1; inner[30] = [4]; inner[41] = [1]; inner[53] = 0;
inner[59] = randomUUID().toUpperCase(); inner[61] = []; inner[68] = 2;
return 'f.req=' + encodeURIComponent(JSON.stringify([null, JSON.stringify(inner)]));
}
function buildNoAuthHeaders(cookies) {
const uuid = randomUUID().toUpperCase();
return {
'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 'user-agent': CHROME_USER_AGENT,
'accept-language': LANG, 'origin': 'https://gemini.google.com', 'referer': 'https://gemini.google.com/',
'x-same-domain': '1',
[MODEL_HEADER_KEY]: `[1,null,null,null,"${TEMP_MODEL_HASH}",null,null,0,[4],null,null,1,null,null,1,null,"${uuid}"]`,
[SESSION_HEADER_KEY]: `["${uuid}",1]`, [TRACKING_HEADER_KEY]: '[0]', [SAFETY_HEADER_KEY]: '[0,0,0]',
...(cookies ? { cookie: cookies } : {}),
};
}
export class TempChat {
#session = null;
async #getSession() { if (!this.#session) this.#session = await fetchSession(); return this.#session; }
async send(prompt) {
const { fsid, bl, cookies } = await this.#getSession();
const reqid = nextReqid();
const body = buildNoAuthBody(prompt);
const headers = buildNoAuthHeaders(cookies);
const params = new URLSearchParams();
if (bl) params.set('bl', bl);
if (fsid) params.set('f.sid', fsid);
params.set('hl', 'en-US'); params.set('_reqid', String(reqid)); params.set('rt', 'c');
const path = `${STREAM_PATH}?${params.toString()}`;
headers['content-length'] = String(Buffer.byteLength(body));
logger.debug(`tempChat: StreamGenerate [no-auth] ${prompt.slice(0, 50)}…`);
const res = await httpsPost({ hostname: GEMINI_HOST, path, headers }, body);
if (res.status >= 400) { this.#session = null; throw new Error(`tempChat: StreamGenerate HTTP ${res.status}: ${res.body.slice(0, 150)}`); }
logger.debug(`tempChat: received ${res.body.length} bytes`);
const parsed = buildStreamResponse(res.body);
return parsed.text ?? parsed.candidates?.[0]?.text ?? '';
}
async refresh() { this.#session = await fetchSession(); }
async close() {}
}
let _tempInstance = null;
export function getTempChat() { if (!_tempInstance) _tempInstance = new TempChat(); return _tempInstance; }
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 10 — Cookie Manager
// ═══════════════════════════════════════════════════════════════════════════
const __filename_cm = fileURLToPath(import.meta.url);
const __dirname_cm = path.dirname(__filename_cm);
export const COOKIES_JSON_PATH = path.resolve(__dirname_cm, '../..', 'data/gemini.json');
export const LOCAL_COOKIES_PATH = COOKIES_JSON_PATH;
if (!fs.existsSync(COOKIES_JSON_PATH)) {
fs.writeFileSync(COOKIES_JSON_PATH, '[]', 'utf-8');
}
export const AUTH_COOKIE_KEYS = [
'__Secure-1PSID', '__Secure-1PSIDTS', '__Secure-1PSIDCC', '__Secure-1PAPISID',
'__Secure-3PSID', '__Secure-3PSIDTS', '__Secure-3PSIDCC', '__Secure-3PAPISID',
'SID', 'HSID', 'SSID', 'APISID', 'SAPISID', 'SIDCC', 'AEC', 'NID',
'1P_JAR', '__Secure-ENID', 'SEARCH_SAMESITE',
];
export const CRITICAL_COOKIES = ['__Secure-1PSID', '__Secure-1PSIDTS'];
export const ALT_CRITICAL = ['__Secure-3PSID', '__Secure-3PSIDTS'];
function activePath() { return COOKIES_JSON_PATH; }
export function isCookiesFileEmpty() {
if (!fs.existsSync(COOKIES_JSON_PATH)) return true;
try {
const data = JSON.parse(fs.readFileSync(COOKIES_JSON_PATH, 'utf-8'));
if (Array.isArray(data)) return data.length === 0;
if (data !== null && typeof data === 'object') return Object.keys(data).length === 0;
} catch { /* ignore */ }
return true;
}
export function parseCookies(source) {
if (source !== null && typeof source === 'object' && !Array.isArray(source)) {
const out = {};
for (const [k, v] of Object.entries(source)) {
if (typeof v === 'string') out[k] = v;
}
return out;
}
if (Array.isArray(source)) {
const result = {};
for (const item of source) {
if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
if (typeof item.name === 'string' && typeof item.value === 'string') {
result[item.name] = item.value;
}
}
}
return result;
}
if (typeof source === 'string') {
let s = source.trim()
.replace(/^(var|let|const)\s+\w+\s*=\s*/, '')
.replace(/;$/, '');
try { return parseCookies(JSON.parse(s)); } catch { /* fall through */ }
if (s.startsWith('{') && !s.startsWith('[')) {
try { return parseCookies(JSON.parse(`[${s}]`)); } catch { /* fall through */ }
}
const result = {};
for (const rawLine of s.split(/\r?\n/)) {
let line = rawLine.trim().replace(/^,/, '').replace(/,$/, '')
.replace(/^["']/, '').replace(/["']$/, '');
if (line.includes(':')) {
const idx = line.indexOf(':');
let k = line.slice(0, idx).trim().replace(/^["']/, '').replace(/["']$/, '');
let v = line.slice(idx + 1).trim().replace(/^["']/, '').replace(/["']$/, '');
if (k && v) result[k] = v;
} else if (line.includes('=')) {
const idx = line.indexOf('=');
const k = line.slice(0, idx).trim();
const v = line.slice(idx + 1).trim();
if (k && v) result[k] = v;
}
}
return result;
}
return {};
}
export function loadCookiesJson() {
const p = activePath();
if (!fs.existsSync(p)) return [];
try {
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
if (Array.isArray(data)) return data;
if (data !== null && typeof data === 'object') {
return Object.entries(data).map(([k, v]) => ({
name: k, value: String(v), domain: '.google.com', path: '/',
secure: true, httpOnly: true, session: false,
}));
}
} catch { /* ignore */ }
return [];
}
export function saveCookiesJson(cookies, targetPath) {
const target = targetPath ?? COOKIES_JSON_PATH;
fs.mkdirSync(path.dirname(target), { recursive: true });
let toWrite;
if (!Array.isArray(cookies)) {
toWrite = Object.entries(cookies).map(([k, v]) => ({
name: k, value: v, domain: '.google.com', path: '/',
secure: true, httpOnly: true, session: false,
}));
} else {
toWrite = cookies;
}
fs.writeFileSync(target, JSON.stringify(toWrite, null, 2));
}
export function mergeCookies(existing, newCookies) {
const index = {};
for (const c of existing) { if (c.name) index[c.name] = c; }
if (Array.isArray(newCookies)) {
for (const item of newCookies) {
if (item.name && item.value !== undefined) {
if (item.name in index) Object.assign(index[item.name], item);
else index[item.name] = item;
}
}
} else {
for (const [name, value] of Object.entries(newCookies)) {
if (name in index) index[name].value = value;
else index[name] = { name, value, domain: '.google.com', path: '/', secure: true, httpOnly: true, session: false };
}
}
return Object.values(index);
}
export function getFlatCookies(filterAuth = true) {
const raw = loadCookiesJson();
const flat = parseCookies(raw);
if (filterAuth) {
const filtered = {};
for (const [k, v] of Object.entries(flat)) {
if (AUTH_COOKIE_KEYS.includes(k)) filtered[k] = v;
}
return filtered;
}
return flat;
}
export function checkCookies() {
const flat = getFlatCookies(false);
const p = activePath();
const results = {};
for (const key of CRITICAL_COOKIES) {
const val = flat[key] ?? '';
results[key] = { present: !!val, value_preview: val.length > 28 ? val.slice(0, 28) + '...' : val };
}
const altResults = {};
for (const key of ALT_CRITICAL) {
const val = flat[key] ?? '';
altResults[key] = { present: !!val, value_preview: val.length > 28 ? val.slice(0, 28) + '...' : val };
}
const otherFound = AUTH_COOKIE_KEYS.filter(
k => !CRITICAL_COOKIES.includes(k) && !ALT_CRITICAL.includes(k) && k in flat
);
const primaryValid = Object.values(results).every(r => r.present);
const altValid = Object.values(altResults).every(r => r.present);
return {
critical: results, alt: altResults, other_auth_found: otherFound,
total_cookies: Object.keys(flat).length,
valid: primaryValid || altValid,
psid: flat['__Secure-1PSID'] || flat['__Secure-3PSID'] || '',
path: p,
jar_size: Object.keys(flat).length,
};
}
export function importCookiesFromRaw(rawText) {
const parsed = parseCookies(rawText);
if (Object.keys(parsed).length === 0)
return { ok: false, error: 'Could not parse any cookies from input' };
const psid = parsed['__Secure-1PSID'] || parsed['__Secure-3PSID'] || parsed.SID || '';
const psidts = parsed['__Secure-1PSIDTS'] || parsed['__Secure-3PSIDTS'] || '';
const existing = loadCookiesJson();
const merged = mergeCookies(existing, parsed);
saveCookiesJson(merged);
return {
ok: true,
psid_found: !!psid,
psidts_found: !!psidts,
total: Object.keys(parsed).length,
path: COOKIES_JSON_PATH,
preview: psid ? psid.slice(0, 20) + '...' : '(not found)',
};
}
export function getCookiesPath() { return activePath(); }
export function clearCookiesFile() {
const p = activePath();
if (!fs.existsSync(p)) return { ok: true, path: p, message: 'No cookies file found — nothing to delete.' };
fs.writeFileSync(p, '[]', 'utf-8');
return { ok: true, path: p, message: 'Cookies cleared.' };
}
export function discoverCookiesPath() {
return fs.existsSync(COOKIES_JSON_PATH) ? COOKIES_JSON_PATH : null;
}
export function importCookiesToProfile(auth) {
const flat = getFlatCookies(true);
if (Object.keys(flat).length === 0) return false;
auth.saveCookies(flat);
return true;
}
function cookiesFromJar(jar) {
const c = jar.toObject ? jar.toObject() : jar;
return Object.entries(c).map(([name, value]) => ({ name, value, domain: '.google.com', path: '/', secure: true }));
}
function cookiesFromState(state) {
return Object.entries(state.cookies || {}).map(([name, value]) => ({ name, value, domain: '.google.com', path: '/', secure: true }));
}
// ═══════════════════════════════════════════════════════════════════════════
// SECTION 11 — GeminiClient (main class)
// ═══════════════════════════════════════════════════════════════════════════
export class GeminiClient {
#httpClient = null;
#jar = null;
#transport = null;
#initialized = false;
constructor({ profileName = null, proxy = null, timeoutMs = 300_000 } = {}) {
this.profileManager = new ProfileManager();
this.authManager = new AuthManager(this.profileManager, profileName, proxy);
this.proxy = proxy;
this.timeoutMs = timeoutMs;
}
get transport() { if (!this.#transport) throw new AuthError('Client not initialized. Call init() first.'); return this.#transport; }
get initialized() { return this.#initialized; }
async init() {
let authState = this.authManager.load();
const localFound = discoverCookiesPath();
const haveAnyCookies = localFound !== null;
if (haveAnyCookies && importCookiesToProfile(this.authManager)) {
authState = this.authManager.load();
logger.info(`client: auto-synced exocoreCookies.json → profile (${Object.keys(authState.cookies).length} cookies).`);
}
const hasPsid = !!(authState.cookies['__Secure-1PSID'] || authState.cookies['__Secure-3PSID']);
if (!hasPsid) {
throw new AuthError('Not signed in yet — tap the login button to paste your cookies.');
}
// Don't call refreshTokens() here — hitting gemini.google.com/app from a
// datacenter IP with user cookies triggers Google's anti-abuse system and
// invalidates the user's browser session (auto-logout). Use whatever cached
// SNlM0e token we have; if missing, the API call will fail naturally.
if (!authState.tokens.snlm0e) {
logger.warn('client: no cached SNlM0e token. Gemini API calls may fail — paste fresh cookies after signing in again.');
}
this.#jar = new CookieJar(authState.cookies);
this.#httpClient = axios.create({
timeout: this.timeoutMs,
headers: { ...GEMINI_HEADERS },
maxRedirects: 5,
proxy: this.proxy ? parseAxiosProxy(this.proxy) : false,
validateStatus: () => true,
});
this.#jar.attachTo(this.#httpClient);
this.#transport = new RPCTransport({
client: this.#httpClient,
accessToken: authState.tokens.snlm0e,
buildLabel: authState.tokens.cfb2h ?? null,
sessionId: authState.tokens.fdrfje ?? null,
});
this.#initialized = true;
logger.info('client: GeminiClient initialized.');
}
async send(prompt, { model = null, metadata = null, fileData = null, gemId = null, toolId = null, stylePreset = null, temp = null, onDelta = null } = {}) {
if (!this.#initialized) throw new AuthError('GeminiClient not initialized. Call init() first.');
if (toolId !== null && toolId !== undefined) throw new GeminiError('Tool-flagged requests require BotGuard and are not supported in the HTTP-only port.');
if (fileData?.length > 0) logger.warn('client: file_data provided over HTTP path — Google may return 0 candidates.');
const resolvedModel = this.#resolveModel(model);
return this.transport.streamGenerate({ prompt, model: resolvedModel, metadata, fileData, gemId, toolId: null, stylePreset, temp, onDelta });
}
async executeRpc(rpcId, payload = '[]', sourcePath = '/app') {
return this.transport.batchexecute([new RPCPayload({ rpcId, payload })], sourcePath);
}
async executeRpcs(payloads, sourcePath = '/app') {
return this.transport.batchexecute(payloads, sourcePath);
}
async uploadFile(content, mimeType, filename) {
if (!this.#initialized || !this.#httpClient) return null;
const buf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf-8');
const storagePath = await uploadToContentPush(this.#httpClient, this.getCookies(), buf, mimeType, filename);
if (!storagePath) return null;
return buildFileDataItem(storagePath, mimeType, filename);
}
getCookies() { return this.#jar ? this.#jar.toObject() : { ...this.authManager.state.cookies }; }
async listChats(limit = 50) {
if (!this.#initialized) throw new AuthError('GeminiClient not initialized.');
throw new AuthError('Puppeteer module removed; listChats is unavailable.');
try {
const { ensurePage, setCookies } = await import('../../puppeteer/chrome/index.js');
const page = await ensurePage(undefined, { fresh: true });
await page.setViewport({ width: 1400, height: 900 });
const cookies = this.#jar ? cookiesFromJar(this.#jar) : cookiesFromState(this.authManager.state);
await setCookies(page, cookies);
await page.goto('https://gemini.google.com/', { waitUntil: 'domcontentloaded', timeout: 20000 }).catch(() => {});
await page.waitForSelector('[data-test-id="conversation"]', { timeout: 15000 }).catch(() => {});
await new Promise(r => setTimeout(r, 3000));
const chats = await page.evaluate(async () => {
const sidebar = document.querySelector('conversations-list')?.parentElement || document.documentElement;
const target = sidebar === document.documentElement ? window : sidebar;
const sleep = ms => new Promise(r => setTimeout(r, ms));
let prev = -1;
for (let i = 0; i < 30; i++) {
target.scrollBy(0, 500);
await sleep(300);
const cur = target.scrollTop || 0;
if (cur === prev) break;
prev = cur;
}
return Array.from(document.querySelectorAll('[data-test-id="conversation"]')).map(a => {
const href = a.getAttribute('href') || '';
const title = a.querySelector('.conversation-title')?.textContent?.trim() || '';
const match = href.match(/\/app\/([a-z0-9_]+)/);
return match ? { id: match[1], title: title || '', pinned: false, timestamp: null } : null;
}).filter(Boolean);
});
return chats;
} catch (e) {
logger.debug(`listChats failed: ${e.message}`);
return [];
}
}
async deleteChat(cid) {
if (!this.#initialized) throw new AuthError('GeminiClient not initialized.');
const cleanId = cid.startsWith('c_') ? cid : `c_${cid}`;
const payload1 = JSON.stringify([cleanId]);
await this.transport.batchexecute([new RPCPayload({ rpcId: RPC.DELETE_CHAT, payload: payload1 })], `/app/${cleanId.slice(2)}`);
const payload2 = JSON.stringify([cleanId, [1, null, 0, 1]]);
await this.transport.batchexecute([new RPCPayload({ rpcId: RPC.DELETE_CHAT_2, payload: payload2 })], `/app/${cleanId.slice(2)}`);
return { deleted: true, id: cleanId };
}
async deleteAllChats() {
const chats = await this.listChats(200);
const results = { deleted: 0, failed: 0, errors: [] };
for (const chat of chats) {
try { await this.deleteChat(chat.id); results.deleted++; }
catch (e) { results.failed++; results.errors.push({ id: chat.id, error: e.message }); }
}
return results;
}
#resolveModel(model) {
if (model instanceof Model) return model;
if (typeof model === 'string' && model.length > 0) return getModel(model);
return DEFAULT_MODEL;
}
async close() {
this.#httpClient = null;
this.#jar = null;
this.#transport = null;
this.#initialized = false;
}
}