fix(madao): 修复换号后国家区号残留
让 MaDao activation 使用 ISO country code,并避免 routing replace 沿用旧 ticket 的 country。 提交 OpenAI add-phone 前校验当前国家区号与手机号匹配,防止页面残留 Thailand 区号。
This commit is contained in:
@@ -107,14 +107,14 @@
|
||||
const PHONE_SMS_FAILURE_SKIP_THRESHOLD = 2;
|
||||
const MAX_ACTIVATION_PRICE_HINTS = 256;
|
||||
const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([
|
||||
{ prefix: '84', id: 10, label: 'Vietnam' },
|
||||
{ prefix: '66', id: 52, label: 'Thailand' },
|
||||
{ prefix: '62', id: 6, label: 'Indonesia' },
|
||||
{ prefix: '44', id: 16, label: 'United Kingdom' },
|
||||
{ prefix: '81', id: 151, label: 'Japan' },
|
||||
{ prefix: '49', id: 43, label: 'Germany' },
|
||||
{ prefix: '33', id: 73, label: 'France' },
|
||||
{ prefix: '1', id: 187, label: 'USA' },
|
||||
{ prefix: '84', id: 10, iso: 'VN', label: 'Vietnam' },
|
||||
{ prefix: '66', id: 52, iso: 'TH', label: 'Thailand' },
|
||||
{ prefix: '62', id: 6, iso: 'ID', label: 'Indonesia' },
|
||||
{ prefix: '44', id: 16, iso: 'GB', label: 'United Kingdom' },
|
||||
{ prefix: '81', id: 151, iso: 'JP', label: 'Japan' },
|
||||
{ prefix: '49', id: 43, iso: 'DE', label: 'Germany' },
|
||||
{ prefix: '33', id: 73, iso: 'FR', label: 'France' },
|
||||
{ prefix: '1', id: 187, iso: 'US', label: 'USA' },
|
||||
]);
|
||||
const activationPriceHintsByKey = new Map();
|
||||
let activePhoneVerificationLogStep = null;
|
||||
@@ -587,6 +587,44 @@
|
||||
return fallbackNormalized || DEFAULT_HERO_SMS_OPERATOR;
|
||||
}
|
||||
|
||||
function normalizeMaDaoCountryCode(value = '', fallback = '') {
|
||||
const raw = String(value || '').trim() || String(fallback || '').trim();
|
||||
if (!raw) {
|
||||
return '';
|
||||
}
|
||||
if (/^[a-z]{2}$/i.test(raw)) {
|
||||
return raw.toUpperCase();
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function getRegionDisplayName(regionCode, locale = 'en') {
|
||||
const normalizedRegionCode = normalizeMaDaoCountryCode(regionCode, '');
|
||||
const normalizedLocale = String(locale || '').trim();
|
||||
if (!/^[A-Z]{2}$/.test(normalizedRegionCode) || !normalizedLocale || typeof Intl?.DisplayNames !== 'function') {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return String(
|
||||
new Intl.DisplayNames([normalizedLocale], { type: 'region' }).of(normalizedRegionCode) || ''
|
||||
).trim();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMaDaoCountryLabel(value = '', countryCode = '') {
|
||||
const label = String(value || '').trim();
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
const normalizedCountryCode = normalizeMaDaoCountryCode(countryCode, '');
|
||||
if (!normalizedCountryCode) {
|
||||
return '';
|
||||
}
|
||||
return getRegionDisplayName(normalizedCountryCode, 'en') || normalizedCountryCode;
|
||||
}
|
||||
|
||||
function inferHeroSmsCountryFromPhoneNumber(phoneNumber = '') {
|
||||
const digits = String(phoneNumber || '').replace(/\D+/g, '');
|
||||
if (!digits) {
|
||||
@@ -598,10 +636,22 @@
|
||||
}
|
||||
return {
|
||||
id: normalizeCountryId(match.id, 0),
|
||||
iso: normalizeMaDaoCountryCode(match.iso, ''),
|
||||
label: normalizeCountryLabel(match.label, `Country #${match.id}`),
|
||||
};
|
||||
}
|
||||
|
||||
function inferMaDaoCountryFromPhoneNumber(phoneNumber = '') {
|
||||
const inferred = inferHeroSmsCountryFromPhoneNumber(phoneNumber);
|
||||
if (!inferred?.iso) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: inferred.iso,
|
||||
label: normalizeMaDaoCountryLabel(inferred.label, inferred.iso),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePhoneCodeWaitSeconds(value) {
|
||||
const parsed = Math.floor(Number(value));
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
@@ -1391,6 +1441,9 @@
|
||||
const rawProvider = String(record.provider || '').trim();
|
||||
const provider = normalizePhoneSmsProvider(rawProvider);
|
||||
const rawCountryId = record.countryId ?? record.country;
|
||||
const inferredCountry = provider === PHONE_SMS_PROVIDER_MADAO
|
||||
? inferMaDaoCountryFromPhoneNumber(phoneNumber)
|
||||
: null;
|
||||
const fallbackCountryId = provider === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? 'england'
|
||||
: (provider === PHONE_SMS_PROVIDER_MADAO ? '' : HERO_SMS_COUNTRY_ID);
|
||||
@@ -1412,10 +1465,13 @@
|
||||
? normalizeNexSmsCountryId(rawCountryId, 0)
|
||||
: (
|
||||
provider === PHONE_SMS_PROVIDER_MADAO
|
||||
? String(rawCountryId || '').trim()
|
||||
? normalizeMaDaoCountryCode(rawCountryId, inferredCountry?.id || '')
|
||||
: normalizeCountryId(rawCountryId, fallbackCountryId)
|
||||
)
|
||||
);
|
||||
const normalizedCountryLabel = provider === PHONE_SMS_PROVIDER_MADAO
|
||||
? normalizeMaDaoCountryLabel(countryLabel, countryId)
|
||||
: countryLabel;
|
||||
const ignoredPhoneCodeKeys = normalizeStringList(record.ignoredPhoneCodeKeys);
|
||||
return {
|
||||
activationId,
|
||||
@@ -1424,7 +1480,7 @@
|
||||
serviceCode,
|
||||
countryId,
|
||||
...(provider === PHONE_SMS_PROVIDER_FIVE_SIM ? { countryCode: countryId } : {}),
|
||||
...(countryLabel ? { countryLabel } : {}),
|
||||
...(normalizedCountryLabel ? { countryLabel: normalizedCountryLabel } : {}),
|
||||
successfulUses: normalizeUseCount(record.successfulUses),
|
||||
maxUses: Math.max(1, Math.floor(Number(record.maxUses) || DEFAULT_PHONE_NUMBER_MAX_USES)),
|
||||
...(expiresAt > 0 ? { expiresAt } : {}),
|
||||
@@ -4888,6 +4944,18 @@
|
||||
const providerId = getActivationProviderId(activation, fallbackState);
|
||||
const candidates = resolveCountryCandidatesForProvider(fallbackState, providerId);
|
||||
if (activation && typeof activation === 'object') {
|
||||
if (providerId === PHONE_SMS_PROVIDER_MADAO) {
|
||||
const countryId = normalizeMaDaoCountryCode(activation.countryId ?? activation.country, '');
|
||||
const inferredCountry = inferMaDaoCountryFromPhoneNumber(activation.phoneNumber);
|
||||
const resolvedCountryId = countryId || inferredCountry?.id || '';
|
||||
return {
|
||||
id: resolvedCountryId,
|
||||
label: normalizeMaDaoCountryLabel(
|
||||
activation.countryLabel || inferredCountry?.label || '',
|
||||
resolvedCountryId
|
||||
),
|
||||
};
|
||||
}
|
||||
if (providerId === PHONE_SMS_PROVIDER_FIVE_SIM) {
|
||||
const countryId = normalizeFiveSimCountryId(activation.countryId, '');
|
||||
if (countryId) {
|
||||
@@ -4898,14 +4966,6 @@
|
||||
label: normalizeFiveSimCountryLabel(activation.countryLabel, countryId),
|
||||
};
|
||||
}
|
||||
} else if (providerId === PHONE_SMS_PROVIDER_MADAO) {
|
||||
const countryId = String(activation.countryId || '').trim();
|
||||
if (countryId) {
|
||||
return {
|
||||
id: countryId,
|
||||
label: normalizeCountryLabel(activation.countryLabel, countryId),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const inferredCountry = inferHeroSmsCountryFromPhoneNumber(activation.phoneNumber);
|
||||
const rawCountryId = normalizeCountryId(activation.countryId, 0);
|
||||
@@ -6668,8 +6728,8 @@
|
||||
return normalizedCountryId >= 0 ? `${normalizedProvider}:${normalizedCountryId}` : '';
|
||||
}
|
||||
if (normalizedProvider === PHONE_SMS_PROVIDER_MADAO) {
|
||||
const normalizedCountryId = String(countryId || '').trim();
|
||||
return normalizedCountryId ? `${normalizedProvider}:${normalizedCountryId}` : '';
|
||||
const normalizedCountryCode = normalizeMaDaoCountryCode(countryId, '');
|
||||
return normalizedCountryCode ? `${normalizedProvider}:${normalizedCountryCode}` : '';
|
||||
}
|
||||
const normalizedCountryId = normalizeCountryId(countryId, 0);
|
||||
return normalizedCountryId > 0 ? `${normalizedProvider}:${normalizedCountryId}` : '';
|
||||
@@ -6712,7 +6772,7 @@
|
||||
return matched?.label || `Country #${normalizedCountryId}`;
|
||||
}
|
||||
if (normalizedProvider === PHONE_SMS_PROVIDER_MADAO) {
|
||||
return normalizedCountryKey || 'MaDao';
|
||||
return normalizeMaDaoCountryLabel('', normalizedCountryKey) || normalizedCountryKey || 'Unknown country';
|
||||
}
|
||||
const normalizedCountryId = normalizeCountryId(normalizedCountryKey, 0);
|
||||
const matched = resolveCountryCandidates(state)
|
||||
@@ -6816,9 +6876,11 @@
|
||||
const getCountryFailureKey = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => (
|
||||
normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_FIVE_SIM
|
||||
? normalizeFiveSimCountryId(countryId, '')
|
||||
: (normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_MADAO
|
||||
? String(countryId || '').trim()
|
||||
: String(normalizeCountryId(countryId, 0) || ''))
|
||||
: (
|
||||
normalizePhoneSmsProvider(providerId) === PHONE_SMS_PROVIDER_MADAO
|
||||
? normalizeMaDaoCountryCode(countryId, '')
|
||||
: String(normalizeCountryId(countryId, 0) || '')
|
||||
)
|
||||
);
|
||||
|
||||
const getCountryFailureCount = (countryId, providerId = normalizePhoneSmsProvider(state?.phoneSmsProvider)) => {
|
||||
|
||||
@@ -199,6 +199,23 @@
|
||||
return String(visibleSpan?.textContent || '').trim();
|
||||
}
|
||||
|
||||
function phoneNumberMatchesDialCode(phoneNumber = '', dialCode = '') {
|
||||
const digits = normalizePhoneDigits(phoneNumber);
|
||||
const normalizedDialCode = normalizePhoneDigits(dialCode);
|
||||
if (!digits || !normalizedDialCode) {
|
||||
return false;
|
||||
}
|
||||
return digits.startsWith(normalizedDialCode) && digits.length > normalizedDialCode.length;
|
||||
}
|
||||
|
||||
function selectedCountryMatchesPhoneNumber(phoneNumber = '') {
|
||||
const digits = normalizePhoneDigits(phoneNumber);
|
||||
if (!digits) {
|
||||
return true;
|
||||
}
|
||||
return phoneNumberMatchesDialCode(phoneNumber, getDisplayedDialCode());
|
||||
}
|
||||
|
||||
function toNationalPhoneNumber(value, dialCode) {
|
||||
const digits = normalizePhoneDigits(value);
|
||||
const normalizedDialCode = normalizePhoneDigits(dialCode);
|
||||
@@ -364,15 +381,17 @@
|
||||
|
||||
const byLabel = findCountryOptionByLabel(countryLabel);
|
||||
if (await trySelectCountryOption(select, byLabel)) {
|
||||
return true;
|
||||
if (selectedCountryMatchesPhoneNumber(phoneNumber)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const byPhoneNumber = findCountryOptionByPhoneNumber(phoneNumber);
|
||||
if (await trySelectCountryOption(select, byPhoneNumber)) {
|
||||
return true;
|
||||
return selectedCountryMatchesPhoneNumber(phoneNumber);
|
||||
}
|
||||
|
||||
return Boolean(getSelectedCountryOption());
|
||||
return selectedCountryMatchesPhoneNumber(phoneNumber);
|
||||
}
|
||||
|
||||
function getAddPhoneSubmitButton() {
|
||||
|
||||
@@ -366,7 +366,6 @@
|
||||
routing_plan_id: activation?.madaoRoutingPlanId,
|
||||
routing_plan_name: activation?.madaoRoutingPlanName,
|
||||
service: activation?.serviceCode,
|
||||
country: activation?.countryId,
|
||||
});
|
||||
if (!nextTicket) {
|
||||
throw new Error('MaDao 返回的下一条路由激活记录无效。');
|
||||
|
||||
@@ -356,6 +356,68 @@ test('phone auth can auto-select country by dial code even when number has no pl
|
||||
}
|
||||
});
|
||||
|
||||
test('phone auth rejects stale selected country when it does not match phone dial code', async () => {
|
||||
const originalDocument = global.document;
|
||||
const originalEvent = global.Event;
|
||||
const originalLocation = global.location;
|
||||
|
||||
const dom = createFakeAddPhoneDom({
|
||||
options: [
|
||||
{ value: 'TH', textContent: 'Thailand (+66)', buttonText: 'Thailand (+66)' },
|
||||
{ value: 'GB', textContent: 'United Kingdom (+44)', buttonText: 'United Kingdom (+44)' },
|
||||
],
|
||||
selectedIndex: 0,
|
||||
});
|
||||
|
||||
global.document = dom.document;
|
||||
global.Event = class Event {
|
||||
constructor(type) {
|
||||
this.type = type;
|
||||
}
|
||||
};
|
||||
global.location = { href: 'https://auth.openai.com/add-phone' };
|
||||
|
||||
try {
|
||||
const helpers = api.createPhoneAuthHelpers({
|
||||
fillInput: (element, value) => {
|
||||
element.value = value;
|
||||
},
|
||||
getActionText: () => '',
|
||||
getPageTextSnapshot: () => '',
|
||||
getVerificationErrorText: () => '',
|
||||
humanPause: async () => {},
|
||||
isActionEnabled: () => true,
|
||||
isAddPhonePageReady: () => true,
|
||||
isConsentReady: () => false,
|
||||
isPhoneVerificationPageReady: () => false,
|
||||
isVisibleElement: () => true,
|
||||
simulateClick: (element) => {
|
||||
element.click?.();
|
||||
},
|
||||
sleep: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
waitForElement: async () => null,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => helpers.submitPhoneNumber({
|
||||
countryLabel: 'Country #10',
|
||||
phoneNumber: '+84943328460',
|
||||
}),
|
||||
/Failed to select "Country #10" on the add-phone page\./
|
||||
);
|
||||
|
||||
assert.equal(dom.select.value, 'TH');
|
||||
assert.equal(dom.phoneInput.value, '');
|
||||
assert.equal(dom.hiddenPhoneInput.value, '');
|
||||
assert.equal(dom.wasSubmitClicked(), false);
|
||||
} finally {
|
||||
global.document = originalDocument;
|
||||
global.Event = originalEvent;
|
||||
global.location = originalLocation;
|
||||
}
|
||||
});
|
||||
|
||||
test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of endless Try-again loop', async () => {
|
||||
const originalDocument = global.document;
|
||||
const originalLocation = global.location;
|
||||
|
||||
@@ -285,7 +285,6 @@ test('phone verification helper acquires, polls and releases MaDao activation th
|
||||
};
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
createMaDaoProvider: maDaoModule.createProvider,
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsedUrl = new URL(url);
|
||||
@@ -365,6 +364,161 @@ test('phone verification helper acquires, polls and releases MaDao activation th
|
||||
assert.deepStrictEqual(requests[2].body, { ticket_id: 'madao-1', action: 'finish' });
|
||||
});
|
||||
|
||||
test('phone verification helper keeps MaDao routing replacement country as ISO code on resubmit', async () => {
|
||||
const requests = [];
|
||||
const messages = [];
|
||||
let currentState = {
|
||||
phoneSmsProvider: 'madao',
|
||||
madaoBaseUrl: 'http://127.0.0.1:7822/',
|
||||
madaoHttpSecret: 'secret-token',
|
||||
madaoMode: 'routing_plan',
|
||||
madaoRoutingPlanId: 'rp-openai',
|
||||
verificationResendCount: 0,
|
||||
phoneVerificationReplacementLimit: 2,
|
||||
phoneCodeWaitSeconds: 15,
|
||||
phoneCodeTimeoutWindows: 1,
|
||||
phoneCodePollIntervalSeconds: 1,
|
||||
phoneCodePollMaxRounds: 1,
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: null,
|
||||
};
|
||||
let acquireCalls = 0;
|
||||
let replaceCalls = 0;
|
||||
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
const parsedUrl = new URL(url);
|
||||
const body = options.body ? JSON.parse(options.body) : null;
|
||||
requests.push({ url: parsedUrl, options, body });
|
||||
if (parsedUrl.pathname === '/api/acquire') {
|
||||
acquireCalls += 1;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({
|
||||
ticket_id: 'madao-ticket-routing-1',
|
||||
provider: 'herosms',
|
||||
service: 'openai',
|
||||
country: 'TH',
|
||||
phone_number: '+66950002222',
|
||||
routing_plan_id: 'rp-openai',
|
||||
routing_plan_name: 'OpenAI Plan',
|
||||
routing_item_id: 'route-1',
|
||||
status: 'waiting_code',
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (parsedUrl.pathname === '/api/poll') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({
|
||||
ticket_id: body.ticket_id,
|
||||
provider: body.ticket_id === 'madao-ticket-routing-1' ? 'herosms' : 'smsbower',
|
||||
status: body.ticket_id === 'madao-ticket-routing-1' ? 'waiting_code' : 'code_received',
|
||||
code: body.ticket_id === 'madao-ticket-routing-1' ? null : '654321',
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (parsedUrl.pathname === '/api/routing/replace') {
|
||||
replaceCalls += 1;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({
|
||||
current_ticket_id: 'madao-ticket-routing-1',
|
||||
current_ticket_release: {
|
||||
ticket_id: 'madao-ticket-routing-1',
|
||||
provider: 'herosms',
|
||||
status: 'cancelled',
|
||||
},
|
||||
next_ticket: {
|
||||
ticket_id: 'madao-ticket-routing-2',
|
||||
provider: 'smsbower',
|
||||
service: 'openai',
|
||||
country: 'VN',
|
||||
phone_number: '+84943328460',
|
||||
routing_plan_id: 'rp-openai',
|
||||
routing_plan_name: 'OpenAI Plan',
|
||||
routing_item_id: 'route-2',
|
||||
status: 'waiting_code',
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (parsedUrl.pathname === '/api/release') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ ok: true }),
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected MaDao path: ${parsedUrl.pathname}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
messages.push(message);
|
||||
if (message.type === 'STEP8_GET_STATE') {
|
||||
return {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
};
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return {
|
||||
phoneVerificationPage: true,
|
||||
url: 'https://auth.openai.com/phone-verification',
|
||||
};
|
||||
}
|
||||
if (message.type === 'RETURN_TO_ADD_PHONE') {
|
||||
return {
|
||||
addPhonePage: true,
|
||||
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 () => {},
|
||||
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.equal(acquireCalls, 1);
|
||||
assert.equal(replaceCalls, 1);
|
||||
const phoneSubmissions = messages.filter((message) => message.type === 'SUBMIT_PHONE_NUMBER');
|
||||
assert.equal(phoneSubmissions.length, 2);
|
||||
assert.equal(phoneSubmissions[0].payload.phoneNumber, '+66950002222');
|
||||
assert.equal(phoneSubmissions[0].payload.countryId, 'TH');
|
||||
assert.equal(phoneSubmissions[0].payload.countryLabel, 'Thailand');
|
||||
assert.equal(phoneSubmissions[1].payload.phoneNumber, '+84943328460');
|
||||
assert.equal(phoneSubmissions[1].payload.countryId, 'VN');
|
||||
assert.equal(phoneSubmissions[1].payload.countryLabel, 'Vietnam');
|
||||
});
|
||||
|
||||
test('signup phone helper persists signup runtime state without touching add-phone activation', async () => {
|
||||
const setStateCalls = [];
|
||||
let currentState = {
|
||||
|
||||
Reference in New Issue
Block a user