'use strict'; const assert = require('assert'); const crypto = require('crypto'); const zlib = require('zlib'); const { loginOpenAIOAuthWithGoogle, _private } = require('../src/services/openai_oauth_service'); function jwt(payload) { const header = Buffer.from(JSON.stringify({ alg: 'none' })).toString('base64url'); const body = Buffer.from(JSON.stringify(payload)).toString('base64url'); return `${header}.${body}.signature`; } (async () => { const { verifier, challenge } = _private.generatePKCE(); assert.match(verifier, /^[a-f0-9]{128}$/); assert.strictEqual( challenge, crypto.createHash('sha256').update(verifier).digest().toString('base64url'), ); const generatedState = _private.generateState(); assert.match(generatedState, /^[a-f0-9]{64}$/); const authorize = new URL(_private.buildAuthorizeUrl({ challenge, state: 'state-1' })); assert.strictEqual(authorize.origin, 'https://auth.openai.com'); assert.strictEqual(authorize.pathname, '/oauth/authorize'); assert.strictEqual(authorize.searchParams.get('client_id'), _private.OAUTH_CLIENT_ID); assert.strictEqual(authorize.searchParams.get('redirect_uri'), _private.OAUTH_REDIRECT_URI); assert.strictEqual(authorize.searchParams.get('state'), 'state-1'); assert.strictEqual(authorize.searchParams.get('code_challenge'), challenge); assert.deepStrictEqual( [...authorize.searchParams.keys()], [ 'client_id', 'code_challenge', 'code_challenge_method', 'codex_cli_simplified_flow', 'id_token_add_organizations', 'redirect_uri', 'response_type', 'scope', 'state', ], ); const callback = _private.readCallback( `${_private.OAUTH_REDIRECT_URI}?code=code-1&state=state-1`, 'state-1', ); assert.strictEqual(callback.code, 'code-1'); assert.strictEqual(_private.readCallback('http://localhost:1455/other?code=x', 'state-1'), null); assert.throws( () => _private.readCallback(`${_private.OAUTH_REDIRECT_URI}?code=x&state=wrong`, 'state-1'), error => error && error.code === 'OPENAI_OAUTH_STATE_MISMATCH', ); const session = { workspaces: [{ id: 'personal' }, { id: 'team' }] }; const encoded = Buffer.from(JSON.stringify(session)).toString('base64url'); assert.deepStrictEqual(_private.decodeAuthSessionCookie(`${encoded}.stamp.signature`), session); const compressed = zlib.deflateRawSync(Buffer.from(JSON.stringify(session))).toString('base64url'); assert.deepStrictEqual(_private.decodeAuthSessionCookie(`.${compressed}.stamp.signature`), session); assert.strictEqual(_private.chooseWorkspace(session, 'team').id, 'team'); assert.strictEqual(_private.chooseWorkspace(session, 'missing').id, 'personal'); const accessToken = jwt({ email: 'user@gmail.com', 'https://api.openai.com/auth': { user_id: 'user-1', chatgpt_account_id: 'account-1', }, }); const idToken = jwt({ sub: 'user-1', email: 'user@gmail.com' }); const auth = _private.buildAuthJson('code-1', { access_token: accessToken, refresh_token: 'refresh-1', id_token: idToken, token_type: 'Bearer', expires_in: 3600, }); assert.strictEqual(auth.code, 'code-1'); assert.strictEqual(auth.code_used, true); assert.strictEqual(auth.access_token, accessToken); assert.strictEqual(auth.refresh_token, 'refresh-1'); assert.strictEqual(auth.id_token, idToken); assert.strictEqual(auth.tokens.account_id, 'account-1'); assert.strictEqual(auth.email, 'user@gmail.com'); assert.ok(auth.expires_at); assert.strictEqual(_private.normalizeIdentityEmail('U.ser+tag@googlemail.com'), 'user@gmail.com'); assert.strictEqual( _private.shouldRunOAuthInteractor('https://auth.openai.com/choose-an-account', true), true, '即使已有授权 Cookie,也必须先处理账号选择页', ); assert.strictEqual( _private.shouldRunOAuthInteractor('https://auth.openai.com/sign-in-with-chatgpt/codex/consent', true), false, '已建立授权会话后才允许进入协议 consent', ); assert.strictEqual( _private.shouldRunOAuthInteractor('https://auth.openai.com/sign-in-with-chatgpt/codex/consent', false), true, 'consent 页面缺少授权 Cookie 时仍需恢复登录态', ); let accountClicked = false; const accountCandidate = { isVisible: async () => true, innerText: async () => 'Caufield Amistadi\namistadicaufield@gmail.com', getAttribute: async () => null, isDisabled: async () => false, click: async () => { accountClicked = true; }, }; const accountPage = { locator: () => ({ count: async () => 1, nth: () => accountCandidate, }), getByText: () => ({ first: () => ({ isVisible: async () => false }), }), }; const selected = await _private.clickMatchingOpenAIAccount( accountPage, 'AmistadiCaufield@gmail.com', ); assert.strictEqual(selected.ok, true); assert.strictEqual(accountClicked, true); const busyPage = { locator: () => ({ count: async () => 1, nth: () => ({ isVisible: async () => true, innerText: async () => 'amistadicaufield@gmail.com', getAttribute: async name => (name === 'aria-busy' ? 'true' : null), isDisabled: async () => true, click: async () => { throw new Error('busy account button must not be clicked'); }, }), }), getByText: () => ({ first: () => ({ isVisible: async () => false }) }), }; const pending = await _private.clickMatchingOpenAIAccount( busyPage, 'AmistadiCaufield@gmail.com', ); assert.strictEqual(pending.ok, true); assert.strictEqual(pending.pending, true); assert.strictEqual( _private.isOpenAIPhoneRequiredPage('https://auth.openai.com/add-phone'), true, ); assert.strictEqual( _private.isOpenAIPhoneRequiredPage('https://auth.openai.com/other', 'Phone number required'), true, ); const deactivatedCopy = 'Authentication Error. You do not have an account because it has been deleted or deactivated.'; assert.strictEqual( _private.detectOpenAIAccountDeactivated('https://auth.openai.com/error', deactivatedCopy).code, 'CHATGPT_ACCOUNT_DEACTIVATED', ); const deactivatedPage = { url: () => 'https://auth.openai.com/error', waitForTimeout: async () => {}, locator: selector => (selector === 'body' ? { innerText: async () => deactivatedCopy } : { first: () => ({ isVisible: async () => false }) }), }; const deactivatedResult = await loginOpenAIOAuthWithGoogle( deactivatedPage, { email: 'deleted@example.com' }, () => {}, ); assert.strictEqual(deactivatedResult.code, 'CHATGPT_ACCOUNT_DEACTIVATED'); assert.strictEqual(deactivatedResult.retryable, false); assert.strictEqual(deactivatedResult.restartBrowser, false); assert.strictEqual( _private.isOpenAIWorkspaceSelectionPage( 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent', 'Select a workspace Personal account Continue', ), true, ); let workspaceContinueClicked = false; const workspaceContinueButton = { isVisible: async () => true, innerText: async () => 'Continue', getAttribute: async () => null, isDisabled: async () => false, click: async () => { workspaceContinueClicked = true; }, }; const workspacePage = { url: () => 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent', locator: selector => (selector === 'body' ? { innerText: async () => 'Select a workspace Personal account Continue' } : { count: async () => 1, nth: () => workspaceContinueButton }), waitForTimeout: async () => {}, }; const workspaceSubmitted = await _private.submitOpenAIWorkspaceSelection(workspacePage); assert.strictEqual(workspaceSubmitted.ok, true); assert.strictEqual(workspaceContinueClicked, true); let nativeSubmitCalled = false; const protocolCandidate = { isVisible: async () => true, innerText: async () => 'Caufield Amistadi\namistadicaufield@gmail.com', evaluate: async (fn) => { const button = { name: 'session_id', value: 'session-1', closest: () => ({ action: 'https://auth.openai.com/choose-an-account', method: 'POST', requestSubmit: submittedButton => { nativeSubmitCalled = submittedButton === button; }, }), }; const OriginalFormData = global.FormData; global.FormData = class MockFormData { entries() { return [['intent', 'select']][Symbol.iterator](); } }; try { return fn(button); } finally { global.FormData = OriginalFormData; } }, }; const protocolPage = { url: () => 'https://auth.openai.com/choose-an-account', locator: () => ({ count: async () => 1, nth: () => protocolCandidate }), }; const submitted = await _private.submitMatchingOpenAIAccount( protocolPage, 'AmistadiCaufield@gmail.com', ); assert.strictEqual(submitted.ok, true); assert.strictEqual(nativeSubmitCalled, true); assert.strictEqual(submitted.targetUrl, 'https://auth.openai.com/choose-an-account'); let exchangePayload = null; const exchangePage = { url: () => 'https://auth.openai.com/oauth/token', evaluate: async (_fn, payload) => { exchangePayload = payload; return { ok: true, status: 200, url: payload.targetUrl, data: { access_token: 'access-2', refresh_token: 'refresh-2', id_token: 'id-2', }, }; }, }; const exchanged = await _private.exchangeCodeInBrowser(exchangePage, 'code-2', 'verifier-2'); assert.strictEqual(exchanged.refresh_token, 'refresh-2'); assert.strictEqual(exchangePayload.targetUrl, 'https://auth.openai.com/oauth/token'); assert.strictEqual(exchangePayload.method, 'POST'); assert.strictEqual(exchangePayload.form.code, 'code-2'); assert.strictEqual(exchangePayload.form.code_verifier, 'verifier-2'); let chatGPTGoogleClicks = 0; let chatGPTLoginUrl = 'https://chatgpt.com/'; const missingAction = { first: () => ({ isVisible: async () => false }), }; const chatGPTGoogleButton = { isVisible: async () => true, click: async () => { chatGPTGoogleClicks++; chatGPTLoginUrl = 'https://accounts.google.com/signin/v2'; }, }; const chatGPTPanelPage = { url: () => chatGPTLoginUrl, waitForTimeout: async () => {}, locator: selector => selector === 'button:text-is("Continue with Google")' ? { first: () => chatGPTGoogleButton } : missingAction, }; const panelResult = await loginOpenAIOAuthWithGoogle( chatGPTPanelPage, { email: 'target@example.com' }, () => {}, { isSignedInChatGPTHome: async () => false, handleGoogleOAuth: async () => ({ ok: false, code: 'TEST_GOOGLE_REACHED', msg: 'reached' }), }, ); assert.strictEqual(chatGPTGoogleClicks, 1, 'OAuth二次登录应点击ChatGPT右侧面板的Continue with Google'); assert.strictEqual(panelResult.code, 'TEST_GOOGLE_REACHED'); let chooserCalls = 0; let chooserUrl = 'https://chatgpt.com/'; const chooserPage = { url: () => chooserUrl, waitForTimeout: async () => {}, locator: () => missingAction, }; const chooserResult = await loginOpenAIOAuthWithGoogle( chooserPage, { email: 'target@example.com' }, () => {}, { isSignedInChatGPTHome: async () => false, handleChatGPTAccountChooser: async () => { chooserCalls++; chooserUrl = 'https://accounts.google.com/signin/v2'; return { found: true, handled: true, transitioned: true }; }, handleGoogleOAuth: async () => ({ ok: false, code: 'TEST_CHOOSER_REACHED', msg: 'reached' }), }, ); assert.strictEqual(chooserCalls, 1, 'OAuth二次登录应复用ChatGPT账号选择弹窗处理器'); assert.strictEqual(chooserResult.code, 'TEST_CHOOSER_REACHED'); console.log('openai_oauth_service.test.js: PASS'); })().catch(error => { console.error(error); process.exitCode = 1; });