From 018b84264b051aa86115f67d3004f61ae56defdc Mon Sep 17 00:00:00 2001 From: root Date: Sat, 9 May 2026 05:28:01 -0400 Subject: [PATCH] fix: rotate unusable phone verification numbers --- background/phone-verification-flow.js | 107 ++++++- content/phone-auth.js | 54 +++- content/signup-page.js | 2 +- tests/phone-auth-country-match.test.js | 74 +++++ tests/phone-verification-flow.test.js | 421 +++++++++++++++++++++++++ 5 files changed, 645 insertions(+), 13 deletions(-) diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index e445e0b..4603336 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -406,6 +406,31 @@ return /phone\s+number\s+is\s+not\s+valid|invalid\s+phone\s+number|invalid\s+phone|not\s+a\s+valid\s+phone|号码.*无效|手机号.*无效|电话号码.*无效/i.test(text); } + function isPhoneNumberDeliveryRefusedError(value) { + const text = String(value || '').trim(); + if (!text) { + return false; + } + return /无法向此电话号码发送验证码|无法向.*(?:电话号码|手机号|号码).*发送(?:验证码|短信)|(?:不能|无法).*发送.*(?:验证码|短信).*(?:电话号码|手机号|号码)|(?:cannot|can't|could\s*not|couldn't|unable\s+to)\s+(?:send|deliver).{0,80}(?:verification\s+code|code|sms|text(?:\s+message)?).{0,80}(?:phone|number)|(?:verification\s+code|sms|text(?:\s+message)?).{0,80}(?:cannot|can't|could\s*not|couldn't|unable\s+to).{0,80}(?:send|deliver)/i.test(text); + } + + function isWhatsAppPhoneResendResult(value) { + if (!value) { + return false; + } + const text = typeof value === 'string' + ? value + : [ + value.channel, + value.channelText, + value.text, + value.buttonText, + value.label, + value.message, + ].filter(Boolean).join(' '); + return /whats\s*app/i.test(String(text || '')); + } + function isRecoverableAddPhoneSubmitError(value) { const text = String(value || '').trim(); if (!text) { @@ -4080,7 +4105,7 @@ return result || {}; } - async function resendPhoneVerificationCode(tabId) { + async function resendPhoneVerificationCode(tabId, options = {}) { const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9; const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function' ? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: 'resend phone verification code' }) @@ -4088,7 +4113,7 @@ const result = await sendToContentScriptResilient('signup-page', { type: 'RESEND_PHONE_VERIFICATION_CODE', source: 'background', - payload: {}, + payload: options || {}, }, { timeoutMs, responseTimeoutMs: timeoutMs, @@ -5001,7 +5026,6 @@ ); continue; } - await requestAdditionalPhoneSms(state, normalizedActivation); if (resendTriggeredForCurrentNumber) { await addLog( `步骤 9:号码 ${normalizedActivation.phoneNumber} 已触发过一次页面重发;为避免限流,将继续轮询不再点击重发。`, @@ -5010,7 +5034,35 @@ continue; } try { - await resendPhoneVerificationCode(tabId); + const resendProbeResult = await resendPhoneVerificationCode(tabId, { probeOnly: true }); + if (isWhatsAppPhoneResendResult(resendProbeResult)) { + await addLog( + `步骤 9:页面重发入口显示 WhatsApp 通道(${resendProbeResult.channelText || resendProbeResult.text || 'WhatsApp'}),当前接码平台无法读取 WhatsApp 消息,立即更换号码。`, + 'warn' + ); + await clearPhoneRuntimeCountdown(); + return { + code: '', + replaceNumber: true, + reason: 'whatsapp_resend_channel', + }; + } + await requestAdditionalPhoneSms(state, normalizedActivation); + if (resendProbeResult?.probed) { + const resendResult = await resendPhoneVerificationCode(tabId); + if (isWhatsAppPhoneResendResult(resendResult)) { + await addLog( + `步骤 9:页面重发入口切换为 WhatsApp 通道(${resendResult.channelText || resendResult.text || 'WhatsApp'}),当前接码平台无法读取 WhatsApp 消息,立即更换号码。`, + 'warn' + ); + await clearPhoneRuntimeCountdown(); + return { + code: '', + replaceNumber: true, + reason: 'whatsapp_resend_channel', + }; + } + } resendTriggeredForCurrentNumber = true; await addLog('步骤 9:已点击手机验证码页面的“重新发送短信”。', 'info'); } catch (resendError) { @@ -5982,10 +6034,10 @@ submitResult = await submitPhoneNumber(tabId, activation.phoneNumber, activation); } catch (submitError) { const submitErrorText = String(submitError?.message || submitError || 'unknown error'); - if (isRecoverableAddPhoneSubmitError(submitErrorText)) { + if (isPhoneNumberDeliveryRefusedError(submitErrorText) || isRecoverableAddPhoneSubmitError(submitErrorText)) { await rotateActivationAfterAddPhoneFailure( submitErrorText, - 'add_phone_submit_failed', + isPhoneNumberDeliveryRefusedError(submitErrorText) ? 'phone_delivery_refused' : 'add_phone_submit_failed', { url: pageState?.url || '' } ); continue; @@ -6032,6 +6084,14 @@ }; continue; } + if (isPhoneNumberDeliveryRefusedError(addPhoneRejectText)) { + await rotateActivationAfterAddPhoneFailure( + addPhoneRejectText, + 'phone_delivery_refused', + submitResult || {} + ); + continue; + } await addLog( `步骤 9:添加手机号页面拒绝当前号码,但未明确提示已使用(${addPhoneRejectText}),将用同一号码再试一次。`, @@ -6050,10 +6110,16 @@ || submitResult?.url || 'unknown error' ); - if (isPhoneNumberUsedError(retryRejectText) || isRecoverableAddPhoneSubmitError(retryRejectText)) { + if ( + isPhoneNumberUsedError(retryRejectText) + || isPhoneNumberDeliveryRefusedError(retryRejectText) + || isRecoverableAddPhoneSubmitError(retryRejectText) + ) { await rotateActivationAfterAddPhoneFailure( `add-phone keeps rejecting ${activation.phoneNumber} (${retryRejectText})`, - isPhoneNumberUsedError(retryRejectText) ? 'phone_number_used' : 'add_phone_rejected', + isPhoneNumberUsedError(retryRejectText) + ? 'phone_number_used' + : (isPhoneNumberDeliveryRefusedError(retryRejectText) ? 'phone_delivery_refused' : 'add_phone_rejected'), submitResult || {} ); continue; @@ -6170,9 +6236,30 @@ if (remainingResendRequests > 0) { remainingResendRequests -= 1; - await requestAdditionalPhoneSms(state, activation); try { - await resendPhoneVerificationCode(tabId); + const resendProbeResult = await resendPhoneVerificationCode(tabId, { probeOnly: true }); + if (isWhatsAppPhoneResendResult(resendProbeResult)) { + shouldReplaceNumber = true; + replaceReason = 'whatsapp_resend_channel'; + await addLog( + `步骤 9:验证码被拒后的重发入口显示 WhatsApp 通道(${resendProbeResult.channelText || resendProbeResult.text || 'WhatsApp'}),当前接码平台无法读取 WhatsApp 消息,将更换号码。`, + 'warn' + ); + break; + } + await requestAdditionalPhoneSms(state, activation); + if (resendProbeResult?.probed) { + const resendResult = await resendPhoneVerificationCode(tabId); + if (isWhatsAppPhoneResendResult(resendResult)) { + shouldReplaceNumber = true; + replaceReason = 'whatsapp_resend_channel'; + await addLog( + `步骤 9:验证码被拒后的重发入口切换为 WhatsApp 通道(${resendResult.channelText || resendResult.text || 'WhatsApp'}),当前接码平台无法读取 WhatsApp 消息,将更换号码。`, + 'warn' + ); + break; + } + } await addLog('步骤 9:手机验证码被拒后已点击“重新发送短信”。', 'info'); } catch (resendError) { await addLog(`步骤 9:验证码被拒后点击重发失败。${resendError.message}`, 'warn'); diff --git a/content/phone-auth.js b/content/phone-auth.js index 64ca593..02ec249 100644 --- a/content/phone-auth.js +++ b/content/phone-auth.js @@ -403,6 +403,33 @@ }) || buttons.find((button) => isVisibleElement(button)); } + function getPhoneVerificationResendActionText(button) { + if (!button) return ''; + return [ + button.getAttribute?.('value'), + button.getAttribute?.('aria-label'), + button.getAttribute?.('title'), + getActionText(button), + button.textContent, + ].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); + } + + function isWhatsAppResendText(value) { + return /whats\s*app/i.test(String(value || '')); + } + + function getPhoneVerificationResendActionInfo(button) { + const text = getPhoneVerificationResendActionText(button); + const channel = isWhatsAppResendText(text) + ? 'whatsapp' + : (/(?:sms|text\s+message|短信)/i.test(text) ? 'sms' : ''); + return { + channel, + channelText: text, + text, + }; + } + function getPhoneVerificationResendButton(options = {}) { const { allowDisabled = false } = options; const form = getPhoneVerificationForm(); @@ -413,7 +440,7 @@ if (!allowDisabled && !isActionEnabled(button)) return false; const intent = String(button.getAttribute('value') || '').trim().toLowerCase(); if (intent === 'resend') return true; - return /resend/i.test(getActionText(button)); + return /resend|重新发送|再次发送|whats\s*app/i.test(getPhoneVerificationResendActionText(button)); }) || null; } @@ -829,7 +856,7 @@ return waitForPhoneVerificationOutcome(); } - async function resendPhoneVerificationCode(timeout = 45000) { + async function resendPhoneVerificationCode(timeout = 45000, options = {}) { if (activePhoneResendPromise) { return activePhoneResendPromise; } @@ -872,6 +899,26 @@ } const resendButton = getPhoneVerificationResendButton({ allowDisabled: true }); if (resendButton && isActionEnabled(resendButton)) { + const resendInfo = getPhoneVerificationResendActionInfo(resendButton); + if (resendInfo.channel === 'whatsapp') { + return { + resent: false, + channel: 'whatsapp', + channelText: resendInfo.channelText, + text: resendInfo.text, + url: location.href, + }; + } + if (options?.probeOnly) { + return { + resent: false, + probed: true, + channel: resendInfo.channel || 'unknown', + channelText: resendInfo.channelText, + text: resendInfo.text, + url: location.href, + }; + } await humanPause(250, 700); await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'click', label: 'phone-verification-resend' }, async () => { simulateClick(resendButton); @@ -891,6 +938,9 @@ } return { resent: true, + channel: resendInfo.channel || 'sms', + channelText: resendInfo.channelText, + text: resendInfo.text, url: location.href, }; } diff --git a/content/signup-page.js b/content/signup-page.js index d24a4f4..121a343 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -105,7 +105,7 @@ async function handleCommand(message) { } return await submitPhoneVerificationCodeWithProfileFallback(message.payload); case 'RESEND_PHONE_VERIFICATION_CODE': - return await phoneAuthHelpers.resendPhoneVerificationCode(); + return await phoneAuthHelpers.resendPhoneVerificationCode(undefined, message.payload || {}); case 'CHECK_PHONE_RESEND_ERROR': return phoneAuthHelpers.checkPhoneResendError(); case 'RETURN_TO_ADD_PHONE': diff --git a/tests/phone-auth-country-match.test.js b/tests/phone-auth-country-match.test.js index 7b6e46d..ef849d8 100644 --- a/tests/phone-auth-country-match.test.js +++ b/tests/phone-auth-country-match.test.js @@ -449,6 +449,80 @@ test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of en } }); +test('phone auth probes WhatsApp resend channel without clicking', async () => { + const originalDocument = global.document; + const originalLocation = global.location; + const originalWindow = global.window; + let clickCount = 0; + const fakeWhatsAppButton = { + disabled: false, + textContent: '重新发送 WhatsApp 消息', + getAttribute() { + return ''; + }, + click() { + clickCount += 1; + }, + }; + const fakePhoneForm = { + querySelectorAll(selector) { + if (selector === 'button, input[type="submit"], input[type="button"]') { + return [fakeWhatsAppButton]; + } + return []; + }, + }; + + global.document = { + querySelector(selector) { + if (selector === 'form[action*="/phone-verification" i]') { + return fakePhoneForm; + } + return null; + }, + querySelectorAll() { + return []; + }, + }; + global.location = { + href: 'https://auth.openai.com/phone-verification', + pathname: '/phone-verification', + }; + global.window = global; + + const helpers = api.createPhoneAuthHelpers({ + fillInput: () => {}, + getActionText: (element) => String(element?.textContent || ''), + getPageTextSnapshot: () => '重新发送 WhatsApp 消息', + getVerificationErrorText: () => '', + humanPause: async () => {}, + isActionEnabled: () => true, + isAddPhonePageReady: () => false, + isConsentReady: () => false, + isPhoneVerificationPageReady: () => true, + isVisibleElement: () => true, + simulateClick: (element) => { + element?.click?.(); + }, + sleep: async () => {}, + throwIfStopped: () => {}, + waitForElement: async () => null, + }); + + try { + const result = await helpers.resendPhoneVerificationCode(1000, { probeOnly: true }); + + assert.equal(result.resent, false); + assert.equal(result.channel, 'whatsapp'); + assert.match(result.channelText, /WhatsApp/i); + assert.equal(clickCount, 0); + } finally { + global.document = originalDocument; + global.location = originalLocation; + global.window = originalWindow; + } +}); + test('phone auth exposes resend page error checks for banned numbers', () => { const originalLocation = global.location; const originalDocument = global.document; diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index bf5a29f..14fa9e3 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -2938,6 +2938,284 @@ test('phone verification helper immediately replaces number when page says the p ); }); +test('phone verification helper rotates number when verification code cannot be sent from add-phone rejection', async () => { + const requests = []; + const messages = []; + const submittedNumbers = []; + const refusedText = '无法向此电话号码发送验证码。请稍后重试或使用其他号码。'; + let currentState = { + heroSmsApiKey: 'demo-key', + verificationResendCount: 0, + phoneVerificationReplacementLimit: 2, + currentPhoneActivation: null, + reusablePhoneActivation: null, + }; + + const numbers = [ + { activationId: '331111', phoneNumber: '66950003111' }, + { activationId: '332222', phoneNumber: '66950003222' }, + ]; + let numberIndex = 0; + + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + requests.push(parsedUrl); + const action = parsedUrl.searchParams.get('action'); + const id = parsedUrl.searchParams.get('id'); + + if (action === 'getPrices') { + return { + ok: true, + text: async () => buildHeroSmsPricesPayload(), + }; + } + if (action === 'getNumber') { + const nextNumber = numbers[numberIndex]; + numberIndex += 1; + return { + ok: true, + text: async () => `ACCESS_NUMBER:${nextNumber.activationId}:${nextNumber.phoneNumber}`, + }; + } + if (action === 'getStatus') { + return { + ok: true, + text: async () => (id === '332222' ? 'STATUS_OK:654321' : 'STATUS_WAIT_CODE'), + }; + } + if (action === 'setStatus') { + return { + ok: true, + text: async () => `STATUS_UPDATED:${id}`, + }; + } + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + sendToContentScriptResilient: async (_source, message) => { + messages.push(message.type); + if (message.type === 'SUBMIT_PHONE_NUMBER') { + const phoneNumber = message.payload.phoneNumber; + submittedNumbers.push(phoneNumber); + if (phoneNumber === '66950003111') { + return { + addPhoneRejected: true, + addPhonePage: true, + errorText: refusedText, + url: 'https://auth.openai.com/add-phone', + }; + } + return { + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + }; + } + if (message.type === 'RETURN_TO_ADD_PHONE') { + return { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }; + } + if (message.type === 'STEP8_GET_STATE') { + return { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + return { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }; + } + if (message.type === 'RESEND_PHONE_VERIFICATION_CODE') { + throw new Error('delivery-refused add-phone rejection should rotate before resend.'); + } + throw new Error(`Unexpected content-script message: ${message.type}`); + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + const result = await helpers.completePhoneVerificationFlow(1, { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }); + + assert.deepStrictEqual(result, { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }); + assert.deepStrictEqual(submittedNumbers, ['66950003111', '66950003222']); + assert.equal(numberIndex, 2); + assert.equal(messages.includes('RESEND_PHONE_VERIFICATION_CODE'), false); + assert.equal( + requests.filter((requestUrl) => requestUrl.searchParams.get('action') === 'getNumber').length, + 2 + ); +}); + +test('phone verification helper keeps used-number cleanup when add-phone rejection asks to use a different phone number', async () => { + const messages = []; + const submittedNumbers = []; + const usedText = 'This phone number is already linked to the maximum number of accounts. Please use a different phone number.'; + let currentState = { + heroSmsApiKey: 'demo-key', + verificationResendCount: 0, + phoneVerificationReplacementLimit: 2, + currentPhoneActivation: null, + reusablePhoneActivation: null, + phoneReusableActivationPool: [ + { + activationId: 'pool-used-different-match', + phoneNumber: '+66 95 000 4111', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + successfulUses: 1, + maxUses: 3, + }, + ], + freeReusablePhoneActivation: { + activationId: 'free-used-different-match', + phoneNumber: '+66 95 000 4111', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + countryLabel: 'Thailand', + successfulUses: 0, + maxUses: 3, + source: 'free-manual-reuse', + }, + }; + + const numbers = [ + { activationId: '341111', phoneNumber: '66950004111' }, + { activationId: '342222', phoneNumber: '66950004222' }, + ]; + let numberIndex = 0; + + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + const action = parsedUrl.searchParams.get('action'); + const id = parsedUrl.searchParams.get('id'); + + if (action === 'getPrices') { + return { + ok: true, + text: async () => buildHeroSmsPricesPayload(), + }; + } + if (action === 'getNumber') { + const nextNumber = numbers[numberIndex]; + numberIndex += 1; + return { + ok: true, + text: async () => `ACCESS_NUMBER:${nextNumber.activationId}:${nextNumber.phoneNumber}`, + }; + } + if (action === 'getStatus') { + return { + ok: true, + text: async () => (id === '342222' ? 'STATUS_OK:654321' : 'STATUS_WAIT_CODE'), + }; + } + if (action === 'setStatus') { + return { + ok: true, + text: async () => `STATUS_UPDATED:${id}`, + }; + } + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + sendToContentScriptResilient: async (_source, message) => { + messages.push(message.type); + if (message.type === 'SUBMIT_PHONE_NUMBER') { + const phoneNumber = message.payload.phoneNumber; + submittedNumbers.push(phoneNumber); + if (phoneNumber === '66950004111') { + return { + addPhoneRejected: true, + addPhonePage: true, + errorText: usedText, + url: 'https://auth.openai.com/add-phone', + }; + } + return { + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + }; + } + if (message.type === 'RETURN_TO_ADD_PHONE') { + return { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }; + } + if (message.type === 'STEP8_GET_STATE') { + return { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + return { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }; + } + if (message.type === 'RESEND_PHONE_VERIFICATION_CODE') { + throw new Error('used add-phone rejection should rotate before resend.'); + } + throw new Error(`Unexpected content-script message: ${message.type}`); + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + const result = await helpers.completePhoneVerificationFlow(1, { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }); + + assert.deepStrictEqual(result, { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }); + assert.deepStrictEqual(submittedNumbers, ['66950004111', '66950004222']); + assert.equal(currentState.freeReusablePhoneActivation, null); + assert.deepStrictEqual( + currentState.phoneReusableActivationPool.filter((activation) => activation.phoneNumber === '+66 95 000 4111'), + [] + ); + assert.equal(messages.includes('RESEND_PHONE_VERIFICATION_CODE'), false); +}); + test('phone verification helper treats phone_max_usage_exceeded as used-number and rotates immediately', async () => { const requests = []; const messages = []; @@ -5103,6 +5381,149 @@ test('phone verification helper skips page resend for 5sim timeouts and rotates ); }); +test('phone verification helper rotates number when resend action is WhatsApp in SMS provider mode', async () => { + const requests = []; + const messages = []; + const submittedNumbers = []; + const statusCallsById = {}; + const heroSmsResendStatusCalls = []; + let currentState = { + heroSmsApiKey: 'demo-key', + heroSmsCountryId: 52, + heroSmsCountryLabel: 'Thailand', + verificationResendCount: 1, + phoneVerificationReplacementLimit: 2, + phoneCodeWaitSeconds: 60, + phoneCodeTimeoutWindows: 2, + phoneCodePollIntervalSeconds: 1, + phoneCodePollMaxRounds: 1, + currentPhoneActivation: null, + reusablePhoneActivation: null, + }; + const numbers = [ + { activationId: '520001', phoneNumber: '66952220001' }, + { activationId: '520002', phoneNumber: '66952220002' }, + ]; + let numberIndex = 0; + const realDateNow = Date.now; + let fakeNow = 0; + Date.now = () => fakeNow; + + try { + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + requests.push(parsedUrl); + const action = parsedUrl.searchParams.get('action'); + const id = parsedUrl.searchParams.get('id'); + const status = parsedUrl.searchParams.get('status'); + + if (action === 'getPrices') { + return { + ok: true, + text: async () => buildHeroSmsPricesPayload(), + }; + } + if (action === 'getNumber') { + const nextNumber = numbers[numberIndex]; + numberIndex += 1; + return { + ok: true, + text: async () => `ACCESS_NUMBER:${nextNumber.activationId}:${nextNumber.phoneNumber}`, + }; + } + if (action === 'getStatus') { + statusCallsById[id] = (statusCallsById[id] || 0) + 1; + return { + ok: true, + text: async () => (id === '520001' ? 'STATUS_WAIT_CODE' : 'STATUS_OK:123456'), + }; + } + if (action === 'setStatus') { + if (id === '520001' && status === '3') { + heroSmsResendStatusCalls.push(parsedUrl); + } + return { + ok: true, + text: async () => `STATUS_UPDATED:${id}`, + }; + } + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + sendToContentScriptResilient: async (_source, message) => { + messages.push(message.type); + if (message.type === 'SUBMIT_PHONE_NUMBER') { + submittedNumbers.push(message.payload.phoneNumber); + return { + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + }; + } + if (message.type === 'RESEND_PHONE_VERIFICATION_CODE') { + return { + resent: false, + channel: 'whatsapp', + channelText: '重新发送 WhatsApp 消息', + text: '重新发送 WhatsApp 消息', + url: 'https://auth.openai.com/phone-verification', + }; + } + if (message.type === 'RETURN_TO_ADD_PHONE') { + return { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }; + } + if (message.type === 'STEP8_GET_STATE') { + return { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + return { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }; + } + throw new Error(`Unexpected content-script message: ${message.type}`); + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => { + fakeNow += 61000; + }, + throwIfStopped: () => {}, + }); + + const result = await helpers.completePhoneVerificationFlow(1, { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }); + + assert.deepStrictEqual(result, { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }); + assert.deepStrictEqual(submittedNumbers, ['66952220001', '66952220002']); + assert.equal(heroSmsResendStatusCalls.length, 0, 'WhatsApp resend must not call HeroSMS setStatus(3)'); + assert.equal(statusCallsById['520001'], 1, 'WhatsApp resend should replace after the first SMS wait window'); + assert.equal(messages.includes('RESEND_PHONE_VERIFICATION_CODE'), true); + } finally { + Date.now = realDateNow; + } +}); + test('phone verification helper rotates number immediately when 5sim activation is missing (order not found)', async () => { const requests = []; let currentState = {