From e9d7086b30155ff5a799fcbad97fd55d686eb870 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 9 May 2026 14:24:12 -0400 Subject: [PATCH] fix: abort stale phone signup email verification --- background/phone-verification-flow.js | 86 +++- ...ne-signup-stale-email-verification-plan.md | 10 + ...ne-signup-stale-email-verification-plan.md | 18 + ...-signup-stale-email-verification-design.md | 26 + phone-sms/providers/five-sim.js | 9 + tests/phone-verification-flow.test.js | 471 ++++++++++++++++++ 6 files changed, 619 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/drift-adjudication/2026-05-09-phone-signup-stale-email-verification-plan.md create mode 100644 docs/superpowers/plans/2026-05-09-phone-signup-stale-email-verification-plan.md create mode 100644 docs/superpowers/specs/2026-05-09-phone-signup-stale-email-verification-design.md diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index 4603336..f4a3b19 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -90,6 +90,7 @@ ]); const MAX_PHONE_REUSABLE_POOL = 12; const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::'; + const PHONE_STALE_SIGNUP_EMAIL_VERIFICATION_ERROR_CODE = 'PHONE_SIGNUP_STALE_EMAIL_VERIFICATION'; const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::'; const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::'; const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::'; @@ -1331,10 +1332,39 @@ return new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${suffix}`); } + function isSignupEmailVerificationPageState(pageState = {}) { + const url = String(pageState?.url || pageState?.href || '').trim(); + return Boolean( + pageState?.emailVerificationPage + || pageState?.emailVerificationRequired + || /\/email-verification(?:[/?#]|$)/i.test(url) + ); + } + + function buildSignupPhoneStaleEmailVerificationError(pageState = {}) { + const url = String(pageState?.url || pageState?.href || '').trim(); + const message = `步骤 4:OpenAI 在手机短信验证码提交前已切到邮箱验证${url ? `(URL: ${url})` : ''}。这通常表示当前手机号已关联现有账号或登录路径,请更换手机号后重新开始注册。`; + const error = new Error(message); + error.code = PHONE_STALE_SIGNUP_EMAIL_VERIFICATION_ERROR_CODE; + error.stalePhoneSignupEmailVerification = true; + if (url) { + error.url = url; + } + error.pageState = pageState; + return error; + } + function isPhoneCodeTimeoutError(error) { return String(error?.message || '').startsWith(PHONE_CODE_TIMEOUT_ERROR_PREFIX); } + function isStaleSignupPhoneEmailVerificationError(error) { + return Boolean( + error?.stalePhoneSignupEmailVerification + || error?.code === PHONE_STALE_SIGNUP_EMAIL_VERIFICATION_ERROR_CODE + ); + } + function isPhoneResendThrottledError(error) { const message = String(error?.message || error || '').trim(); if (!message) { @@ -3771,6 +3801,17 @@ const digitMatch = trimmed.match(/\b(\d{4,8})\b/); return digitMatch?.[1] || ''; }; + const emitWaitingForCode = async (statusText) => { + if (typeof options.onWaitingForCode === 'function') { + await options.onWaitingForCode({ + activation: normalizedActivation, + elapsedMs: Date.now() - start, + pollCount, + statusText, + timeoutMs, + }); + } + }; if (config.provider === PHONE_SMS_PROVIDER_5SIM) { while (Date.now() - start < timeoutMs) { @@ -3798,15 +3839,17 @@ const statusText = String(payload?.status || '').trim().toUpperCase(); if (/^(RECEIVED|PENDING|RETRY|PREPARE|WAITING)$/i.test(statusText) || !statusText) { + const waitingStatusText = statusText || text || 'PENDING'; if (typeof options.onStatus === 'function') { await options.onStatus({ activation: normalizedActivation, elapsedMs: Date.now() - start, pollCount, - statusText: statusText || text || 'PENDING', + statusText: waitingStatusText, timeoutMs, }); } + await emitWaitingForCode(waitingStatusText); await sleepWithStop(intervalMs); continue; } @@ -3857,17 +3900,20 @@ if (directCode) { return directCode; } + await emitWaitingForCode(text || 'PENDING'); await sleepWithStop(intervalMs); continue; } if (isNexSmsPendingMessage(payload)) { + await emitWaitingForCode(text || 'PENDING'); await sleepWithStop(intervalMs); continue; } if (isNexSmsTerminalError(payload)) { throw new Error(`NexSMS get sms messages failed: ${text || 'unknown terminal error'}`); } + await emitWaitingForCode(text || 'PENDING'); await sleepWithStop(intervalMs); } @@ -3916,16 +3962,19 @@ if (extractedCode) { return extractedCode; } + await emitWaitingForCode(text || 'STATUS_OK'); await sleepWithStop(intervalMs); continue; } if (/^STATUS_(WAIT_CODE|WAIT_RETRY|WAIT_RESEND)(?::.+)?$/i.test(text)) { + await emitWaitingForCode(text); await sleepWithStop(intervalMs); continue; } if (statusAction === 'getStatusV2' && payload && typeof payload === 'object' && !Array.isArray(payload)) { + await emitWaitingForCode(text || 'PENDING'); await sleepWithStop(intervalMs); continue; } @@ -5133,6 +5182,7 @@ const stepKey = String(options?.stepKey || 'fetch-signup-code').trim() || 'fetch-signup-code'; const purpose = String(options?.purpose || 'signup').trim() || 'signup'; const actionLabelPrefix = String(options?.actionLabelPrefix || 'signup phone verification').trim() || 'phone verification'; + const onPollStatus = typeof options?.onPollStatus === 'function' ? options.onPollStatus : null; if (!normalizedActivation) { throw new Error(options?.missingActivationMessage || `步骤 ${visibleStep}:手机号激活记录缺失,请重新执行前置步骤。`); } @@ -5186,6 +5236,11 @@ { step: visibleStep, stepKey } ); }, + onWaitingForCode: async ({ elapsedMs, pollCount, statusText }) => { + if (onPollStatus) { + await onPollStatus({ elapsedMs, pollCount, statusText }); + } + }, }); await clearPhoneRuntimeCountdown(); await setPhoneRuntimeState({ @@ -5283,12 +5338,39 @@ throw new Error('步骤 4:未找到当前注册手机号激活记录,请重新执行步骤 2。'); } + const assertSignupPhoneStillApplicable = async (phaseLabel) => { + try { + const pageState = await readPhonePageState(tabId, 5000); + if (isSignupEmailVerificationPageState(pageState)) { + throw buildSignupPhoneStaleEmailVerificationError(pageState); + } + return pageState; + } catch (error) { + if (isStopRequestedError(error) || isStaleSignupPhoneEmailVerificationError(error)) { + throw error; + } + await addLog( + `步骤 4:检查注册手机号页面状态(${phaseLabel})失败,将继续等待短信。${error.message}`, + 'warn', + { + step: 4, + stepKey: 'fetch-signup-code', + } + ); + return null; + } + }; + let shouldCancelActivation = true; try { for (let attempt = 1; attempt <= DEFAULT_PHONE_SUBMIT_ATTEMPTS; attempt += 1) { throwIfStopped(); state = await getState(); + await assertSignupPhoneStillApplicable('waiting for SMS code'); const code = await waitForSignupPhoneCode(state, activation, { + onPollStatus: async () => { + await assertSignupPhoneStillApplicable('while waiting for SMS code'); + }, onTimeoutWindow: async () => { try { await resendSignupPhoneVerificationCode(tabId); @@ -5308,6 +5390,8 @@ }, }); + await assertSignupPhoneStillApplicable('before submitting SMS code'); + await setPhoneRuntimeState({ [PHONE_VERIFICATION_CODE_STATE_KEY]: String(code || '').trim(), signupPhoneVerificationRequestedAt: Date.now(), diff --git a/docs/superpowers/drift-adjudication/2026-05-09-phone-signup-stale-email-verification-plan.md b/docs/superpowers/drift-adjudication/2026-05-09-phone-signup-stale-email-verification-plan.md new file mode 100644 index 0000000..900fa07 --- /dev/null +++ b/docs/superpowers/drift-adjudication/2026-05-09-phone-signup-stale-email-verification-plan.md @@ -0,0 +1,10 @@ +# Phone Signup Stale Email Verification Drift Adjudication Ledger + +Immutable spec: `/root/projects/resister-codex/codex-oauth-automation-extension-upstream-base-vu8.0/codex-oauth-automation-extension/.worktrees/fix-icloud-polling-addphone-handoff/docs/superpowers/specs/2026-05-09-phone-signup-stale-email-verification-design.md` + +Immutable plan: `/root/projects/resister-codex/codex-oauth-automation-extension-upstream-base-vu8.0/codex-oauth-automation-extension/.worktrees/fix-icloud-polling-addphone-handoff/docs/superpowers/plans/2026-05-09-phone-signup-stale-email-verification-plan.md` + +## Active Adjudications Summary + +| DRIFT_ID | STATUS | SCOPE | P9_FINAL_ADJUDICATION | EFFECTIVE_EXECUTION_CONTRACT | +| --- | --- | --- | --- | --- | diff --git a/docs/superpowers/plans/2026-05-09-phone-signup-stale-email-verification-plan.md b/docs/superpowers/plans/2026-05-09-phone-signup-stale-email-verification-plan.md new file mode 100644 index 0000000..c80543e --- /dev/null +++ b/docs/superpowers/plans/2026-05-09-phone-signup-stale-email-verification-plan.md @@ -0,0 +1,18 @@ +# Phone Signup Stale Email Verification Fix Plan + +## Task 1: Detect Stale Email Verification During Phone SMS Wait + +- Add a TDD regression test in the existing phone verification test area. +- Simulate `completeSignupPhoneVerificationFlow()` or its SMS wait helper entering phone-signup Step 4 with a valid signup phone activation while the auth tab is already on `/email-verification`. +- The failing test must show the helper currently continues SMS polling instead of returning/failing fast. +- Implement the smallest background-side page-state check needed to stop SMS waiting when the auth tab is already on email verification before SMS submission. +- Return or throw a distinct stale-phone-signup condition that `executeStep4()` can use to fail fast without treating it as a successful post-SMS handoff. +- Preserve existing successful post-SMS `emailVerificationRequired` handoff behavior. + +## Task 2: Verification And Integration + +- Run targeted tests covering phone verification and Step 4 handoff. +- Run full `PYTHONDONTWRITEBYTECODE=1 npm test`. +- Clean generated `scripts/__pycache__` artifacts. +- Create one checkpoint commit only after spec and code reviewers approve. +- Push the resulting branch to the Git export refs and the existing GitHub PR fork branch after final verification. diff --git a/docs/superpowers/specs/2026-05-09-phone-signup-stale-email-verification-design.md b/docs/superpowers/specs/2026-05-09-phone-signup-stale-email-verification-design.md new file mode 100644 index 0000000..5b4a999 --- /dev/null +++ b/docs/superpowers/specs/2026-05-09-phone-signup-stale-email-verification-design.md @@ -0,0 +1,26 @@ +# Phone Signup Stale Email Verification Fix Design + +## Scope + +Fix phone-signup Step 4 behavior when OpenAI has already moved the auth tab to an email verification page before a phone SMS verification code is submitted. + +## Required Behavior + +- If phone-signup Step 4 is waiting for an SMS code and the auth tab is still on the phone verification page, keep polling the configured SMS provider. +- If phone-signup Step 4 is waiting for an SMS code but the auth tab has moved to `/email-verification`, stop waiting for SMS immediately. +- The `/email-verification` transition before SMS submission means the phone number is not usable for a new phone-signup account in this run. The flow must fail fast with a clear, actionable error so the current auto-run attempt can rotate/restart instead of hanging. +- If phone-signup Step 4 submits a valid phone SMS code and then OpenAI moves to `/email-verification`, preserve the existing handoff behavior and continue mailbox verification. +- Do not add operation-delay to SMS polling, email polling, backend retries, or background timers. +- Do not broaden iCloud-specific polling behavior to other mail providers. + +## Error Semantics + +The stale email-verification bailout must be distinguishable from SMS-provider timeout, invalid SMS code, user stop, and normal post-SMS email-verification handoff. + +The user-facing message should explain that OpenAI moved to email verification before phone SMS verification completed, likely because the phone number is already associated with an existing account or login path, and the current number should be replaced. + +## Testing Requirements + +- Add a regression test that simulates phone-signup Step 4 polling while the auth tab is already on `/email-verification`; the test must fail before the implementation and pass after. +- Add or update tests to prove normal post-SMS email-verification handoff still works. +- Run targeted phone Step 4 tests and the full suite before completion. diff --git a/phone-sms/providers/five-sim.js b/phone-sms/providers/five-sim.js index b034e3f..a4f81bf 100644 --- a/phone-sms/providers/five-sim.js +++ b/phone-sms/providers/five-sim.js @@ -838,6 +838,15 @@ if (['CANCELED', 'BANNED', 'FINISHED', 'TIMEOUT'].includes(status)) { throw new Error(`5sim 查询验证码失败:订单状态 ${status}`); } + if (typeof options.onWaitingForCode === 'function') { + await options.onWaitingForCode({ + activation: normalizedActivation, + elapsedMs: Date.now() - start, + pollCount, + statusText: String(payload?.status || lastResponse || '未知'), + timeoutMs, + }); + } await deps.sleepWithStop(intervalMs); } diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index 14fa9e3..c5f1544 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -331,6 +331,13 @@ test('signup phone helper completes signup SMS verification without touching add getState: async () => currentState, sendToContentScriptResilient: async (_source, message) => { contentMessages.push(message); + if (message.type === 'STEP8_GET_STATE') { + return { + emailVerificationPage: false, + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + }; + } if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { return { success: true }; } @@ -363,6 +370,18 @@ test('signup phone helper completes signup SMS verification without touching add code: message.payload?.code, purpose: message.payload?.purpose, })), [ + { + type: 'STEP8_GET_STATE', + step: undefined, + code: undefined, + purpose: undefined, + }, + { + type: 'STEP8_GET_STATE', + step: undefined, + code: undefined, + purpose: undefined, + }, { type: 'SUBMIT_PHONE_VERIFICATION_CODE', step: 4, @@ -378,6 +397,458 @@ test('signup phone helper completes signup SMS verification without touching add assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation'))); }); +test('signup phone helper fails stale email-verification before polling SMS', async () => { + let smsPollCount = 0; + const contentMessages = []; + let currentState = { + heroSmsApiKey: 'demo-key', + heroSmsReuseEnabled: false, + phoneCodeWaitSeconds: 15, + phoneCodeTimeoutWindows: 1, + phoneCodePollIntervalSeconds: 1, + phoneCodePollMaxRounds: 1, + signupPhoneNumber: '66959916439', + signupPhoneVerificationPurpose: 'signup', + signupPhoneActivation: { + activationId: 'signup-123', + phoneNumber: '66959916439', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + successfulUses: 0, + maxUses: 3, + }, + }; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + const action = parsedUrl.searchParams.get('action'); + if (action === 'getStatus') { + smsPollCount += 1; + return { + ok: true, + text: async () => 'STATUS_WAIT_CODE', + }; + } + if (action === 'setStatus') { + return { + ok: true, + text: async () => 'ACCESS_READY', + }; + } + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getOAuthFlowStepTimeoutMs: async (fallback) => fallback, + getState: async () => currentState, + sendToContentScriptResilient: async (_source, message) => { + contentMessages.push(message); + if (message.type === 'STEP8_GET_STATE') { + return { + emailVerificationPage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/email-verification', + }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + throw new Error('stale email verification should fail before SMS submit'); + } + return {}; + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + let caughtError = null; + try { + await helpers.completeSignupPhoneVerificationFlow(77, { state: currentState }); + } catch (error) { + caughtError = error; + } + + assert.equal(smsPollCount, 0, 'stale email-verification should stop before SMS polling'); + assert.ok(caughtError, 'expected stale email-verification to fail fast'); + assert.equal(caughtError.code, 'PHONE_SIGNUP_STALE_EMAIL_VERIFICATION'); + assert.equal(caughtError.stalePhoneSignupEmailVerification, true); + assert.match(caughtError.message, /邮箱验证.*更换手机号/i); + assert.deepStrictEqual(contentMessages.map((message) => message.type), ['STEP8_GET_STATE']); +}); + +test('signup phone helper fails stale email-verification that appears during SMS polling', async () => { + let smsPollCount = 0; + let pageStateReads = 0; + const contentMessages = []; + let currentState = { + heroSmsApiKey: 'demo-key', + heroSmsReuseEnabled: false, + phoneCodeWaitSeconds: 15, + phoneCodeTimeoutWindows: 1, + phoneCodePollIntervalSeconds: 1, + phoneCodePollMaxRounds: 3, + signupPhoneNumber: '66959916439', + signupPhoneVerificationPurpose: 'signup', + signupPhoneActivation: { + activationId: 'signup-123', + phoneNumber: '66959916439', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + successfulUses: 0, + maxUses: 3, + }, + }; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + const action = parsedUrl.searchParams.get('action'); + if (action === 'getStatus') { + smsPollCount += 1; + return { + ok: true, + text: async () => 'STATUS_WAIT_CODE', + }; + } + if (action === 'setStatus') { + return { + ok: true, + text: async () => 'ACCESS_READY', + }; + } + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getOAuthFlowStepTimeoutMs: async (fallback) => fallback, + getState: async () => currentState, + sendToContentScriptResilient: async (_source, message) => { + contentMessages.push(message); + if (message.type === 'STEP8_GET_STATE') { + pageStateReads += 1; + return pageStateReads === 1 + ? { + emailVerificationPage: false, + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + } + : { + emailVerificationPage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/email-verification', + }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + throw new Error('stale email verification should fail before SMS submit'); + } + return {}; + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + let caughtError = null; + try { + await helpers.completeSignupPhoneVerificationFlow(77, { state: currentState }); + } catch (error) { + caughtError = error; + } + + assert.equal(smsPollCount, 1, 'stale email-verification should stop during SMS polling before a second poll'); + assert.equal(pageStateReads, 2, 'should re-check page state during SMS polling'); + assert.ok(caughtError, 'expected stale email-verification to fail fast'); + assert.equal(caughtError.code, 'PHONE_SIGNUP_STALE_EMAIL_VERIFICATION'); + assert.equal(caughtError.stalePhoneSignupEmailVerification, true); + assert.match(caughtError.message, /邮箱验证.*更换手机号/i); + assert.deepStrictEqual(contentMessages.map((message) => message.type), ['STEP8_GET_STATE', 'STEP8_GET_STATE']); +}); + +test('signup phone helper fails stale email-verification on 5sim RECEIVED without code during SMS polling', async () => { + const fiveSimSource = fs.readFileSync('phone-sms/providers/five-sim.js', 'utf8'); + const fiveSimModule = new Function('self', `${fiveSimSource}; return self.PhoneSmsFiveSimProvider;`)({}); + let checkCount = 0; + let pageStateReads = 0; + const contentMessages = []; + let currentState = { + phoneSmsProvider: '5sim', + fiveSimApiKey: 'five-token', + fiveSimCountryOrder: ['thailand'], + fiveSimOperator: 'any', + fiveSimProduct: 'openai', + phoneCodeWaitSeconds: 15, + phoneCodeTimeoutWindows: 1, + phoneCodePollIntervalSeconds: 1, + phoneCodePollMaxRounds: 3, + signupPhoneNumber: '+66900000000', + signupPhoneVerificationPurpose: 'signup', + signupPhoneActivation: { + activationId: '5001', + phoneNumber: '+66900000000', + provider: '5sim', + serviceCode: 'openai', + countryId: 'thailand', + countryCode: 'thailand', + successfulUses: 0, + maxUses: 1, + }, + }; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + createFiveSimProvider: fiveSimModule.createProvider, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + if (parsedUrl.pathname === '/v1/user/check/5001') { + checkCount += 1; + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ + id: 5001, + phone: '+66900000000', + status: 'RECEIVED', + sms: [], + }), + }; + } + throw new Error(`Unexpected 5sim path: ${parsedUrl.pathname}`); + }, + getOAuthFlowStepTimeoutMs: async (fallback) => fallback, + getState: async () => currentState, + sendToContentScriptResilient: async (_source, message) => { + contentMessages.push(message); + if (message.type === 'STEP8_GET_STATE') { + pageStateReads += 1; + return pageStateReads === 1 + ? { + emailVerificationPage: false, + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + } + : { + emailVerificationPage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/email-verification', + }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + throw new Error('stale email verification should fail before SMS submit'); + } + return {}; + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + let caughtError = null; + try { + await helpers.completeSignupPhoneVerificationFlow(77, { state: currentState }); + } catch (error) { + caughtError = error; + } + + assert.equal(checkCount, 1, 'stale email-verification should stop after the first waiting 5sim RECEIVED poll'); + assert.equal(pageStateReads, 2, 'should re-check page state for 5sim RECEIVED without a code'); + assert.ok(caughtError, 'expected stale email-verification to fail fast'); + assert.equal(caughtError.code, 'PHONE_SIGNUP_STALE_EMAIL_VERIFICATION'); + assert.equal(caughtError.stalePhoneSignupEmailVerification, true); + assert.match(caughtError.message, /邮箱验证.*更换手机号/i); + assert.deepStrictEqual(contentMessages.map((message) => message.type), ['STEP8_GET_STATE', 'STEP8_GET_STATE']); +}); + +test('signup phone helper fails stale email-verification on HeroSMS V2 no-code response during SMS polling', async () => { + let smsPollCount = 0; + let pageStateReads = 0; + const contentMessages = []; + let currentState = { + heroSmsApiKey: 'demo-key', + heroSmsReuseEnabled: false, + phoneCodeWaitSeconds: 15, + phoneCodeTimeoutWindows: 1, + phoneCodePollIntervalSeconds: 1, + phoneCodePollMaxRounds: 3, + signupPhoneNumber: '447911123456', + signupPhoneVerificationPurpose: 'signup', + signupPhoneActivation: { + activationId: 'signup-v2', + phoneNumber: '447911123456', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 16, + successfulUses: 0, + maxUses: 3, + statusAction: 'getStatusV2', + }, + }; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + const action = parsedUrl.searchParams.get('action'); + if (action === 'getStatusV2') { + smsPollCount += 1; + return { + ok: true, + text: async () => buildHeroSmsStatusV2Payload(), + }; + } + if (action === 'setStatus') { + return { + ok: true, + text: async () => 'ACCESS_READY', + }; + } + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getOAuthFlowStepTimeoutMs: async (fallback) => fallback, + getState: async () => currentState, + sendToContentScriptResilient: async (_source, message) => { + contentMessages.push(message); + if (message.type === 'STEP8_GET_STATE') { + pageStateReads += 1; + return pageStateReads === 1 + ? { + emailVerificationPage: false, + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + } + : { + emailVerificationPage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/email-verification', + }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + throw new Error('stale email verification should fail before SMS submit'); + } + return {}; + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + let caughtError = null; + try { + await helpers.completeSignupPhoneVerificationFlow(77, { state: currentState }); + } catch (error) { + caughtError = error; + } + + assert.equal(smsPollCount, 1, 'stale email-verification should stop after the first HeroSMS V2 no-code poll'); + assert.equal(pageStateReads, 2, 'should re-check page state for HeroSMS V2 no-code response'); + assert.ok(caughtError, 'expected stale email-verification to fail fast'); + assert.equal(caughtError.code, 'PHONE_SIGNUP_STALE_EMAIL_VERIFICATION'); + assert.equal(caughtError.stalePhoneSignupEmailVerification, true); + assert.match(caughtError.message, /邮箱验证.*更换手机号/i); + assert.deepStrictEqual(contentMessages.map((message) => message.type), ['STEP8_GET_STATE', 'STEP8_GET_STATE']); +}); + +test('signup phone helper fails stale email-verification on NexSMS success without code during SMS polling', async () => { + let smsPollCount = 0; + let pageStateReads = 0; + const contentMessages = []; + let currentState = { + phoneSmsProvider: 'nexsms', + nexSmsApiKey: 'nex-key', + nexSmsCountryOrder: [6], + nexSmsServiceCode: 'ot', + phoneCodeWaitSeconds: 15, + phoneCodeTimeoutWindows: 1, + phoneCodePollIntervalSeconds: 1, + phoneCodePollMaxRounds: 3, + signupPhoneNumber: '+6281234567890', + signupPhoneVerificationPurpose: 'signup', + signupPhoneActivation: { + activationId: '+6281234567890', + phoneNumber: '+6281234567890', + provider: 'nexsms', + serviceCode: 'ot', + countryId: 6, + countryLabel: 'Indonesia', + successfulUses: 0, + maxUses: 1, + }, + }; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + if (parsedUrl.pathname === '/api/sms/messages') { + smsPollCount += 1; + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ code: 0, data: {} }), + }; + } + if (parsedUrl.pathname === '/api/close/activation') { + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ code: 0, data: {} }), + }; + } + throw new Error(`Unexpected NexSMS path: ${parsedUrl.pathname}`); + }, + getOAuthFlowStepTimeoutMs: async (fallback) => fallback, + getState: async () => currentState, + sendToContentScriptResilient: async (_source, message) => { + contentMessages.push(message); + if (message.type === 'STEP8_GET_STATE') { + pageStateReads += 1; + return pageStateReads === 1 + ? { + emailVerificationPage: false, + phoneVerificationPage: true, + url: 'https://auth.openai.com/phone-verification', + } + : { + emailVerificationPage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/email-verification', + }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + throw new Error('stale email verification should fail before SMS submit'); + } + return {}; + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + let caughtError = null; + try { + await helpers.completeSignupPhoneVerificationFlow(77, { state: currentState }); + } catch (error) { + caughtError = error; + } + + assert.equal(smsPollCount, 1, 'stale email-verification should stop after the first NexSMS success without code poll'); + assert.equal(pageStateReads, 2, 'should re-check page state for NexSMS success without code'); + assert.ok(caughtError, 'expected stale email-verification to fail fast'); + assert.equal(caughtError.code, 'PHONE_SIGNUP_STALE_EMAIL_VERIFICATION'); + assert.equal(caughtError.stalePhoneSignupEmailVerification, true); + assert.match(caughtError.message, /邮箱验证.*更换手机号/i); + assert.deepStrictEqual(contentMessages.map((message) => message.type), ['STEP8_GET_STATE', 'STEP8_GET_STATE']); +}); + test('signup phone helper completes login SMS verification by reusing the completed signup activation', async () => { const setStateCalls = []; const contentMessages = [];