From 67c111ed43196b8d8f4c51fd02698231e8cd72b2 Mon Sep 17 00:00:00 2001 From: InkCrow Date: Tue, 12 May 2026 20:39:20 +0800 Subject: [PATCH] fix(auth): preserve phone identity across add-email retries --- background.js | 10 ++ background/message-router.js | 6 ++ background/steps/fetch-login-code.js | 9 +- background/steps/oauth-login.js | 7 ++ ...ckground-message-router-step2-skip.test.js | 25 +++++ tests/background-step6-retry-limit.test.js | 28 ++++- tests/background-step7-recovery.test.js | 100 ++++++++++++++++++ 7 files changed, 183 insertions(+), 2 deletions(-) diff --git a/background.js b/background.js index f802da9..c899566 100644 --- a/background.js +++ b/background.js @@ -9014,6 +9014,15 @@ async function handleStepData(step, payload) { } break; case 7: + if (payload.accountIdentifierType || payload.accountIdentifier || payload.signupPhoneNumber || payload.signupPhoneActivation || payload.signupPhoneCompletedActivation) { + await setState({ + accountIdentifierType: payload.accountIdentifierType || null, + accountIdentifier: String(payload.accountIdentifier || '').trim(), + signupPhoneNumber: String(payload.signupPhoneNumber || '').trim(), + signupPhoneActivation: payload.signupPhoneActivation || null, + signupPhoneCompletedActivation: payload.signupPhoneCompletedActivation || null, + }); + } if (payload.loginVerificationRequestedAt) { await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); } @@ -11509,6 +11518,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({ LUCKMAIL_PROVIDER, resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, resolveSignupEmailForFlow, + persistRegistrationEmailState, phoneVerificationHelpers, rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args), resolveSignupMethod, diff --git a/background/message-router.js b/background/message-router.js index 1285de4..b7d5737 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -574,6 +574,12 @@ } } break; + case 7: + await syncStepAccountIdentityFromPayload(payload); + if (payload.loginVerificationRequestedAt) { + await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt }); + } + break; case 8: await setState({ ...(payload.phoneVerification || payload.loginPhoneVerification ? { diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 2388709..3ad3fb3 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -182,21 +182,28 @@ } const displayedEmail = normalizeStep8VerificationTargetEmail(result?.displayedEmail || resolvedEmail); + let persistedState = latestState; if (typeof persistRegistrationEmailState === 'function') { await persistRegistrationEmailState(latestState, resolvedEmail, { source: 'step8_add_email', preserveAccountIdentity: true, }); + persistedState = typeof getState === 'function' ? await getState() : latestState; } else { await setState({ email: resolvedEmail, step8VerificationTargetEmail: displayedEmail, }); + persistedState = { + ...latestState, + email: resolvedEmail, + step8VerificationTargetEmail: displayedEmail, + }; } return { state: { - ...latestState, + ...persistedState, email: resolvedEmail, step8VerificationTargetEmail: displayedEmail, }, diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index e330125..bd5fd18 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -263,6 +263,13 @@ const completionPayload = { loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, }; + if (currentIdentifierType === 'phone') { + completionPayload.accountIdentifierType = 'phone'; + completionPayload.accountIdentifier = currentPhoneNumber; + completionPayload.signupPhoneNumber = currentPhoneNumber; + completionPayload.signupPhoneCompletedActivation = currentState?.signupPhoneCompletedActivation || null; + completionPayload.signupPhoneActivation = currentState?.signupPhoneActivation || null; + } if (Object.prototype.hasOwnProperty.call(result || {}, 'skipLoginVerificationStep')) { completionPayload.skipLoginVerificationStep = Boolean(result.skipLoginVerificationStep); } diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index eb12289..927b122 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -244,6 +244,31 @@ test('message router preserves phone signup identity when step payload only repo assert.ok(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456)); }); +test('message router persists phone signup identity from step 7 completion payload', async () => { + const completedActivation = { + activationId: 'signup-done', + phoneNumber: '+5511917097811', + }; + const { router, events } = createRouter({ + state: { stepStatuses: { 7: 'completed', 8: 'pending' } }, + }); + + await router.handleStepData(7, { + loginVerificationRequestedAt: 123456, + accountIdentifierType: 'phone', + accountIdentifier: '+5511917097811', + signupPhoneNumber: '+5511917097811', + signupPhoneActivation: null, + signupPhoneCompletedActivation: completedActivation, + }); + + assert.deepStrictEqual(events.signupPhoneSilentStates, ['+5511917097811']); + assert.ok(events.stateUpdates.some((updates) => updates.signupPhoneActivation === null)); + assert.ok(events.stateUpdates.some((updates) => updates.signupPhoneCompletedActivation === completedActivation)); + assert.ok(events.stateUpdates.some((updates) => updates.loginVerificationRequestedAt === 123456)); + assert.deepStrictEqual(events.emailStates, []); +}); + test('message router does not overwrite a completed step 3 when step 2 is replayed', async () => { const { router, events } = createRouter({ state: { stepStatuses: { 3: 'completed' } }, diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js index 39df900..de5a68e 100644 --- a/tests/background-step6-retry-limit.test.js +++ b/tests/background-step6-retry-limit.test.js @@ -463,12 +463,15 @@ test('step 7 forwards phone login identity payload when account identifier is ph const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); const events = { + completions: [], payloads: [], }; const executor = api.createStep7Executor({ addLog: async () => {}, - completeStepFromBackground: async () => {}, + completeStepFromBackground: async (step, payload) => { + events.completions.push({ step, payload }); + }, getErrorMessage: (error) => error?.message || String(error || ''), getLoginAuthStateLabel: (state) => state || 'unknown', getState: async () => ({ @@ -524,6 +527,24 @@ test('step 7 forwards phone login identity payload when account identifier is ph visibleStep: 7, }, ]); + assert.deepStrictEqual(events.completions, [ + { + step: 7, + payload: { + loginVerificationRequestedAt: 123456, + accountIdentifierType: 'phone', + accountIdentifier: '66959916439', + signupPhoneNumber: '66959916439', + signupPhoneCompletedActivation: { + activationId: 'signup-done', + phoneNumber: '66959916439', + countryId: 52, + countryLabel: 'Thailand', + }, + signupPhoneActivation: null, + }, + }, + ]); }); test('step 7 keeps Plus email login even when phone sms runtime exists', async () => { @@ -737,6 +758,11 @@ test('step 7 can start from a manually filled signup phone without completed ste step: 7, payload: { loginVerificationRequestedAt: 987654, + accountIdentifierType: 'phone', + accountIdentifier: '+447780579093', + signupPhoneNumber: '+447780579093', + signupPhoneCompletedActivation: null, + signupPhoneActivation: null, }, }, ]); diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js index 674bafd..e32d7e3 100644 --- a/tests/background-step7-recovery.test.js +++ b/tests/background-step7-recovery.test.js @@ -472,6 +472,106 @@ test('step 8 reruns step 7 with preserved phone login identity after add-email v assert.equal(calls.rerunStates[0].signupPhoneNumber, '+447780579093'); }); +test('step 8 add-email rereads persisted phone identity before rerunning step 7', async () => { + const calls = { + resolveCalls: 0, + rerunStates: [], + }; + let runtimeState = { + visibleStep: 8, + email: '', + password: 'secret', + oauthUrl: 'https://oauth.example/latest', + accountIdentifierType: 'email', + accountIdentifier: 'stale@example.com', + signupMethod: 'phone', + resolvedSignupMethod: 'phone', + phoneVerificationEnabled: true, + signupPhoneNumber: '', + }; + + const executor = api.createStep8Executor({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + confirmCustomVerificationStepBypass: async () => {}, + ensureStep8VerificationPageReady: async () => ({ state: 'add_email_page', url: 'https://auth.openai.com/add-email' }), + getOAuthFlowRemainingMs: async () => 8000, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000), + getMailConfig: () => ({ + provider: 'qq', + label: 'QQ 邮箱', + source: 'mail-qq', + url: 'https://mail.qq.com', + navigateOnReuse: false, + }), + getState: async () => ({ ...runtimeState }), + getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2), + HOTMAIL_PROVIDER: 'hotmail-api', + isTabAlive: async () => true, + isVerificationMailPollingError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + persistRegistrationEmailState: async (_state, email, options) => { + assert.equal(email, 'new.user@example.com'); + assert.equal(options.preserveAccountIdentity, true); + runtimeState = { + ...runtimeState, + email, + accountIdentifierType: 'phone', + accountIdentifier: '+447780579093', + signupPhoneNumber: '+447780579093', + signupPhoneCompletedActivation: { + activationId: 'signup-done', + phoneNumber: '+447780579093', + }, + }; + }, + resolveSignupEmailForFlow: async (_state, options = {}) => { + assert.equal(options.preserveAccountIdentity, true); + return 'new.user@example.com'; + }, + resolveVerificationStep: async (_step, state) => { + calls.resolveCalls += 1; + assert.equal(state.accountIdentifierType, 'phone'); + assert.equal(state.signupPhoneNumber, '+447780579093'); + throw new Error('STEP8_RESTART_STEP7::step 8 verification page fell into login timeout retry state'); + }, + rerunStep7ForStep8Recovery: async () => { + calls.rerunStates.push({ ...runtimeState }); + }, + reuseOrCreateTab: async () => {}, + sendToContentScriptResilient: async () => ({ + submitted: true, + displayedEmail: 'new.user@example.com', + url: 'https://auth.openai.com/email-verification', + }), + setState: async (payload) => { + runtimeState = { + ...runtimeState, + ...payload, + }; + }, + shouldUseCustomRegistrationEmail: () => false, + STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000, + STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 2, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => executor.executeStep8({ ...runtimeState }), + /STEP8_RESTART_STEP7::/ + ); + + assert.equal(calls.resolveCalls, 2); + assert.equal(calls.rerunStates.length, 1); + assert.equal(calls.rerunStates[0].accountIdentifierType, 'phone'); + assert.equal(calls.rerunStates[0].signupPhoneNumber, '+447780579093'); +}); + test('step 8 email_in_use recovery preserves the previous registration baseline', async () => { const calls = { contentCalls: 0,