fix: hand off phone signup to email verification
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user