fix: hand off phone signup to email verification

This commit is contained in:
root
2026-05-09 08:47:58 -04:00
parent 018b84264b
commit 5462400aa4
4 changed files with 321 additions and 71 deletions
+81 -68
View File
@@ -79,6 +79,10 @@
signupProfile,
});
if (result?.emailVerificationRequired || result?.emailVerificationPage) {
return result || {};
}
await completeStepFromBackground(4, {
phoneVerification: true,
code: result?.code || '',
@@ -88,6 +92,77 @@
return result || {};
}
async function executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey) {
if (shouldUseCustomRegistrationEmail(state)) {
await confirmCustomVerificationStepBypass(4);
return;
}
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info');
await ensureIcloudMailSession({
state,
step: 4,
actionLabel: '步骤 4:确认 iCloud 邮箱登录态',
});
}
throwIfStopped();
if (
mail.provider === HOTMAIL_PROVIDER
|| mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|| mail.provider === CLOUD_MAIL_PROVIDER
) {
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
} else if (mail.provider === '2925') {
await addLog(`步骤 4:正在打开${mail.label}...`);
if (typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
} else {
await focusOrOpenMailTab(mail);
}
await addLog(`步骤 4:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
} else {
await addLog(`步骤 4:正在打开${mail.label}...`);
await focusOrOpenMailTab(mail);
}
const shouldRequestFreshCodeFirst = ![
HOTMAIL_PROVIDER,
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
].includes(mail.provider);
const signupProfile = buildSignupProfileForVerificationStep();
await resolveVerificationStep(4, state, mail, {
filterAfterTimestamp: verificationFilterAfterTimestamp,
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
signupProfile,
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
? 15000
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
});
}
async function focusOrOpenMailTab(mail) {
const alive = await isTabAlive(mail.source);
if (alive) {
@@ -210,77 +285,15 @@
}
if (isPhoneSignupState(state)) {
return executeSignupPhoneCodeStep(state, signupTabId);
}
if (shouldUseCustomRegistrationEmail(state)) {
await confirmCustomVerificationStepBypass(4);
return;
}
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
const verificationFilterAfterTimestamp = mail.provider === '2925'
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
: stepStartedAt;
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info');
await ensureIcloudMailSession({
state,
step: 4,
actionLabel: '步骤 4:确认 iCloud 邮箱登录态',
});
}
throwIfStopped();
if (
mail.provider === HOTMAIL_PROVIDER
|| mail.provider === LUCKMAIL_PROVIDER
|| mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER
|| mail.provider === CLOUD_MAIL_PROVIDER
) {
await addLog(`步骤 4:正在通过 ${mail.label} 轮询验证码...`);
} else if (mail.provider === '2925') {
await addLog(`步骤 4:正在打开${mail.label}...`);
if (typeof ensureMail2925MailboxSession === 'function') {
await ensureMail2925MailboxSession({
accountId: state.currentMail2925AccountId || null,
forceRelogin: false,
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
} else {
await focusOrOpenMailTab(mail);
const phoneResult = await executeSignupPhoneCodeStep(state, signupTabId);
if (phoneResult?.emailVerificationRequired || phoneResult?.emailVerificationPage) {
await addLog('步骤 4:手机验证码已通过,OpenAI 要求继续邮箱验证,切换到邮箱验证码轮询。', 'info');
return executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey);
}
await addLog(`步骤 4:将直接使用当前已登录的 ${mail.label} 轮询验证码。`, 'info');
} else {
await addLog(`步骤 4:正在打开${mail.label}...`);
await focusOrOpenMailTab(mail);
return phoneResult;
}
const shouldRequestFreshCodeFirst = ![
HOTMAIL_PROVIDER,
LUCKMAIL_PROVIDER,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER,
].includes(mail.provider);
const signupProfile = buildSignupProfileForVerificationStep();
await resolveVerificationStep(4, state, mail, {
filterAfterTimestamp: verificationFilterAfterTimestamp,
sessionKey: verificationSessionKey,
disableTimeBudgetCap: mail.provider === '2925',
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
signupProfile,
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
? 15000
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
});
return executeSignupEmailVerificationStep(state, stepStartedAt, verificationSessionKey);
}
return { executeStep4 };
+24 -3
View File
@@ -4714,8 +4714,9 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
}
async function waitForVerificationSubmitOutcome(step, timeout) {
async function waitForVerificationSubmitOutcome(step, timeout, options = {}) {
const resolvedTimeout = timeout ?? (step === 8 ? 30000 : 12000);
const purpose = options?.purpose || '';
const start = Date.now();
let recoveryCount = 0;
const maxRecoveryCount = 2;
@@ -4761,6 +4762,14 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
if (postVerificationState?.state === 'step5') {
return { success: true };
}
if (purpose === 'signup' && isEmailVerificationPage()) {
return {
success: true,
emailVerificationRequired: true,
emailVerificationPage: true,
url: location.href,
};
}
}
const errorText = getVerificationErrorText();
@@ -4796,6 +4805,14 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
if (postVerificationState?.state === 'step5') {
return { success: true };
}
if (purpose === 'signup' && isEmailVerificationPage()) {
return {
success: true,
emailVerificationRequired: true,
emailVerificationPage: true,
url: location.href,
};
}
}
if (isVerificationPageStillVisible()) {
@@ -5034,9 +5051,11 @@ async function fillVerificationCode(step, payload) {
log(`步骤 ${step}:分格验证码页面未找到可点击提交按钮,继续等待页面自动推进。`, 'info');
}
const outcome = await waitForVerificationSubmitOutcome(step);
const outcome = await waitForVerificationSubmitOutcome(step, undefined, payload);
if (outcome.invalidCode) {
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
} else if (outcome.emailVerificationRequired) {
log(`步骤 ${step}:手机验证码已通过,页面进入邮箱验证码验证。`, 'ok');
} else if (outcome.addPhonePage) {
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
} else {
@@ -5073,9 +5092,11 @@ async function fillVerificationCode(step, payload) {
log(`步骤 ${step}:未找到可提交的验证码按钮,先等待页面自动推进或反馈结果。`, 'warn');
}
const outcome = await waitForVerificationSubmitOutcome(step);
const outcome = await waitForVerificationSubmitOutcome(step, undefined, payload);
if (outcome.invalidCode) {
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
} else if (outcome.emailVerificationRequired) {
log(`步骤 ${step}:手机验证码已通过,页面进入邮箱验证码验证。`, 'ok');
} else if (outcome.addPhonePage) {
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
} else {
@@ -307,6 +307,87 @@ test('step 4 phone signup branch uses SMS helper and does not poll mailbox', asy
]);
});
test('step 4 phone signup email-verification handoff polls mailbox instead of completing phone-only', async () => {
const completions = [];
const phoneCalls = [];
const mailConfigCalls = [];
const resolvedCalls = [];
const tabReuses = [];
const executor = api.createStep4Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
completeStepFromBackground: async (step, payload) => {
completions.push({ step, payload });
},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {},
getMailConfig: (state) => {
mailConfigCalls.push({ state });
return {
provider: '163',
label: '163 邮箱',
source: 'mail-163',
url: 'https://mail.163.com',
};
},
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
phoneVerificationHelpers: {
completeSignupPhoneVerificationFlow: async (tabId, options) => {
phoneCalls.push({ tabId, options });
return {
success: true,
code: '123456',
emailVerificationRequired: true,
emailVerificationPage: true,
url: 'https://auth.openai.com/email-verification',
};
},
},
resolveSignupMethod: () => 'phone',
resolveVerificationStep: async (step, state, mail, options) => {
resolvedCalls.push({ step, state, mail, options });
},
reuseOrCreateTab: async (source, url) => {
tabReuses.push({ source, url });
},
sendToContentScript: async () => ({ ready: true }),
sendToContentScriptResilient: async () => ({ ready: true }),
isRetryableContentScriptTransportError: () => false,
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
});
await executor.executeStep4({
email: 'user@example.com',
resolvedSignupMethod: 'phone',
accountIdentifierType: 'phone',
signupPhoneNumber: '66959916439',
signupPhoneActivation: { activationId: 'signup-123', phoneNumber: '66959916439' },
});
assert.equal(phoneCalls.length, 1);
assert.equal(mailConfigCalls.length, 1);
assert.equal(resolvedCalls.length, 1);
assert.deepStrictEqual(completions, []);
assert.deepStrictEqual(tabReuses, [
{ source: 'mail-163', url: 'https://mail.163.com' },
]);
assert.equal(resolvedCalls[0].step, 4);
assert.equal(resolvedCalls[0].mail.label, '163 邮箱');
assert.equal(resolvedCalls[0].options.requestFreshCodeFirst, true);
assert.equal(Object.prototype.hasOwnProperty.call(resolvedCalls[0].options, 'signupProfile'), true);
});
test('step 4 prepare retries transport by recovering retry page without replaying full prepare loop', async () => {
let sendToContentScriptCalls = 0;
let recoverCalls = 0;
+135
View File
@@ -206,6 +206,141 @@ return {
assert.equal(snapshot.splitCodeEvents.filter((event) => event.startsWith('delay:split-code')).length, 1);
});
test('fillVerificationCode treats phone signup SMS landing on email verification as handoff success', async () => {
const api = new Function(`
let now = 0;
const logs = [];
const clicks = [];
const VERIFICATION_CODE_INPUT_SELECTOR = 'input[name="code"]';
const VERIFICATION_PAGE_PATTERN = /verification code/i;
const location = {
href: 'https://auth.openai.com/phone-verification',
pathname: '/phone-verification',
};
function KeyboardEvent(type, init = {}) {
this.type = type;
Object.assign(this, init);
}
const codeInput = {
value: '',
disabled: false,
form: null,
getAttribute(name) {
if (name === 'maxlength') return '6';
if (name === 'aria-disabled') return 'false';
if (name === 'name') return 'code';
return '';
},
closest() { return null; },
focus() {},
dispatchEvent() {},
};
const submitBtn = {
tagName: 'BUTTON',
textContent: 'Continue',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'submit';
if (name === 'aria-disabled') return 'false';
return '';
},
click() {
location.href = 'https://auth.openai.com/email-verification';
location.pathname = '/email-verification';
},
};
const document = {
readyState: 'complete',
body: {
textContent: 'Enter the verification code we just sent to your email',
innerText: 'Enter the verification code we just sent to your email',
},
querySelector(selector) {
if (selector === VERIFICATION_CODE_INPUT_SELECTOR && location.pathname === '/phone-verification') return codeInput;
if (selector === 'button[type="submit"]' && location.pathname === '/phone-verification') return submitBtn;
if (selector === 'form[action*="email-verification" i]' && location.pathname === '/email-verification') return {};
return null;
},
querySelectorAll(selector) {
if (selector === 'input[maxlength="1"]') return [];
if (location.pathname === '/phone-verification' && selector === 'button[type="submit"], input[type="submit"]') return [submitBtn];
if (location.pathname === '/phone-verification' && selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [submitBtn];
if (location.pathname === '/email-verification' && selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return [{ textContent: 'Resend code', disabled: false, getAttribute() { return ''; } }];
}
return [];
},
};
const Date = { now: () => now };
function throwIfStopped() {}
function log(message, level = 'info') { logs.push({ message, level }); }
async function waitForLoginVerificationPageReady() {}
function is405MethodNotAllowedPage() { return false; }
async function handle405ResendError() {}
function fillInput(el, value) { el.value = value; }
async function sleep(ms = 0) { now += ms || 15000; }
async function waitForDocumentLoadComplete() {}
function isStep5Ready() { return false; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
function isVisibleElement() { return true; }
function isActionEnabled(el) { return Boolean(el) && !el.disabled; }
function getActionText(el) { return el?.textContent || ''; }
async function humanPause() {}
function simulateClick(el) { el.click(); clicks.push(el.textContent); }
function getVerificationErrorText() { return ''; }
function getCurrentAuthRetryPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function isPhoneVerificationPageReady() { return location.pathname === '/phone-verification'; }
function getPageTextSnapshot() { return document.body.textContent; }
function findResendVerificationCodeTrigger() {
return location.pathname === '/email-verification'
? { textContent: 'Resend code', disabled: false, getAttribute() { return ''; } }
: null;
}
${extractFunction('getVisibleSplitVerificationInputs')}
${extractFunction('getVerificationCodeTarget')}
${extractFunction('isEmailVerificationPage')}
${extractFunction('isVerificationPageStillVisible')}
${extractFunction('isSignupProfilePageUrl')}
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
${extractFunction('getStep4PostVerificationState')}
${extractFunction('getVerificationSubmitButtonForTarget')}
${extractFunction('waitForVerificationSubmitButton')}
${extractFunction('waitForVerificationCodeTarget')}
${extractFunction('waitForSplitVerificationInputsFilled')}
${extractFunction('isCombinedSignupVerificationProfilePage')}
${extractFunction('waitForCombinedSignupVerificationProfilePage')}
${extractFunction('waitForVerificationSubmitOutcome')}
${extractFunction('fillVerificationCode')}
return {
run() {
return fillVerificationCode(4, { code: '123456', purpose: 'signup' });
},
snapshot() {
return { clicks, logs, href: location.href };
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.equal(snapshot.href, 'https://auth.openai.com/email-verification');
assert.deepStrictEqual(snapshot.clicks, ['Continue']);
assert.equal(result.success, true);
assert.equal(result.emailVerificationRequired, true);
assert.equal(result.emailVerificationPage, true);
assert.equal(result.url, 'https://auth.openai.com/email-verification');
assert.equal(Object.prototype.hasOwnProperty.call(result, 'invalidCode'), false);
});
test('fillVerificationCode does not short-circuit on mixed email-verification profile page before verification exits', async () => {
const api = new Function(`
const logs = [];