feat: 手动配置 HeroSMS maxPrice 并调整手机号复用计数

This commit is contained in:
zhangkun
2026-04-28 02:51:48 +08:00
parent 0f430113fa
commit 127fd1c653
6 changed files with 264 additions and 396 deletions
+3
View File
@@ -477,6 +477,7 @@ const PERSISTED_SETTING_DEFAULTS = {
hotmailAccounts: [],
mail2925Accounts: [],
heroSmsApiKey: '',
heroSmsMaxPrice: '',
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
};
@@ -1316,6 +1317,8 @@ function normalizePersistentSettingValue(key, value) {
return normalizeMail2925Accounts(value);
case 'heroSmsApiKey':
return String(value || '');
case 'heroSmsMaxPrice':
return String(value || '').trim();
case 'heroSmsCountryId':
return Math.max(1, Math.floor(Number(value) || HERO_SMS_COUNTRY_ID));
case 'heroSmsCountryLabel':
+59 -144
View File
@@ -27,7 +27,6 @@
const DEFAULT_PHONE_SUBMIT_ATTEMPTS = 3;
const DEFAULT_PHONE_CODE_WAIT_WINDOW_MS = 60000;
const DEFAULT_PHONE_NUMBER_MAX_USES = 3;
const DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS = 3;
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
@@ -47,6 +46,18 @@
return String(value || '').trim();
}
function normalizeManualHeroSmsMaxPrice(value) {
const trimmed = String(value ?? '').trim();
if (!trimmed) {
return null;
}
const price = Number(trimmed);
if (!Number.isFinite(price) || price <= 0) {
return null;
}
return String(price);
}
function normalizeUseCount(value) {
return Math.max(0, Math.floor(Number(value) || 0));
}
@@ -241,8 +252,13 @@
if (!apiKey) {
throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.');
}
const maxPrice = normalizeManualHeroSmsMaxPrice(state.heroSmsMaxPrice);
if (!maxPrice) {
throw new Error('HeroSMS maxPrice is missing. Fill it in below the country selector before running the phone flow.');
}
return {
apiKey,
maxPrice,
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
};
}
@@ -289,91 +305,10 @@
return activation?.statusAction === 'getStatusV2' ? 'getStatusV2' : 'getStatus';
}
function normalizeHeroSmsPrice(value) {
const price = Number(value);
if (!Number.isFinite(price) || price < 0) {
return null;
}
return price;
}
function collectHeroSmsPriceCandidates(payload, candidates = []) {
if (Array.isArray(payload)) {
payload.forEach((entry) => collectHeroSmsPriceCandidates(entry, candidates));
return candidates;
}
if (!payload || typeof payload !== 'object') {
return candidates;
}
const cost = normalizeHeroSmsPrice(payload.cost);
if (cost !== null) {
const count = Number(payload.count);
const physicalCount = Number(payload.physicalCount);
const hasCount = Number.isFinite(count);
const hasPhysicalCount = Number.isFinite(physicalCount);
if ((!hasCount && !hasPhysicalCount) || count > 0 || physicalCount > 0) {
candidates.push(cost);
}
}
Object.values(payload).forEach((value) => collectHeroSmsPriceCandidates(value, candidates));
return candidates;
}
function findLowestHeroSmsPrice(payload) {
const candidates = collectHeroSmsPriceCandidates(payload, []);
if (!candidates.length) {
return null;
}
return Math.min(...candidates);
}
function isHeroSmsNoNumbersPayload(payload) {
return /\bNO_NUMBERS\b/i.test(describeHeroSmsPayload(payload));
}
function extractHeroSmsWrongMaxPrice(payload) {
if (payload && typeof payload === 'object') {
const title = String(payload.title || '').trim();
const minPrice = normalizeHeroSmsPrice(payload.info?.min);
if (/^WRONG_MAX_PRICE$/i.test(title) && minPrice !== null) {
return minPrice;
}
}
const text = describeHeroSmsPayload(payload);
const match = text.match(/\bWRONG_MAX_PRICE:(\d+(?:\.\d+)?)\b/i);
if (!match) {
return null;
}
return normalizeHeroSmsPrice(match[1]);
}
function isNetworkFetchFailure(error) {
const message = String(error?.message || '').trim();
return /failed to fetch|networkerror|load failed/i.test(message);
}
async function resolveCheapestPhoneActivationPrice(config, countryConfig) {
for (let attempt = 1; attempt <= DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS; attempt += 1) {
try {
const payload = await fetchHeroSmsPayload(config, {
action: 'getPrices',
service: HERO_SMS_SERVICE_CODE,
country: countryConfig.id,
}, 'HeroSMS getPrices');
const price = findLowestHeroSmsPrice(payload);
if (price !== null) {
return price;
}
} catch (_) {
// Best-effort lookup only.
}
}
return null;
}
async function fetchPhoneActivationPayload(config, countryConfig, action, options = {}) {
const query = {
action,
@@ -387,49 +322,10 @@
return fetchHeroSmsPayload(config, query, `HeroSMS ${action}`);
}
async function requestPhoneActivationWithPrice(config, countryConfig, action, maxPrice) {
let nextMaxPrice = maxPrice;
let retriedWithUpdatedPrice = false;
let retriedWithoutPrice = false;
while (true) {
try {
return await fetchPhoneActivationPayload(config, countryConfig, action, {
maxPrice: nextMaxPrice,
});
} catch (error) {
const updatedMaxPrice = extractHeroSmsWrongMaxPrice(error?.payload || error?.message);
if (
nextMaxPrice !== null
&& nextMaxPrice !== undefined
&& !retriedWithUpdatedPrice
&& updatedMaxPrice !== null
) {
nextMaxPrice = updatedMaxPrice;
retriedWithUpdatedPrice = true;
continue;
}
if (
nextMaxPrice !== null
&& nextMaxPrice !== undefined
&& !retriedWithoutPrice
&& isNetworkFetchFailure(error)
) {
nextMaxPrice = null;
retriedWithoutPrice = true;
continue;
}
throw error;
}
}
}
async function requestPhoneActivation(state = {}) {
const config = resolvePhoneConfig(state);
const countryConfig = resolveCountryConfig(state);
const maxPrice = await resolveCheapestPhoneActivationPrice(config, countryConfig);
const maxPrice = config.maxPrice;
const buildFallbackActivation = (requestAction) => ({
countryId: countryConfig.id,
...(requestAction === 'getNumberV2' ? { statusAction: 'getStatusV2' } : {}),
@@ -438,19 +334,19 @@
let payload;
try {
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
} catch (error) {
if (!isHeroSmsNoNumbersPayload(error?.payload || error?.message)) {
throw error;
}
requestAction = 'getNumberV2';
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
}
let activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
if (!activation && requestAction === 'getNumber' && isHeroSmsNoNumbersPayload(payload)) {
requestAction = 'getNumberV2';
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
}
@@ -496,7 +392,7 @@
}
async function completePhoneActivation(state = {}, activation) {
await setPhoneActivationStatus(state, activation, 6, 'HeroSMS setStatus(6)');
await setPhoneActivationStatus(state, activation, 3, 'HeroSMS setStatus(3)');
}
async function cancelPhoneActivation(state = {}, activation) {
@@ -738,6 +634,20 @@
await persistReusableActivation(null);
}
async function incrementCurrentActivationUseCount(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
return null;
}
const nextActivation = {
...normalizedActivation,
successfulUses: Math.min(normalizedActivation.successfulUses + 1, normalizedActivation.maxUses),
};
await persistCurrentActivation(nextActivation);
return nextActivation;
}
async function acquirePhoneActivation(state = {}) {
const countryConfig = resolveCountryConfig(state);
const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]);
@@ -769,28 +679,24 @@
return activation;
}
async function markActivationReusableAfterSuccess(activation) {
async function syncReusableActivationAfterUse(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
await clearReusableActivation();
return;
}
const successfulUses = normalizedActivation.successfulUses + 1;
if (successfulUses >= normalizedActivation.maxUses) {
if (normalizedActivation.successfulUses >= normalizedActivation.maxUses) {
await clearReusableActivation();
return;
}
await persistReusableActivation({
...normalizedActivation,
successfulUses,
});
await persistReusableActivation(normalizedActivation);
}
async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
let currentActivation = normalizeActivation(activation);
if (!currentActivation) {
throw new Error('Phone activation is missing.');
}
@@ -799,11 +705,11 @@
for (let windowIndex = 1; windowIndex <= 2; windowIndex += 1) {
await addLog(
`Step 9: waiting up to 60 seconds for SMS on ${normalizedActivation.phoneNumber} (${windowIndex}/2).`,
`Step 9: waiting up to 60 seconds for SMS on ${currentActivation.phoneNumber} (${windowIndex}/2).`,
'info'
);
try {
const code = await pollPhoneActivationCode(state, normalizedActivation, {
const code = await pollPhoneActivationCode(state, currentActivation, {
actionLabel: windowIndex === 1
? 'poll phone verification code from HeroSMS'
: 'poll resent phone verification code from HeroSMS',
@@ -820,7 +726,7 @@
lastLoggedStatus = statusText;
lastLoggedPollCount = pollCount;
await addLog(
`Step 9: HeroSMS status for ${normalizedActivation.phoneNumber}: ${statusText} (${Math.ceil(elapsedMs / 1000)}s elapsed).`,
`Step 9: HeroSMS status for ${currentActivation.phoneNumber}: ${statusText} (${Math.ceil(elapsedMs / 1000)}s elapsed).`,
'info'
);
},
@@ -836,10 +742,10 @@
if (windowIndex === 1) {
await addLog(
`Step 9: no SMS arrived for ${normalizedActivation.phoneNumber} within 60 seconds, requesting another SMS.`,
`Step 9: no SMS arrived for ${currentActivation.phoneNumber} within 60 seconds, requesting another SMS.`,
'warn'
);
await requestAdditionalPhoneSms(state, normalizedActivation);
await requestAdditionalPhoneSms(state, currentActivation);
try {
await resendPhoneVerificationCode(tabId);
await addLog('Step 9: clicked "Resend text message" on the phone verification page.', 'info');
@@ -850,10 +756,10 @@
}
await addLog(
`Step 9: still no SMS for ${normalizedActivation.phoneNumber} 60 seconds after resend, restarting from step 7 with a new number.`,
`Step 9: still no SMS for ${currentActivation.phoneNumber} 60 seconds after resend, restarting from step 7 with a new number.`,
'warn'
);
throw buildPhoneRestartStep7Error(normalizedActivation.phoneNumber);
throw buildPhoneRestartStep7Error(currentActivation.phoneNumber);
}
}
@@ -965,8 +871,17 @@
continue;
}
await completePhoneActivation(state, activation);
await markActivationReusableAfterSuccess(activation);
activation = await incrementCurrentActivationUseCount(activation);
try {
await completePhoneActivation(state, activation);
await syncReusableActivationAfterUse(activation);
} catch (activationStatusError) {
await clearReusableActivation();
await addLog(
`Step 9: phone verification succeeded, but HeroSMS setStatus(3) failed. The next flow will request a new number. ${activationStatusError.message}`,
'warn'
);
}
shouldCancelActivation = false;
await clearCurrentActivation();
await addLog('Step 9: phone verification finished, waiting for OAuth consent.', 'ok');
+5
View File
@@ -569,6 +569,11 @@
<option value="52" selected>Thailand</option>
</select>
</div>
<div class="data-row" id="row-hero-sms-max-price" style="display:none;">
<span class="data-label">最高价格</span>
<input type="number" id="input-hero-sms-max-price" class="data-input mono" min="0.0001" step="0.0001" required
placeholder="必填,例如 0.08" />
</div>
<div class="data-row" id="row-hero-sms-api-key" style="display:none;">
<span class="data-label">接码 API</span>
<input type="password" id="input-hero-sms-api-key" class="data-input mono" placeholder="请输入 HeroSMS API Key" />
+34 -1
View File
@@ -292,8 +292,10 @@ const rowPhoneVerificationEnabled = document.getElementById('row-phone-verificat
const inputPhoneVerificationEnabled = document.getElementById('input-phone-verification-enabled');
const rowHeroSmsPlatform = document.getElementById('row-hero-sms-platform');
const rowHeroSmsCountry = document.getElementById('row-hero-sms-country');
const rowHeroSmsMaxPrice = document.getElementById('row-hero-sms-max-price');
const rowHeroSmsApiKey = document.getElementById('row-hero-sms-api-key');
const inputHeroSmsApiKey = document.getElementById('input-hero-sms-api-key');
const inputHeroSmsMaxPrice = document.getElementById('input-hero-sms-max-price');
const selectHeroSmsCountry = document.getElementById('select-hero-sms-country');
const displayHeroSmsPlatform = document.getElementById('display-hero-sms-platform');
const rowAccountRunHistoryHelperBaseUrl = document.getElementById('row-account-run-history-helper-base-url');
@@ -2200,6 +2202,9 @@ function collectSettingsPayload() {
const heroSmsApiKeyValue = typeof inputHeroSmsApiKey !== 'undefined' && inputHeroSmsApiKey
? (inputHeroSmsApiKey.value || '')
: '';
const heroSmsMaxPriceValue = typeof inputHeroSmsMaxPrice !== 'undefined' && inputHeroSmsMaxPrice
? normalizeHeroSmsMaxPriceValue(inputHeroSmsMaxPrice.value)
: '';
const heroSmsCountry = typeof getSelectedHeroSmsCountryOption === 'function'
? getSelectedHeroSmsCountryOption()
: {
@@ -2299,6 +2304,7 @@ function collectSettingsPayload() {
DEFAULT_VERIFICATION_RESEND_COUNT
),
heroSmsApiKey: heroSmsApiKeyValue,
heroSmsMaxPrice: heroSmsMaxPriceValue,
heroSmsCountryId: heroSmsCountry.id,
heroSmsCountryLabel: heroSmsCountry.label,
};
@@ -2357,6 +2363,18 @@ function normalizeHeroSmsCountryLabel(value = '') {
return String(value || '').trim() || DEFAULT_HERO_SMS_COUNTRY_LABEL;
}
function normalizeHeroSmsMaxPriceValue(value, fallback = '') {
const trimmed = String(value ?? '').trim();
if (!trimmed) {
return String(fallback || '').trim();
}
const numeric = Number(trimmed);
if (!Number.isFinite(numeric) || numeric <= 0) {
return String(fallback || '').trim();
}
return String(numeric);
}
function getSelectedHeroSmsCountryOption() {
if (!selectHeroSmsCountry) {
return {
@@ -2472,7 +2490,7 @@ function updateAccountRunHistorySettingsUI() {
function updatePhoneVerificationSettingsUI() {
const enabled = Boolean(inputPhoneVerificationEnabled?.checked);
[rowHeroSmsPlatform, rowHeroSmsCountry, rowHeroSmsApiKey].forEach((row) => {
[rowHeroSmsPlatform, rowHeroSmsCountry, rowHeroSmsMaxPrice, rowHeroSmsApiKey].forEach((row) => {
if (!row) {
return;
}
@@ -2983,6 +3001,9 @@ function applySettingsState(state) {
if (inputHeroSmsApiKey) {
inputHeroSmsApiKey.value = state?.heroSmsApiKey || '';
}
if (inputHeroSmsMaxPrice) {
inputHeroSmsMaxPrice.value = normalizeHeroSmsMaxPriceValue(state?.heroSmsMaxPrice);
}
if (selectHeroSmsCountry) {
const restoredCountryId = String(normalizeHeroSmsCountryId(state?.heroSmsCountryId));
if (Array.from(selectHeroSmsCountry.options).some((option) => option.value === restoredCountryId)) {
@@ -6413,6 +6434,15 @@ inputHeroSmsApiKey?.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
inputHeroSmsMaxPrice?.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
});
inputHeroSmsMaxPrice?.addEventListener('blur', () => {
inputHeroSmsMaxPrice.value = normalizeHeroSmsMaxPriceValue(inputHeroSmsMaxPrice.value);
saveSettings({ silent: true }).catch(() => { });
});
selectHeroSmsCountry?.addEventListener('change', () => {
updateHeroSmsPlatformDisplay(getSelectedHeroSmsCountryOption().label);
markSettingsDirty(true);
@@ -6811,6 +6841,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.payload.heroSmsApiKey !== undefined && inputHeroSmsApiKey) {
inputHeroSmsApiKey.value = message.payload.heroSmsApiKey || '';
}
if (message.payload.heroSmsMaxPrice !== undefined && inputHeroSmsMaxPrice) {
inputHeroSmsMaxPrice.value = normalizeHeroSmsMaxPriceValue(message.payload.heroSmsMaxPrice);
}
if (message.payload.phoneVerificationEnabled !== undefined && inputPhoneVerificationEnabled) {
inputPhoneVerificationEnabled.checked = Boolean(message.payload.phoneVerificationEnabled);
updatePhoneVerificationSettingsUI();
+155 -251
View File
@@ -32,7 +32,7 @@ function buildHeroSmsStatusV2Payload({ smsCode = '', smsText = '', callCode = ''
});
}
test('phone verification helper requests HeroSMS numbers with fixed OpenAI and Thailand parameters', async () => {
test('phone verification helper requests HeroSMS numbers with manual maxPrice and fixed OpenAI/Thailand parameters', async () => {
const requests = [];
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
@@ -40,26 +40,22 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
return {
ok: true,
text: async () => 'ACCESS_NUMBER:123456:66959916439',
};
},
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08' }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
const activation = await helpers.requestPhoneActivation({
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
});
assert.deepStrictEqual(activation, {
activationId: '123456',
@@ -70,111 +66,33 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
successfulUses: 0,
maxUses: 3,
});
assert.equal(requests.length, 2);
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
assert.equal(requests.length, 1);
assert.equal(requests[0].searchParams.get('action'), 'getNumber');
assert.equal(requests[0].searchParams.get('service'), 'dr');
assert.equal(requests[0].searchParams.get('country'), '52');
assert.equal(requests[0].searchParams.get('api_key'), 'demo-key');
assert.equal(requests[1].searchParams.get('action'), 'getNumber');
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
assert.equal(requests[1].searchParams.get('service'), 'dr');
assert.equal(requests[1].searchParams.get('country'), '52');
assert.equal(requests[1].searchParams.get('api_key'), 'demo-key');
assert.equal(requests[0].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[0].searchParams.get('fixedPrice'), 'true');
});
test('phone verification helper retries HeroSMS getPrices until it receives a usable lowest price', async () => {
const requests = [];
let getPricesAttempt = 0;
test('phone verification helper requires manual HeroSMS maxPrice', async () => {
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');
if (action === 'getPrices') {
getPricesAttempt += 1;
return getPricesAttempt < 3
? {
ok: true,
text: async () => JSON.stringify({ unavailable: true }),
}
: {
ok: true,
text: async () => buildHeroSmsPricesPayload({ cost: 0.09 }),
};
}
if (action === 'getNumber') {
return {
ok: true,
text: async () => 'ACCESS_NUMBER:123456:66959916439',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
fetchImpl: async () => {
throw new Error('should not request HeroSMS without maxPrice');
},
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '' }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
assert.equal(requests.length, 4);
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
assert.equal(requests[1].searchParams.get('action'), 'getPrices');
assert.equal(requests[2].searchParams.get('action'), 'getPrices');
assert.equal(requests[3].searchParams.get('action'), 'getNumber');
assert.equal(requests[3].searchParams.get('maxPrice'), '0.09');
assert.equal(requests[3].searchParams.get('fixedPrice'), 'true');
});
test('phone verification helper falls back to plain getNumber only after HeroSMS getPrices fails three times', async () => {
const requests = [];
let getPricesAttempt = 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');
if (action === 'getPrices') {
getPricesAttempt += 1;
return {
ok: true,
text: async () => JSON.stringify({ unavailable: getPricesAttempt }),
};
}
if (action === 'getNumber') {
return {
ok: true,
text: async () => 'ACCESS_NUMBER:123456:66959916439',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
assert.equal(requests.length, 4);
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
assert.equal(requests[1].searchParams.get('action'), 'getPrices');
assert.equal(requests[2].searchParams.get('action'), 'getPrices');
assert.equal(requests[2].searchParams.get('service'), 'dr');
assert.equal(requests[2].searchParams.get('country'), '52');
assert.equal(requests[2].searchParams.get('api_key'), 'demo-key');
assert.equal(requests[3].searchParams.get('action'), 'getNumber');
assert.equal(requests[3].searchParams.get('maxPrice'), null);
assert.equal(requests[3].searchParams.get('fixedPrice'), null);
await assert.rejects(
() => helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '' }),
/HeroSMS maxPrice is missing/i
);
});
test('phone verification helper retries with HeroSMS getNumberV2 when getNumber reports NO_NUMBERS', async () => {
@@ -186,12 +104,6 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload({ country: '16' }),
};
}
if (action === 'getNumber') {
return {
ok: true,
@@ -209,7 +121,7 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsCountryId: 16 }),
getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08', heroSmsCountryId: 16 }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
@@ -218,6 +130,7 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
const activation = await helpers.requestPhoneActivation({
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16,
});
@@ -231,17 +144,15 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
maxUses: 3,
statusAction: 'getStatusV2',
});
assert.equal(requests.length, 3);
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
assert.equal(requests.length, 2);
assert.equal(requests[0].searchParams.get('action'), 'getNumber');
assert.equal(requests[0].searchParams.get('country'), '16');
assert.equal(requests[1].searchParams.get('action'), 'getNumber');
assert.equal(requests[0].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[0].searchParams.get('fixedPrice'), 'true');
assert.equal(requests[1].searchParams.get('action'), 'getNumberV2');
assert.equal(requests[1].searchParams.get('country'), '16');
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
assert.equal(requests[2].searchParams.get('action'), 'getNumberV2');
assert.equal(requests[2].searchParams.get('country'), '16');
assert.equal(requests[2].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[2].searchParams.get('fixedPrice'), 'true');
});
test('phone verification helper uses HeroSMS getStatusV2 after acquiring a number via getNumberV2', async () => {
@@ -249,6 +160,7 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
const stateUpdates = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16,
heroSmsCountryLabel: 'United Kingdom',
verificationResendCount: 0,
@@ -264,12 +176,6 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload({ country: '16' }),
};
}
if (action === 'getNumber') {
return {
ok: true,
@@ -354,6 +260,18 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
statusAction: 'getStatusV2',
},
},
{
currentPhoneActivation: {
activationId: '654321',
phoneNumber: '447911123456',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 16,
successfulUses: 1,
maxUses: 3,
statusAction: 'getStatusV2',
},
},
{
reusablePhoneActivation: {
activationId: '654321',
@@ -372,7 +290,6 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
]);
const actions = requests.map((url) => url.searchParams.get('action'));
assert.deepStrictEqual(actions, [
'getPrices',
'getNumber',
'getNumberV2',
'getStatusV2',
@@ -381,117 +298,32 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
]);
});
test('phone verification helper refreshes maxPrice when HeroSMS returns WRONG_MAX_PRICE', async () => {
const requests = [];
let getNumberAttempt = 0;
test('phone verification helper keeps the user-provided maxPrice and surfaces HeroSMS price errors', async () => {
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');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
if (action === 'getNumber') {
getNumberAttempt += 1;
return getNumberAttempt === 1
? {
ok: false,
text: async () => 'WRONG_MAX_PRICE:0.09',
}
: {
ok: true,
text: async () => 'ACCESS_NUMBER:123456:66959916439',
};
return {
ok: false,
text: async () => 'WRONG_MAX_PRICE:0.09',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08' }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
assert.deepStrictEqual(activation, {
activationId: '123456',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
});
assert.equal(requests.length, 3);
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
assert.equal(requests[1].searchParams.get('action'), 'getNumber');
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[2].searchParams.get('action'), 'getNumber');
assert.equal(requests[2].searchParams.get('maxPrice'), '0.09');
assert.equal(requests[2].searchParams.get('fixedPrice'), 'true');
});
test('phone verification helper falls back to plain getNumber when priced request fails to fetch', async () => {
const requests = [];
let getNumberAttempt = 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');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
if (action === 'getNumber') {
getNumberAttempt += 1;
if (getNumberAttempt === 1) {
throw new TypeError('Failed to fetch');
}
return {
ok: true,
text: async () => 'ACCESS_NUMBER:123456:66959916439',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => ({ heroSmsApiKey: 'demo-key' }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
assert.deepStrictEqual(activation, {
activationId: '123456',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
});
assert.equal(requests.length, 3);
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
assert.equal(requests[1].searchParams.get('action'), 'getNumber');
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
assert.equal(requests[2].searchParams.get('action'), 'getNumber');
assert.equal(requests[2].searchParams.get('maxPrice'), null);
assert.equal(requests[2].searchParams.get('fixedPrice'), null);
await assert.rejects(
() => helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08' }),
/WRONG_MAX_PRICE:0.09/
);
});
test('phone verification helper completes add-phone flow, clears current activation, and stores reusable number state', async () => {
@@ -499,6 +331,7 @@ test('phone verification helper completes add-phone flow, clears current activat
const stateUpdates = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
verificationResendCount: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
@@ -511,12 +344,6 @@ test('phone verification helper completes add-phone flow, clears current activat
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
if (action === 'getNumber') {
return {
ok: true,
@@ -586,6 +413,17 @@ test('phone verification helper completes add-phone flow, clears current activat
maxUses: 3,
},
},
{
currentPhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 1,
maxUses: 3,
},
},
{
reusablePhoneActivation: {
activationId: '123456',
@@ -603,7 +441,86 @@ test('phone verification helper completes add-phone flow, clears current activat
]);
const actions = requests.map((url) => url.searchParams.get('action'));
assert.deepStrictEqual(actions, ['getPrices', 'getNumber', 'getStatus', 'setStatus']);
assert.deepStrictEqual(actions, ['getNumber', 'getStatus', 'setStatus']);
});
test('phone verification helper still succeeds when HeroSMS setStatus(3) fails after a successful submit', async () => {
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
verificationResendCount: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
};
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');
if (action === 'getNumber') {
return {
ok: true,
text: async () => 'ACCESS_NUMBER:123456:66959916439',
};
}
if (action === 'getStatus') {
return {
ok: true,
text: async () => 'STATUS_OK:654321',
};
}
if (action === 'setStatus') {
return {
ok: false,
text: async () => 'TEMPORARY_ERROR',
};
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'SUBMIT_PHONE_NUMBER') {
return {
phoneVerificationPage: true,
url: 'https://auth.openai.com/phone-verification',
};
}
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.deepStrictEqual(currentState.currentPhoneActivation, null);
assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
const actions = requests.map((url) => url.searchParams.get('action'));
assert.deepStrictEqual(actions, ['getNumber', 'getStatus', 'setStatus']);
});
test('phone verification helper uses the configured HeroSMS country for both number acquisition and add-phone submission', async () => {
@@ -611,6 +528,7 @@ test('phone verification helper uses the configured HeroSMS country for both num
const submittedPayloads = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16,
heroSmsCountryLabel: 'United Kingdom',
verificationResendCount: 0,
@@ -625,12 +543,6 @@ test('phone verification helper uses the configured HeroSMS country for both num
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload({ country: '16' }),
};
}
if (action === 'getNumber') {
return {
ok: true,
@@ -688,12 +600,10 @@ test('phone verification helper uses the configured HeroSMS country for both num
consentReady: true,
url: 'https://auth.openai.com/authorize',
});
assert.equal(requests[0].searchParams.get('action'), 'getPrices');
assert.equal(requests[0].searchParams.get('action'), 'getNumber');
assert.equal(requests[0].searchParams.get('country'), '16');
assert.equal(requests[1].searchParams.get('action'), 'getNumber');
assert.equal(requests[1].searchParams.get('country'), '16');
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
assert.equal(requests[0].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[0].searchParams.get('fixedPrice'), 'true');
assert.deepStrictEqual(submittedPayloads, [{
phoneNumber: '447911123456',
countryId: 16,
@@ -704,8 +614,10 @@ test('phone verification helper uses the configured HeroSMS country for both num
test('phone verification helper throws a step-7 restart error after 60 seconds plus one resend window without SMS', async () => {
const requests = [];
const messages = [];
const stateUpdates = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
verificationResendCount: 0,
currentPhoneActivation: null,
reusablePhoneActivation: null,
@@ -725,13 +637,6 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
if (action === 'getNumber') {
return {
ok: true,
@@ -775,6 +680,7 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
stateUpdates.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {
@@ -799,7 +705,6 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
assert.deepStrictEqual(actions, [
'getPrices:',
'getNumber:',
'getStatus:123456',
'setStatus:123456',
@@ -807,6 +712,11 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
'setStatus:123456',
]);
assert.equal(currentState.currentPhoneActivation, null);
assert.equal(
stateUpdates.some((updates) => Number(updates.currentPhoneActivation?.successfulUses) > 0),
false,
'60 seconds without SMS should not increment reuse count'
);
} finally {
Date.now = realDateNow;
}
@@ -817,6 +727,7 @@ test('phone verification helper replaces the number when code submission returns
const messages = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
verificationResendCount: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
@@ -838,13 +749,6 @@ test('phone verification helper replaces the number when code submission returns
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;
@@ -925,11 +829,9 @@ test('phone verification helper replaces the number when code submission returns
const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
assert.deepStrictEqual(actions, [
'getPrices:',
'getNumber:',
'getStatus:111111',
'setStatus:111111',
'getPrices:',
'getNumber:',
'getStatus:222222',
'setStatus:222222',
@@ -950,6 +852,7 @@ test('phone verification helper reuses the same number up to three successful re
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
verificationResendCount: 0,
currentPhoneActivation: null,
reusablePhoneActivation: {
@@ -1038,6 +941,7 @@ test('phone verification helper keeps maxUses behavior for reused V2 activations
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16,
heroSmsCountryLabel: 'United Kingdom',
verificationResendCount: 0,
@@ -59,6 +59,7 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
assert.match(html, /id="input-phone-verification-enabled"/);
assert.match(html, /id="row-hero-sms-platform"/);
assert.match(html, /id="row-hero-sms-country"/);
assert.match(html, /id="row-hero-sms-max-price"/);
assert.match(html, /id="row-hero-sms-api-key"/);
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
});
@@ -68,6 +69,7 @@ test('updatePhoneVerificationSettingsUI toggles HeroSMS rows from the sms switch
const inputPhoneVerificationEnabled = { checked: false };
const rowHeroSmsPlatform = { style: { display: 'none' } };
const rowHeroSmsCountry = { style: { display: 'none' } };
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
const rowHeroSmsApiKey = { style: { display: 'none' } };
${extractFunction('updatePhoneVerificationSettingsUI')}
@@ -76,6 +78,7 @@ return {
inputPhoneVerificationEnabled,
rowHeroSmsPlatform,
rowHeroSmsCountry,
rowHeroSmsMaxPrice,
rowHeroSmsApiKey,
updatePhoneVerificationSettingsUI,
};
@@ -84,12 +87,14 @@ return {
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowHeroSmsPlatform.style.display, 'none');
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
api.inputPhoneVerificationEnabled.checked = true;
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowHeroSmsPlatform.style.display, '');
assert.equal(api.rowHeroSmsCountry.style.display, '');
assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
assert.equal(api.rowHeroSmsApiKey.style.display, '');
});
@@ -141,6 +146,7 @@ const inputAutoStepDelaySeconds = { value: '' };
const inputPhoneVerificationEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' };
const inputHeroSmsApiKey = { value: 'demo-key' };
const inputHeroSmsMaxPrice = { value: '0.08' };
const inputAccountRunHistoryHelperBaseUrl = { value: 'http://127.0.0.1:17373' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
@@ -169,6 +175,7 @@ function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Num
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${extractFunction('normalizeHeroSmsCountryId')}
${extractFunction('normalizeHeroSmsCountryLabel')}
${extractFunction('normalizeHeroSmsMaxPriceValue')}
${extractFunction('getSelectedHeroSmsCountryOption')}
${extractFunction('collectSettingsPayload')}
return { collectSettingsPayload };
@@ -180,6 +187,7 @@ return { collectSettingsPayload };
assert.equal(payload.accountRunHistoryTextEnabled, true);
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(payload.heroSmsApiKey, 'demo-key');
assert.equal(payload.heroSmsMaxPrice, '0.08');
assert.equal(payload.heroSmsCountryId, 52);
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
});