'use strict'; const assert = require('assert'); const crypto = require('crypto'); const fs = require('fs'); const os = require('os'); const path = require('path'); const vm = require('vm'); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-credential-')); const Config = require('../src/core/config'); Config.DATA_DIR = tempDir; Config.DB_PATH = path.join(tempDir, 'accounts.db'); const db = require('../src/core/database'); const accountsRouter = require('../src/routes/accounts'); const { CLIENT_ID, REDIRECT_URI, createGeminiCliOAuthSession, buildGeminiCliCredential, _private, } = require('../src/services/gemini_cli_oauth_service'); function routeHandler(routePath) { const layer = accountsRouter.stack.find(item => ( item.route && item.route.path === routePath && item.route.methods.get )); assert(layer, `未找到 GET ${routePath}`); return layer.route.stack[0].handle; } function invoke(handler, params = {}, remoteAddress = '127.0.0.1') { const response = { statusCode: 200, headers: {}, body: null, status(code) { this.statusCode = code; return this; }, set(name, value) { this.headers[String(name).toLowerCase()] = value; return this; }, json(body) { this.body = body; return this; }, }; handler({ params, socket: { remoteAddress } }, response); return response; } function base64Url(buffer) { return Buffer.from(buffer).toString('base64') .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } (async () => { try { const oauthSession = createGeminiCliOAuthSession(); const authUrl = new URL(oauthSession.authUrl); assert.strictEqual(authUrl.origin + authUrl.pathname, 'https://accounts.google.com/o/oauth2/v2/auth'); assert.strictEqual(authUrl.searchParams.get('client_id'), CLIENT_ID); assert.strictEqual(authUrl.searchParams.get('redirect_uri'), REDIRECT_URI); assert.strictEqual(authUrl.searchParams.get('state'), oauthSession.state); assert.strictEqual(authUrl.searchParams.get('code_challenge_method'), 'S256'); assert.strictEqual( authUrl.searchParams.get('code_challenge'), base64Url(crypto.createHash('sha256').update(oauthSession.codeVerifier).digest()), '授权链接中的 PKCE challenge 必须由本次 verifier 生成', ); assert.strictEqual(authUrl.searchParams.get('access_type'), 'offline'); assert.strictEqual(authUrl.searchParams.get('prompt'), 'consent'); const displayedCode = '4/0ATxFakeGeminiCliAuthorizationCode_123456789'; assert.strictEqual(await _private.extractGeminiCliAuthorizationCode({ url: () => REDIRECT_URI, evaluate: async () => ({ values: [displayedCode], bodyText: 'Sign in to Gemini Code Assist and the Gemini CLI\nEnter the following verification code', title: 'Gemini CLI authentication', }), }), displayedCode, 'Gemini CLI 成功页中展示的 verification code 必须被识别为授权成功'); assert.strictEqual(await _private.extractGeminiCliAuthorizationCode({ url: () => 'https://codeassist.google.com/success', evaluate: async () => ({ values: [`${displayedCode.slice(0, 28)}\n${displayedCode.slice(28)}`], bodyText: 'Sign in to Gemini Code Assist and the Gemini CLI\nEnter the following verification code', title: '', }), }), displayedCode, '成功页地址变化且控件值换行时也必须识别授权码'); const framedPage = { url: () => 'https://accounts.google.com/o/oauth2/v2/auth', evaluate: async () => ({ values: [], bodyText: 'Google OAuth', title: 'Google' }), }; const successFrame = { url: () => REDIRECT_URI, evaluate: async () => ({ values: [displayedCode], bodyText: 'Sign in to Gemini Code Assist and the Gemini CLI', title: '', }), }; framedPage.frames = () => [framedPage, successFrame]; assert.strictEqual(await _private.extractGeminiCliAuthorizationCode(framedPage), displayedCode, 'Gemini CLI 成功页位于 frame 中时也必须识别授权码'); assert.strictEqual(await _private.extractGeminiCliAuthorizationCode({ url: () => 'https://accounts.google.com/signin/v2/challenge/pwd', evaluate: async () => ({ values: [displayedCode], bodyText: 'Gemini CLI', title: '' }), }), null, '非 Gemini CLI 回调页不得误提取页面文本'); const credential = buildGeminiCliCredential({ access_token: 'cli-access-token', refresh_token: 'cli-refresh-token', expires_in: 3600, token_type: 'Bearer', scope: 'scope-a scope-b', }, { email: 'cli@example.com', project_id: 'cloud-ai-companion-project', tier_id: 'gcp_standard', }); assert.strictEqual(credential.oauth_type, 'code_assist'); assert.strictEqual(credential.refresh_token, 'cli-refresh-token'); assert.strictEqual(credential.email, 'cli@example.com'); assert.strictEqual(credential.project_id, 'cloud-ai-companion-project'); assert.strictEqual(credential.tier_id, 'gcp_standard'); assert(Number(credential.expires_at) > Math.floor(Date.now() / 1000)); const credentialWithoutProject = buildGeminiCliCredential({ access_token: 'cli-access-without-project', refresh_token: 'cli-refresh-without-project', expires_in: 3600, }, { email: 'no-project@example.com' }); assert.strictEqual(credentialWithoutProject.email, 'no-project@example.com'); assert.strictEqual('project_id' in credentialWithoutProject, false, 'Gemini CLI 授权成功后必须允许在没有 project_id 时保存凭证'); db.initDB(); db.upsertAccount({ email: 'cli@example.com', password: 'password' }); assert.strictEqual(db.updateAccountCliAcCredential('cli@example.com', credential), true); assert.strictEqual( JSON.parse(db.getAccount('cli@example.com').cli_ac_credential).refresh_token, credential.refresh_token, ); db.updateAccountCliAcCredential('cli@example.com', { ...credential, email: 'wrong@example.com' }); assert.strictEqual( JSON.parse(db.getAccount('cli@example.com').cli_ac_credential).email, 'cli@example.com', '写库时必须用账号邮箱覆盖凭证中的错误邮箱', ); const list = invoke(routeHandler('/')); assert.strictEqual(list.body.data[0].has_cli_ac_credential, true); assert.strictEqual('cli_ac_credential' in list.body.data[0], false, '账号列表不得直接暴露 Cli_AC 完整凭证'); const local = invoke(routeHandler('/:email/cli-ac-credential'), { email: encodeURIComponent('cli@example.com'), }); assert.strictEqual(local.statusCode, 200); assert.strictEqual(local.headers['cache-control'], 'no-store'); assert.deepStrictEqual(local.body.data, credential); const remote = invoke(routeHandler('/:email/cli-ac-credential'), { email: encodeURIComponent('cli@example.com'), }, '192.0.2.26'); assert.strictEqual(remote.statusCode, 403); assert(!JSON.stringify(remote.body).includes(credential.refresh_token)); const root = path.join(__dirname, '..'); const html = fs.readFileSync(path.join(root, 'public', 'index.html'), 'utf8'); const appSource = fs.readFileSync(path.join(root, 'public', 'app.js'), 'utf8'); const operations = fs.readFileSync(path.join(root, 'src', 'routes', 'operations.js'), 'utf8'); const worker = fs.readFileSync(path.join(root, 'src', 'workers', 'task_runner.js'), 'utf8'); assert.match(html, /runAgGetCredential\(\)[\s\S]{0,220}runGeminiCliGetCredential\(\)/, '获取Cli凭证按钮必须紧跟在获取凭证右侧'); assert.match(html, /