fix: rotate unusable phone verification numbers
This commit is contained in:
@@ -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);
|
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) {
|
function isRecoverableAddPhoneSubmitError(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -4080,7 +4105,7 @@
|
|||||||
return result || {};
|
return result || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resendPhoneVerificationCode(tabId) {
|
async function resendPhoneVerificationCode(tabId, options = {}) {
|
||||||
const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9;
|
const visibleStep = normalizeLogStep(activePhoneVerificationLogStep) || 9;
|
||||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||||
? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: 'resend phone verification code' })
|
? await getOAuthFlowStepTimeoutMs(65000, { step: visibleStep, actionLabel: 'resend phone verification code' })
|
||||||
@@ -4088,7 +4113,7 @@
|
|||||||
const result = await sendToContentScriptResilient('signup-page', {
|
const result = await sendToContentScriptResilient('signup-page', {
|
||||||
type: 'RESEND_PHONE_VERIFICATION_CODE',
|
type: 'RESEND_PHONE_VERIFICATION_CODE',
|
||||||
source: 'background',
|
source: 'background',
|
||||||
payload: {},
|
payload: options || {},
|
||||||
}, {
|
}, {
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
responseTimeoutMs: timeoutMs,
|
responseTimeoutMs: timeoutMs,
|
||||||
@@ -5001,7 +5026,6 @@
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await requestAdditionalPhoneSms(state, normalizedActivation);
|
|
||||||
if (resendTriggeredForCurrentNumber) {
|
if (resendTriggeredForCurrentNumber) {
|
||||||
await addLog(
|
await addLog(
|
||||||
`步骤 9:号码 ${normalizedActivation.phoneNumber} 已触发过一次页面重发;为避免限流,将继续轮询不再点击重发。`,
|
`步骤 9:号码 ${normalizedActivation.phoneNumber} 已触发过一次页面重发;为避免限流,将继续轮询不再点击重发。`,
|
||||||
@@ -5010,7 +5034,35 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
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;
|
resendTriggeredForCurrentNumber = true;
|
||||||
await addLog('步骤 9:已点击手机验证码页面的“重新发送短信”。', 'info');
|
await addLog('步骤 9:已点击手机验证码页面的“重新发送短信”。', 'info');
|
||||||
} catch (resendError) {
|
} catch (resendError) {
|
||||||
@@ -5982,10 +6034,10 @@
|
|||||||
submitResult = await submitPhoneNumber(tabId, activation.phoneNumber, activation);
|
submitResult = await submitPhoneNumber(tabId, activation.phoneNumber, activation);
|
||||||
} catch (submitError) {
|
} catch (submitError) {
|
||||||
const submitErrorText = String(submitError?.message || submitError || 'unknown error');
|
const submitErrorText = String(submitError?.message || submitError || 'unknown error');
|
||||||
if (isRecoverableAddPhoneSubmitError(submitErrorText)) {
|
if (isPhoneNumberDeliveryRefusedError(submitErrorText) || isRecoverableAddPhoneSubmitError(submitErrorText)) {
|
||||||
await rotateActivationAfterAddPhoneFailure(
|
await rotateActivationAfterAddPhoneFailure(
|
||||||
submitErrorText,
|
submitErrorText,
|
||||||
'add_phone_submit_failed',
|
isPhoneNumberDeliveryRefusedError(submitErrorText) ? 'phone_delivery_refused' : 'add_phone_submit_failed',
|
||||||
{ url: pageState?.url || '' }
|
{ url: pageState?.url || '' }
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -6032,6 +6084,14 @@
|
|||||||
};
|
};
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (isPhoneNumberDeliveryRefusedError(addPhoneRejectText)) {
|
||||||
|
await rotateActivationAfterAddPhoneFailure(
|
||||||
|
addPhoneRejectText,
|
||||||
|
'phone_delivery_refused',
|
||||||
|
submitResult || {}
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
await addLog(
|
await addLog(
|
||||||
`步骤 9:添加手机号页面拒绝当前号码,但未明确提示已使用(${addPhoneRejectText}),将用同一号码再试一次。`,
|
`步骤 9:添加手机号页面拒绝当前号码,但未明确提示已使用(${addPhoneRejectText}),将用同一号码再试一次。`,
|
||||||
@@ -6050,10 +6110,16 @@
|
|||||||
|| submitResult?.url
|
|| submitResult?.url
|
||||||
|| 'unknown error'
|
|| 'unknown error'
|
||||||
);
|
);
|
||||||
if (isPhoneNumberUsedError(retryRejectText) || isRecoverableAddPhoneSubmitError(retryRejectText)) {
|
if (
|
||||||
|
isPhoneNumberUsedError(retryRejectText)
|
||||||
|
|| isPhoneNumberDeliveryRefusedError(retryRejectText)
|
||||||
|
|| isRecoverableAddPhoneSubmitError(retryRejectText)
|
||||||
|
) {
|
||||||
await rotateActivationAfterAddPhoneFailure(
|
await rotateActivationAfterAddPhoneFailure(
|
||||||
`add-phone keeps rejecting ${activation.phoneNumber} (${retryRejectText})`,
|
`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 || {}
|
submitResult || {}
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -6170,9 +6236,30 @@
|
|||||||
|
|
||||||
if (remainingResendRequests > 0) {
|
if (remainingResendRequests > 0) {
|
||||||
remainingResendRequests -= 1;
|
remainingResendRequests -= 1;
|
||||||
await requestAdditionalPhoneSms(state, activation);
|
|
||||||
try {
|
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');
|
await addLog('步骤 9:手机验证码被拒后已点击“重新发送短信”。', 'info');
|
||||||
} catch (resendError) {
|
} catch (resendError) {
|
||||||
await addLog(`步骤 9:验证码被拒后点击重发失败。${resendError.message}`, 'warn');
|
await addLog(`步骤 9:验证码被拒后点击重发失败。${resendError.message}`, 'warn');
|
||||||
|
|||||||
+52
-2
@@ -403,6 +403,33 @@
|
|||||||
}) || buttons.find((button) => isVisibleElement(button));
|
}) || 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 = {}) {
|
function getPhoneVerificationResendButton(options = {}) {
|
||||||
const { allowDisabled = false } = options;
|
const { allowDisabled = false } = options;
|
||||||
const form = getPhoneVerificationForm();
|
const form = getPhoneVerificationForm();
|
||||||
@@ -413,7 +440,7 @@
|
|||||||
if (!allowDisabled && !isActionEnabled(button)) return false;
|
if (!allowDisabled && !isActionEnabled(button)) return false;
|
||||||
const intent = String(button.getAttribute('value') || '').trim().toLowerCase();
|
const intent = String(button.getAttribute('value') || '').trim().toLowerCase();
|
||||||
if (intent === 'resend') return true;
|
if (intent === 'resend') return true;
|
||||||
return /resend/i.test(getActionText(button));
|
return /resend|重新发送|再次发送|whats\s*app/i.test(getPhoneVerificationResendActionText(button));
|
||||||
}) || null;
|
}) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,7 +856,7 @@
|
|||||||
return waitForPhoneVerificationOutcome();
|
return waitForPhoneVerificationOutcome();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resendPhoneVerificationCode(timeout = 45000) {
|
async function resendPhoneVerificationCode(timeout = 45000, options = {}) {
|
||||||
if (activePhoneResendPromise) {
|
if (activePhoneResendPromise) {
|
||||||
return activePhoneResendPromise;
|
return activePhoneResendPromise;
|
||||||
}
|
}
|
||||||
@@ -872,6 +899,26 @@
|
|||||||
}
|
}
|
||||||
const resendButton = getPhoneVerificationResendButton({ allowDisabled: true });
|
const resendButton = getPhoneVerificationResendButton({ allowDisabled: true });
|
||||||
if (resendButton && isActionEnabled(resendButton)) {
|
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 humanPause(250, 700);
|
||||||
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'click', label: 'phone-verification-resend' }, async () => {
|
await performOperationWithDelay({ stepKey: 'phone-auth', kind: 'click', label: 'phone-verification-resend' }, async () => {
|
||||||
simulateClick(resendButton);
|
simulateClick(resendButton);
|
||||||
@@ -891,6 +938,9 @@
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
resent: true,
|
resent: true,
|
||||||
|
channel: resendInfo.channel || 'sms',
|
||||||
|
channelText: resendInfo.channelText,
|
||||||
|
text: resendInfo.text,
|
||||||
url: location.href,
|
url: location.href,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ async function handleCommand(message) {
|
|||||||
}
|
}
|
||||||
return await submitPhoneVerificationCodeWithProfileFallback(message.payload);
|
return await submitPhoneVerificationCodeWithProfileFallback(message.payload);
|
||||||
case 'RESEND_PHONE_VERIFICATION_CODE':
|
case 'RESEND_PHONE_VERIFICATION_CODE':
|
||||||
return await phoneAuthHelpers.resendPhoneVerificationCode();
|
return await phoneAuthHelpers.resendPhoneVerificationCode(undefined, message.payload || {});
|
||||||
case 'CHECK_PHONE_RESEND_ERROR':
|
case 'CHECK_PHONE_RESEND_ERROR':
|
||||||
return phoneAuthHelpers.checkPhoneResendError();
|
return phoneAuthHelpers.checkPhoneResendError();
|
||||||
case 'RETURN_TO_ADD_PHONE':
|
case 'RETURN_TO_ADD_PHONE':
|
||||||
|
|||||||
@@ -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', () => {
|
test('phone auth exposes resend page error checks for banned numbers', () => {
|
||||||
const originalLocation = global.location;
|
const originalLocation = global.location;
|
||||||
const originalDocument = global.document;
|
const originalDocument = global.document;
|
||||||
|
|||||||
@@ -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 () => {
|
test('phone verification helper treats phone_max_usage_exceeded as used-number and rotates immediately', async () => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
const messages = [];
|
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 () => {
|
test('phone verification helper rotates number immediately when 5sim activation is missing (order not found)', async () => {
|
||||||
const requests = [];
|
const requests = [];
|
||||||
let currentState = {
|
let currentState = {
|
||||||
|
|||||||
Reference in New Issue
Block a user