feat: add MaDao phone SMS provider

This commit is contained in:
QLHazyCoder
2026-05-29 00:44:30 +08:00
parent 4b739d91b5
commit 9ef9cb4c46
15 changed files with 3291 additions and 311 deletions
@@ -3,6 +3,8 @@ const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
const DEFAULT_MADAO_BASE_URL_FOR_TEST = 'http://127.0.0.1:7822';
const DEFAULT_MADAO_MODE_FOR_TEST = 'routing_plan';
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
@@ -65,6 +67,12 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeNexSmsCountryId'),
extractFunction('normalizeNexSmsCountryOrder'),
extractFunction('normalizeNexSmsServiceCode'),
extractFunction('normalizeMaDaoBaseUrl'),
extractFunction('normalizeMaDaoMode'),
extractFunction('normalizeMaDaoIdentifier'),
extractFunction('normalizeMaDaoProviderId'),
extractFunction('normalizeMaDaoCountry'),
extractFunction('normalizeMaDaoPrice'),
extractFunction('normalizePhonePreferredActivation'),
extractFunction('normalizePhoneVerificationReplacementLimit'),
extractFunction('normalizePhoneCodeWaitSeconds'),
@@ -119,8 +127,11 @@ const DEFAULT_HERO_SMS_OPERATOR = 'any';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms'];
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms', 'madao'];
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
const DEFAULT_MADAO_MODE = 'routing_plan';
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
@@ -293,8 +304,12 @@ return {
assert.equal(api.normalizePersistentSettingValue('kiroRsKey', ' key-1 '), 'key-1');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', '5SIM'), '5sim');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'MaDao'), 'madao');
assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms');
assert.deepStrictEqual(api.normalizePersistentSettingValue('phoneSmsProviderOrder', ['nexsms', '5sim', 'nexsms']), ['nexsms', '5sim']);
assert.deepStrictEqual(
api.normalizePersistentSettingValue('phoneSmsProviderOrder', ['madao', 'nexsms', '5sim', 'nexsms']),
['madao', 'nexsms', '5sim']
);
assert.equal(api.normalizePersistentSettingValue('phoneSmsReuseEnabled', false), false);
assert.equal(api.normalizePersistentSettingValue('phoneSmsReuseEnabled', true), true);
assert.equal(api.normalizePersistentSettingValue('fiveSimApiKey', ' demo-five '), ' demo-five ');
@@ -373,6 +388,22 @@ return {
[1, 6]
);
assert.equal(api.normalizePersistentSettingValue('nexSmsServiceCode', ' OT! '), 'ot');
assert.equal(
api.normalizePersistentSettingValue('madaoBaseUrl', 'http://127.0.0.1:7822/api/acquire?x=1'),
DEFAULT_MADAO_BASE_URL_FOR_TEST
);
assert.equal(api.normalizePersistentSettingValue('madaoBaseUrl', 'ftp://invalid'), DEFAULT_MADAO_BASE_URL_FOR_TEST);
assert.equal(api.normalizePersistentSettingValue('madaoHttpSecret', ' secret-token '), ' secret-token ');
assert.equal(api.normalizePersistentSettingValue('madaoMode', 'direct'), 'direct');
assert.equal(api.normalizePersistentSettingValue('madaoMode', 'unknown'), DEFAULT_MADAO_MODE_FOR_TEST);
assert.equal(api.normalizePersistentSettingValue('madaoRoutingPlanId', ' rp/openai! '), 'rp/openai!');
assert.equal(api.normalizePersistentSettingValue('madaoProviderId', ' Upstream A! '), 'upstreama');
assert.equal(api.normalizePersistentSettingValue('madaoCountry', ' gb '), 'GB');
assert.equal(api.normalizePersistentSettingValue('madaoCountry', 'ANY'), 'any');
assert.equal(api.normalizePersistentSettingValue('madaoAutoPickCountry', 1), true);
assert.equal(api.normalizePersistentSettingValue('madaoReusePhone', 0), false);
assert.equal(api.normalizePersistentSettingValue('madaoMinPrice', '0.123456'), '0.1235');
assert.equal(api.normalizePersistentSettingValue('madaoMaxPrice', '-1'), '');
const rangePayload = api.buildPersistentSettingsPayload({
heroSmsMinPrice: '0.023456',
fiveSimMinPrice: '0.0789',
@@ -412,6 +412,17 @@ return {
cloudMailToken: 'cloud-token',
gopayHelperApiKey: 'gpc-key',
heroSmsApiKey: 'hero-key',
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822',
madaoHttpSecret: 'madao-secret',
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'rp-openai',
madaoProviderId: 'upstream-a',
madaoCountry: 'GB',
madaoAutoPickCountry: false,
madaoReusePhone: true,
madaoMinPrice: '0.12',
madaoMaxPrice: '0.45',
hotmailAccounts: [
{ id: 'hotmail-1', email: 'owner@example.com', password: 'mail-pass' },
],
@@ -434,6 +445,17 @@ return {
assert.equal(api.getPayloadInput().cloudMailToken, 'cloud-token');
assert.equal(api.getPayloadInput().gopayHelperApiKey, 'gpc-key');
assert.equal(api.getPayloadInput().heroSmsApiKey, 'hero-key');
assert.equal(api.getPayloadInput().phoneSmsProvider, 'madao');
assert.equal(api.getPayloadInput().madaoBaseUrl, 'http://127.0.0.1:7822');
assert.equal(api.getPayloadInput().madaoHttpSecret, 'madao-secret');
assert.equal(api.getPayloadInput().madaoMode, 'routing_plan');
assert.equal(api.getPayloadInput().madaoRoutingPlanId, 'rp-openai');
assert.equal(api.getPayloadInput().madaoProviderId, 'upstream-a');
assert.equal(api.getPayloadInput().madaoCountry, 'GB');
assert.equal(api.getPayloadInput().madaoAutoPickCountry, false);
assert.equal(api.getPayloadInput().madaoReusePhone, true);
assert.equal(api.getPayloadInput().madaoMinPrice, '0.12');
assert.equal(api.getPayloadInput().madaoMaxPrice, '0.45');
assert.deepEqual(api.getPayloadInput().hotmailAccounts, [
{ id: 'hotmail-1', email: 'owner@example.com', password: 'mail-pass' },
]);
@@ -441,5 +463,6 @@ return {
assert.equal(api.getPayloadInput().paypalAccounts[0].email, 'paypal@example.com');
assert.equal(api.getPersistedUpdates().settingsState.flows.kiro.targets['kiro-rs'].apiKey, 'schema-key');
assert.equal(api.getPersistedUpdates().cloudflareTempEmailAdminAuth, 'admin-secret');
assert.equal(api.getPersistedUpdates().madaoRoutingPlanId, 'rp-openai');
assert.deepEqual(api.getPersistOptions(), { replaceExisting: true });
});
@@ -6,6 +6,8 @@ const { readFlowRegistryBundle } = require('./helpers/script-bundles.js');
const flowRegistrySource = readFlowRegistryBundle();
const settingsSchemaSource = fs.readFileSync('core/flow-kernel/settings-schema.js', 'utf8');
const backgroundSource = fs.readFileSync('background.js', 'utf8');
const DEFAULT_MADAO_BASE_URL_FOR_TEST = 'http://127.0.0.1:7822';
const DEFAULT_MADAO_MODE_FOR_TEST = 'routing_plan';
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
@@ -91,6 +93,8 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'stepExecutionRangeByFlow',
]);
const SETTINGS_SCHEMA_VIEW_KEY_SET = new Set(SETTINGS_SCHEMA_VIEW_KEYS);
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
const DEFAULT_MADAO_MODE = 'routing_plan';
const PERSISTED_SETTING_DEFAULTS = {
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
targetId: 'cpa',
@@ -107,6 +111,17 @@ const PERSISTED_SETTING_DEFAULTS = {
ipProxyMode: 'account',
kiroRsUrl: '',
kiroRsKey: '',
phoneSmsProvider: 'hero-sms',
madaoBaseUrl: DEFAULT_MADAO_BASE_URL,
madaoHttpSecret: '',
madaoMode: DEFAULT_MADAO_MODE,
madaoRoutingPlanId: '',
madaoProviderId: '',
madaoCountry: '',
madaoAutoPickCountry: true,
madaoReusePhone: true,
madaoMinPrice: '',
madaoMaxPrice: '',
stepExecutionRangeByFlow: {},
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
@@ -158,6 +173,68 @@ function normalizeCustomMailHelperBaseUrl(value = '') {
}
}
function normalizeStepExecutionRangeByFlow(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
function normalizePhoneSmsProvider(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === '5sim' || normalized === 'nexsms' || normalized === 'madao') {
return normalized;
}
return 'hero-sms';
}
function normalizePhoneSmsProviderOrder(value = []) {
const source = Array.isArray(value) ? value : String(value || '').split(/[\\r\\n,]+/);
const seen = new Set();
return source
.map((entry) => normalizePhoneSmsProvider(entry))
.filter((provider) => {
if (!provider || seen.has(provider)) return false;
seen.add(provider);
return true;
})
.slice(0, 4);
}
function normalizeLocalHttpBaseUrl(value = '', fallback = DEFAULT_MADAO_BASE_URL) {
const rawValue = String(value || fallback).trim();
try {
const parsed = new URL(rawValue);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return fallback;
}
return parsed.toString().replace(/\\/$/, '');
} catch {
return fallback;
}
}
function normalizeMaDaoBaseUrl(value = '') {
const normalized = normalizeLocalHttpBaseUrl(value, DEFAULT_MADAO_BASE_URL);
try {
const parsed = new URL(normalized);
parsed.pathname = parsed.pathname.replace(/\\/api\\/(?:acquire|poll|release|routing\\/replace)$/i, '');
parsed.search = '';
parsed.hash = '';
return parsed.toString().replace(/\\/$/, '');
} catch {
return DEFAULT_MADAO_BASE_URL;
}
}
function normalizeMaDaoMode(value = '') { return String(value || '').trim().toLowerCase() === 'direct' ? 'direct' : DEFAULT_MADAO_MODE; }
function normalizeMaDaoIdentifier(value = '') { return String(value || '').trim(); }
function normalizeMaDaoProviderId(value = '') { return String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, ''); }
function normalizeMaDaoCountry(value = '') {
const trimmed = String(value || '').trim();
if (!trimmed) return '';
const lowered = trimmed.toLowerCase();
if (lowered === 'any' || lowered === 'local') return lowered;
if (/^[a-z]{2}$/i.test(trimmed)) return trimmed.toUpperCase();
return lowered.replace(/[^a-z0-9_-]+/g, '');
}
function normalizeHeroSmsMaxPrice(value = '') {
const rawValue = String(value ?? '').trim();
if (!rawValue) return '';
const numeric = Number(rawValue);
if (!Number.isFinite(numeric) || numeric <= 0) return '';
return String(Math.round(numeric * 10000) / 10000);
}
function normalizeMaDaoPrice(value = '') { return normalizeHeroSmsMaxPrice(value); }
function normalizeIpProxyProviderValue(value) { return String(value || '711proxy').trim() || '711proxy'; }
function normalizeIpProxyMode(value) { return String(value || 'account').trim() || 'account'; }
function normalizeIpProxyServiceProfiles(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
@@ -226,6 +303,11 @@ test('buildPersistentSettingsPayload writes canonical settings schema into persi
assert.equal(payload.targetId, 'kiro-rs');
assert.equal(payload.kiroRsUrl, 'https://kiro.example.com/admin');
assert.equal(payload.kiroRsKey, 'secret-key');
assert.equal(payload.phoneSmsProvider, 'hero-sms');
assert.equal(payload.madaoBaseUrl, DEFAULT_MADAO_BASE_URL_FOR_TEST);
assert.equal(payload.madaoMode, DEFAULT_MADAO_MODE_FOR_TEST);
assert.equal(payload.madaoAutoPickCountry, true);
assert.equal(payload.madaoReusePhone, true);
assert.equal(Object.prototype.hasOwnProperty.call(payload, 'kiroRegion'), false);
assert.equal(payload.settingsSchemaVersion, 5);
assert.equal(payload.settingsState.activeFlowId, 'kiro');
@@ -335,6 +417,8 @@ function getRequestedKeys() {
assert.ok(api.getRequestedKeys().includes('plusAccountAccessStrategy'));
assert.equal(state.settingsSchemaVersion, 5);
assert.equal(state.settingsState.activeFlowId, 'openai');
assert.ok(api.getRequestedKeys().includes('madaoBaseUrl'));
assert.ok(api.getRequestedKeys().includes('madaoMode'));
});
test('getPersistedSettings can project schema-only storage back into legacy flat settings', async () => {
@@ -629,6 +713,37 @@ function getRemovedKeys() {
assert.equal(Object.prototype.hasOwnProperty.call(write, 'customMailReceiveMode'), false);
});
test('buildPersistentSettingsPayload persists normalized MaDao flat settings outside canonical settingsState', () => {
const api = buildHarness();
const payload = api.buildPersistentSettingsPayload({
phoneSmsProvider: 'MaDao',
madaoBaseUrl: 'http://127.0.0.1:7822/api/acquire?x=1',
madaoHttpSecret: ' secret-token ',
madaoMode: 'direct',
madaoRoutingPlanId: ' rp-openai ',
madaoProviderId: ' Upstream A! ',
madaoCountry: ' gb ',
madaoAutoPickCountry: 0,
madaoReusePhone: 1,
madaoMinPrice: '0.123456',
madaoMaxPrice: '-1',
});
assert.equal(payload.phoneSmsProvider, 'madao');
assert.equal(payload.madaoBaseUrl, DEFAULT_MADAO_BASE_URL_FOR_TEST);
assert.equal(payload.madaoHttpSecret, ' secret-token ');
assert.equal(payload.madaoMode, 'direct');
assert.equal(payload.madaoRoutingPlanId, 'rp-openai');
assert.equal(payload.madaoProviderId, 'upstreama');
assert.equal(payload.madaoCountry, 'GB');
assert.equal(payload.madaoAutoPickCountry, false);
assert.equal(payload.madaoReusePhone, true);
assert.equal(payload.madaoMinPrice, '0.1235');
assert.equal(payload.madaoMaxPrice, '');
assert.equal(Object.prototype.hasOwnProperty.call(payload.settingsState || {}, 'madaoBaseUrl'), false);
});
test('setPersistentSettings mirrors flat schema updates without resetting other canonical settings', async () => {
const api = buildHarness(`
const persistedWrites = [];
+212
View File
@@ -0,0 +1,212 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('phone-sms/providers/madao.js', 'utf8');
const api = new Function('self', `${source}; return self.PhoneSmsMaDaoProvider;`)({});
function createJsonResponse(payload, ok = true, status = ok ? 200 : 400, statusText = ok ? 'OK' : 'Bad Request') {
return {
ok,
status,
statusText,
text: async () => (typeof payload === 'string' ? payload : JSON.stringify(payload)),
};
}
test('MaDao direct acquire sends only direct fields and normalizes activation data', async () => {
const requests = [];
const provider = api.createProvider({
fetchImpl: async (url, options = {}) => {
requests.push({ url: new URL(url), options, body: JSON.parse(options.body) });
return createJsonResponse({
ticket_id: 'ticket-1',
phone_number: '+441111111111',
service: 'openai',
country: 'gb',
provider: 'Upstream A!',
price: 0.123456,
status: 'code_received',
});
},
});
const activation = await provider.acquireActivation({
madaoBaseUrl: 'http://127.0.0.1:7822/',
madaoHttpSecret: 'token-1',
madaoMode: 'direct',
madaoRoutingPlanId: 'route-plan-should-not-be-sent',
madaoProviderId: 'Upstream A!',
madaoCountry: 'gb',
madaoAutoPickCountry: 'false',
madaoReusePhone: '1',
madaoMinPrice: '0.01',
madaoMaxPrice: '0.20',
});
assert.equal(requests.length, 1);
assert.equal(requests[0].url.toString(), 'http://127.0.0.1:7822/api/acquire');
assert.equal(requests[0].options.method, 'POST');
assert.equal(requests[0].options.headers.Authorization, 'Bearer token-1');
assert.deepEqual(requests[0].body, {
provider: 'upstreama',
service: 'openai',
auto_pick_country: false,
reuse_phone: true,
country: 'GB',
min_price: 0.01,
max_price: 0.2,
});
assert.equal(activation.provider, 'madao');
assert.equal(activation.activationId, 'ticket-1');
assert.equal(activation.phoneNumber, '+441111111111');
assert.equal(activation.countryId, 'GB');
assert.equal(activation.madaoProviderId, 'upstreama');
assert.equal(activation.madaoPrice, 0.1235);
assert.equal(activation.madaoStatus, 'code_received');
});
test('MaDao routing acquire sends routing plan only', async () => {
const requests = [];
const provider = api.createProvider({
fetchImpl: async (url, options = {}) => {
requests.push({ url: new URL(url), body: JSON.parse(options.body) });
return createJsonResponse({
ticket_id: 'ticket-2',
phone_number: '+442222222222',
routing_plan_id: 'rp-openai',
routing_item_id: 'route-1',
});
},
});
const activation = await provider.acquireActivation({
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'rp-openai',
madaoProviderId: 'direct-provider',
madaoCountry: 'TH',
madaoAutoPickCountry: false,
madaoReusePhone: false,
madaoMinPrice: '0.01',
madaoMaxPrice: '0.20',
});
assert.deepEqual(requests[0].body, {
provider: 'auto',
service: 'openai',
routing_plan_id: 'rp-openai',
});
assert.equal(activation.madaoRoutingPlanId, 'rp-openai');
assert.equal(activation.madaoRoutingItemId, 'route-1');
});
test('MaDao poll extracts codes from nested messages and reports pending status', async () => {
const statusEvents = [];
const provider = api.createProvider({
fetchImpl: async (_url, options = {}) => {
const body = JSON.parse(options.body);
if (body.ticket_id === 'pending-ticket') {
return createJsonResponse({ status: 'waiting_code', message: 'still waiting' });
}
return createJsonResponse({
messages: [
{ text: 'old text without code' },
{ body: 'OpenAI verification code: 765432' },
],
});
},
});
const pending = await provider.pollActivationCode({}, { activationId: 'pending-ticket' }, {
onStatus: async (event) => statusEvents.push(event),
});
const code = await provider.pollActivationCode({}, { activationId: 'ready-ticket' });
assert.equal(pending, '');
assert.equal(statusEvents.length, 1);
assert.equal(statusEvents[0].statusText, 'still waiting');
assert.equal(code, '765432');
});
test('MaDao direct rotate releases the current ticket without acquiring a new one', async () => {
const requests = [];
const provider = api.createProvider({
fetchImpl: async (url, options = {}) => {
requests.push({ url: new URL(url), body: JSON.parse(options.body) });
return createJsonResponse({ released: true });
},
});
const result = await provider.rotateActivation(
{ madaoMode: 'direct' },
{ activationId: 'ticket-old', phoneNumber: '+441111111111' },
{ releaseAction: 'ban' }
);
assert.deepEqual(
requests.map((request) => request.url.pathname),
['/api/release']
);
assert.deepEqual(requests[0].body, { ticket_id: 'ticket-old', action: 'ban' });
assert.equal(result.currentTicketId, 'ticket-old');
assert.equal(result.nextActivation, null);
});
test('MaDao routing rotate rejects invalid replacement payloads', async () => {
const provider = api.createProvider({
fetchImpl: async () => createJsonResponse({
current_ticket_id: 'ticket-old',
next_ticket: {
ticket_id: 'ticket-new',
},
}),
});
await assert.rejects(
() => provider.rotateActivation(
{ madaoMode: 'routing_plan' },
{
activationId: 'ticket-old',
phoneNumber: '+441111111111',
madaoRoutingPlanId: 'rp-openai',
madaoRoutingItemId: 'route-old',
},
{ releaseAction: 'ban', reason: 'phone_number_used' }
),
/MaDao/
);
});
test('MaDao provider surfaces HTTP errors and request timeouts', async () => {
const httpProvider = api.createProvider({
fetchImpl: async () => createJsonResponse({ error: 'upstream unavailable' }, false, 503, 'Service Unavailable'),
});
await assert.rejects(
() => httpProvider.acquireActivation({}),
(error) => {
assert.equal(error.status, 503);
assert.equal(error.payload.error, 'upstream unavailable');
assert.match(error.message, /upstream unavailable/);
return true;
}
);
const timeoutProvider = api.createProvider({
fetchImpl: async (_url, options = {}) => {
await new Promise((resolve, reject) => {
options.signal.addEventListener('abort', () => {
const error = new Error('aborted');
error.name = 'AbortError';
reject(error);
});
});
},
requestTimeoutMs: 1,
});
await assert.rejects(
() => timeoutProvider.acquireActivation({}),
/MaDao/
);
});
+16 -4
View File
@@ -16,29 +16,41 @@ test('phone sms provider registry normalizes ids, order and labels consistently'
PhoneSmsFiveSimProvider: {
createProvider: (deps = {}) => ({ provider: '5sim', deps }),
},
PhoneSmsMaDaoProvider: {
createProvider: (deps = {}) => ({ provider: 'madao', deps }),
},
});
assert.deepStrictEqual(registry.getProviderIds(), ['hero-sms', '5sim', 'nexsms']);
assert.deepStrictEqual(registry.getProviderIds(), ['hero-sms', '5sim', 'nexsms', 'madao']);
assert.equal(registry.normalizeProviderId(' NEXSMS '), 'nexsms');
assert.equal(registry.normalizeProviderId(' MaDao '), 'madao');
assert.equal(registry.normalizeProviderId('unknown-provider'), 'hero-sms');
assert.equal(registry.getProviderLabel('nexsms'), 'NexSMS');
assert.equal(registry.getProviderLabel('madao'), 'MaDao');
assert.equal(registry.getProviderDefinition('nexsms').moduleKey, 'PhoneSmsNexSmsProvider');
assert.equal(registry.getProviderDefinition('madao').moduleKey, 'PhoneSmsMaDaoProvider');
assert.deepStrictEqual(
registry.normalizeProviderOrder([
{ provider: 'madao' },
{ provider: 'nexsms' },
{ id: '5sim' },
{ value: 'hero-sms' },
'MADAO',
'NEXSMS',
]),
['nexsms', '5sim', 'hero-sms']
['madao', 'nexsms', '5sim', 'hero-sms']
);
assert.deepStrictEqual(
registry.normalizeProviderOrder([], ['nexsms', '5sim', 'nexsms']),
['nexsms', '5sim']
registry.normalizeProviderOrder([], ['madao', 'nexsms', '5sim', 'nexsms']),
['madao', 'nexsms', '5sim']
);
assert.deepStrictEqual(
registry.createProvider('5sim', { foo: 1 }),
{ provider: '5sim', deps: { foo: 1 } }
);
assert.deepStrictEqual(
registry.createProvider('madao', { foo: 2 }),
{ provider: 'madao', deps: { foo: 2 } }
);
assert.throws(() => registry.createProvider('nexsms'), /接码平台模块未加载:nexsms/);
});
+364
View File
@@ -5,6 +5,8 @@ const fs = require('node:fs');
const source = fs.readFileSync('background/phone-verification-flow.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundPhoneVerification;`)(globalScope);
const maDaoSource = fs.readFileSync('phone-sms/providers/madao.js', 'utf8');
const maDaoModule = new Function('self', `${maDaoSource}; return self.PhoneSmsMaDaoProvider;`)({});
function buildHeroSmsPricesPayload({ country = '52', service = 'dr', cost = 0.08, count = 25370, physicalCount = 14528 } = {}) {
return JSON.stringify({
@@ -155,6 +157,101 @@ test('phone verification helper reads latest HeroSMS operator from persistent st
assert.equal(getNumberRequest.searchParams.get('operator'), 'dtac');
});
test('phone verification helper acquires, polls and releases MaDao activation through provider adapter', async () => {
const requests = [];
let currentState = {
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822/',
madaoHttpSecret: 'secret-token',
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'rp-openai',
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 2,
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
createMaDaoProvider: maDaoModule.createProvider,
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url);
requests.push({ url: parsedUrl, options, body: options.body ? JSON.parse(options.body) : null });
if (parsedUrl.pathname === '/api/acquire') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: 'madao-1',
phone_number: '+441111111111',
service: 'openai',
country: 'GB',
provider: 'upstream-a',
routing_plan_id: 'rp-openai',
routing_item_id: 'route-1',
status: 'waiting_code',
}),
};
}
if (parsedUrl.pathname === '/api/poll') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: 'madao-1',
status: 'code_received',
code: '123456',
}),
};
}
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 () => ({}),
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const activation = await helpers.requestPhoneActivation(currentState);
const code = await helpers.pollPhoneActivationCode(currentState, activation, {
timeoutMs: 15000,
intervalMs: 1000,
maxRounds: 1,
});
await helpers.finalizeSignupPhoneActivationAfterSuccess(currentState, activation);
assert.equal(activation.provider, 'madao');
assert.equal(activation.activationId, 'madao-1');
assert.equal(activation.phoneNumber, '+441111111111');
assert.equal(activation.countryId, 'GB');
assert.equal(activation.madaoRoutingPlanId, 'rp-openai');
assert.equal(activation.madaoRoutingItemId, 'route-1');
assert.equal(code, '123456');
assert.deepStrictEqual(
requests.map((entry) => entry.url.pathname),
['/api/acquire', '/api/poll', '/api/release']
);
assert.deepStrictEqual(requests[0].body, {
provider: 'auto',
service: 'openai',
routing_plan_id: 'rp-openai',
});
assert.equal(requests[0].options.headers.Authorization, 'Bearer secret-token');
assert.deepStrictEqual(requests[1].body, { ticket_id: 'madao-1' });
assert.deepStrictEqual(requests[2].body, { ticket_id: 'madao-1', action: 'finish' });
});
test('signup phone helper persists signup runtime state without touching add-phone activation', async () => {
const setStateCalls = [];
let currentState = {
@@ -8722,6 +8819,273 @@ test('phone verification helper routes 5sim buy, check, and finish by current ac
);
});
test('phone verification helper uses MaDao routing replace when add-phone rejects current number', async () => {
const requests = [];
const submittedPhones = [];
let currentState = {
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822',
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'rp-openai',
verificationResendCount: 0,
phoneVerificationReplacementLimit: 2,
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
createMaDaoProvider: maDaoModule.createProvider,
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url);
requests.push({ url: parsedUrl, body: options.body ? JSON.parse(options.body) : null });
if (parsedUrl.pathname === '/api/acquire') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: 'madao-old',
phone_number: '+441111111111',
service: 'openai',
country: 'GB',
routing_plan_id: 'rp-openai',
routing_item_id: 'route-old',
status: 'waiting_code',
}),
};
}
if (parsedUrl.pathname === '/api/routing/replace') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
current_ticket_id: 'madao-old',
next_ticket: {
ticket_id: 'madao-new',
phone_number: '+442222222222',
service: 'openai',
country: 'GB',
routing_plan_id: 'rp-openai',
routing_item_id: 'route-new',
status: 'waiting_code',
},
}),
};
}
if (parsedUrl.pathname === '/api/poll') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: 'madao-new',
status: 'code_received',
code: '654321',
}),
};
}
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) => {
if (message.type === 'SUBMIT_PHONE_NUMBER') {
submittedPhones.push(message.payload.phoneNumber);
if (message.payload.phoneNumber === '+441111111111') {
return {
addPhoneRejected: true,
errorText: 'This phone number is already linked to another account',
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
};
}
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
assert.equal(message.payload.code, '654321');
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
}
if (message.type === 'RETURN_TO_ADD_PHONE' || message.type === 'STEP8_GET_STATE') {
return { addPhonePage: true, phoneVerificationPage: false, url: 'https://auth.openai.com/add-phone' };
}
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(submittedPhones, ['+441111111111', '+442222222222']);
assert.deepStrictEqual(
requests.map((entry) => entry.url.pathname),
['/api/acquire', '/api/routing/replace', '/api/poll', '/api/release']
);
assert.deepStrictEqual(requests[1].body, {
ticket_id: 'madao-old',
release_action: 'ban',
failed_item_id: 'route-old',
reason: 'phone_number_used',
});
assert.deepStrictEqual(requests[2].body, { ticket_id: 'madao-new' });
assert.deepStrictEqual(requests[3].body, { ticket_id: 'madao-new', action: 'finish' });
assert.equal(currentState.currentPhoneActivation, null);
});
test('phone verification helper reacquires MaDao direct numbers after releasing rejected number', async () => {
const requests = [];
const submittedPhones = [];
let acquireCount = 0;
let currentState = {
phoneSmsProvider: 'madao',
madaoBaseUrl: 'http://127.0.0.1:7822',
madaoMode: 'direct',
madaoRoutingPlanId: 'rp-stale',
madaoProviderId: 'upstream-a',
madaoCountry: 'gb',
madaoAutoPickCountry: false,
madaoReusePhone: true,
madaoMinPrice: '0.01',
madaoMaxPrice: '0.2',
verificationResendCount: 0,
phoneVerificationReplacementLimit: 2,
phoneCodeWaitSeconds: 15,
phoneCodeTimeoutWindows: 1,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
createMaDaoProvider: maDaoModule.createProvider,
ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url, options = {}) => {
const parsedUrl = new URL(url);
requests.push({ url: parsedUrl, body: options.body ? JSON.parse(options.body) : null });
if (parsedUrl.pathname === '/api/acquire') {
acquireCount += 1;
const isFirst = acquireCount === 1;
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: isFirst ? 'madao-direct-old' : 'madao-direct-new',
phone_number: isFirst ? '+441111111111' : '+442222222222',
service: 'openai',
country: 'GB',
provider: 'upstream-a',
status: 'waiting_code',
}),
};
}
if (parsedUrl.pathname === '/api/release') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({ ok: true }),
};
}
if (parsedUrl.pathname === '/api/poll') {
return {
ok: true,
status: 200,
text: async () => JSON.stringify({
ticket_id: 'madao-direct-new',
status: 'code_received',
code: '246810',
}),
};
}
throw new Error(`Unexpected MaDao path: ${parsedUrl.pathname}`);
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'SUBMIT_PHONE_NUMBER') {
submittedPhones.push(message.payload.phoneNumber);
if (message.payload.phoneNumber === '+441111111111') {
return {
addPhoneRejected: true,
errorText: 'This phone number is already linked to another account',
addPhonePage: true,
phoneVerificationPage: false,
url: 'https://auth.openai.com/add-phone',
};
}
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
}
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
assert.equal(message.payload.code, '246810');
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
}
if (message.type === 'RETURN_TO_ADD_PHONE' || message.type === 'STEP8_GET_STATE') {
return { addPhonePage: true, phoneVerificationPage: false, url: 'https://auth.openai.com/add-phone' };
}
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(submittedPhones, ['+441111111111', '+442222222222']);
assert.deepStrictEqual(
requests.map((entry) => entry.url.pathname),
['/api/acquire', '/api/release', '/api/acquire', '/api/poll', '/api/release']
);
assert.deepStrictEqual(requests[0].body, {
provider: 'upstream-a',
service: 'openai',
auto_pick_country: false,
reuse_phone: true,
country: 'GB',
min_price: 0.01,
max_price: 0.2,
});
assert.equal(Object.prototype.hasOwnProperty.call(requests[0].body, 'routing_plan_id'), false);
assert.deepStrictEqual(requests[1].body, { ticket_id: 'madao-direct-old', action: 'ban' });
assert.deepStrictEqual(requests[3].body, { ticket_id: 'madao-direct-new' });
assert.deepStrictEqual(requests[4].body, { ticket_id: 'madao-direct-new', action: 'finish' });
assert.equal(currentState.currentPhoneActivation, null);
});
test('phone verification helper keeps 5sim reusable activation on the original order', async () => {
const requests = [];
let checkCount = 0;
@@ -88,9 +88,11 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.match(html, /id="select-phone-sms-provider"/);
assert.match(html, /\.\.\/phone-sms\/providers\/hero-sms\.js/);
assert.match(html, /\.\.\/phone-sms\/providers\/five-sim\.js/);
assert.match(html, /\.\.\/phone-sms\/providers\/madao\.js/);
assert.match(html, /\.\.\/phone-sms\/providers\/registry\.js/);
assert.match(html, /<option value="hero-sms">HeroSMS<\/option>/);
assert.match(html, /<option value="5sim">5sim<\/option>/);
assert.match(html, /<option value="madao">MaDao<\/option>/);
assert.match(html, /id="row-hero-sms-country"/);
assert.match(html, /id="row-hero-sms-country-fallback"/);
assert.match(html, /id="row-hero-sms-acquire-priority"/);
@@ -147,6 +149,26 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.match(html, /id="row-nex-sms-country-fallback"/);
assert.match(html, /id="row-nex-sms-service-code"/);
assert.match(html, /id="input-nex-sms-service-code"/);
assert.match(html, /id="row-madao-base-url"/);
assert.match(html, /id="input-madao-base-url"/);
assert.match(html, /id="row-madao-http-secret"/);
assert.match(html, /id="input-madao-http-secret"/);
assert.match(html, /id="row-madao-mode"/);
assert.match(html, /id="select-madao-mode"/);
assert.match(html, /id="row-madao-routing-plan-id"/);
assert.match(html, /id="input-madao-routing-plan-id"/);
assert.match(html, /id="row-madao-provider-id"/);
assert.match(html, /id="input-madao-provider-id"/);
assert.match(html, /id="row-madao-country"/);
assert.match(html, /id="input-madao-country"/);
assert.match(html, /id="row-madao-auto-pick-country"/);
assert.match(html, /id="input-madao-auto-pick-country"/);
assert.match(html, /id="row-madao-reuse-phone"/);
assert.match(html, /id="input-madao-reuse-phone"/);
assert.match(html, /id="row-madao-price-range"/);
assert.match(html, /id="input-madao-min-price"/);
assert.match(html, /id="input-madao-max-price"/);
assert.doesNotMatch(html, /id="btn-open-madao-github"/);
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
});
@@ -623,19 +645,21 @@ const rowPhoneSmsProvider = { style: { display: 'none' } };
const rowPhoneSmsProviderOrder = { style: { display: 'none' } };
const rowPhoneSmsProviderOrderActions = { style: { display: 'none' } };
const selectPhoneSmsProvider = { value: 'hero-sms' };
const selectMaDaoMode = { value: 'routing_plan' };
const btnTogglePhoneVerificationSection = {
disabled: false,
textContent: '',
title: '',
setAttribute: () => {},
};
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms'];
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms', 'madao'];
const phoneSmsProviderOrderSelection = [];
function getPhoneSmsProviderCount() { return DEFAULT_PHONE_SMS_PROVIDER_ORDER.length; }
function normalizePhoneSmsProviderOrderValue(value = [], fallbackOrder = DEFAULT_PHONE_SMS_PROVIDER_ORDER) {
const source = Array.isArray(value) ? value : [];
const normalized = [...source];
if (normalized.length) {
return normalized.slice(0, 3);
return normalized.slice(0, getPhoneSmsProviderCount());
}
if (!Array.isArray(fallbackOrder) || !fallbackOrder.length) {
return [];
@@ -646,7 +670,7 @@ const btnTogglePhoneVerificationSection = {
fallbackNormalized.push(provider);
}
}
return fallbackNormalized.slice(0, 3);
return fallbackNormalized.slice(0, getPhoneSmsProviderCount());
}
function resolveNormalizedProviderOrderForRuntime(state = {}) {
const rawOrder = Array.isArray(state?.phoneSmsProviderOrder) ? state.phoneSmsProviderOrder : [];
@@ -674,6 +698,15 @@ const rowNexSmsApiKey = { style: { display: 'none' } };
const rowNexSmsCountry = { style: { display: 'none' } };
const rowNexSmsCountryFallback = { style: { display: 'none' } };
const rowNexSmsServiceCode = { style: { display: 'none' } };
const rowMaDaoBaseUrl = { style: { display: 'none' } };
const rowMaDaoHttpSecret = { style: { display: 'none' } };
const rowMaDaoMode = { style: { display: 'none' } };
const rowMaDaoRoutingPlanId = { style: { display: 'none' } };
const rowMaDaoProviderId = { style: { display: 'none' } };
const rowMaDaoCountry = { style: { display: 'none' } };
const rowMaDaoAutoPickCountry = { style: { display: 'none' } };
const rowMaDaoReusePhone = { style: { display: 'none' } };
const rowMaDaoPriceRange = { style: { display: 'none' } };
const rowHeroSmsRuntimePair = { style: { display: 'none' } };
const rowHeroSmsCurrentNumber = { style: { display: 'none' } };
const rowHeroSmsCurrentCountdown = { style: { display: 'none' } };
@@ -689,6 +722,8 @@ const rowPhoneCodePollMaxRounds = { style: { display: 'none' } };
const rowFreePhoneReuseEnabled = createMockRow();
const rowFreePhoneReuseAutoEnabled = createMockRow();
const rowFreeReusablePhone = createMockRow();
const rowPhoneSmsPreferredPriceControl = { style: { display: 'none' } };
const rowPhoneSmsReuseControl = { style: { display: 'none' } };
const heroSmsReuseRow = createMockRow();
const inputHeroSmsReuseEnabled = { checked: true, disabled: false, closest: () => heroSmsReuseRow };
const inputFreePhoneReuseEnabled = { checked: true, disabled: false };
@@ -698,10 +733,75 @@ const inputFreeReusablePhone = { disabled: false };
const btnSaveFreeReusablePhone = { disabled: false };
const btnClearFreeReusablePhone = { disabled: false };
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_HERO = PHONE_SMS_PROVIDER_HERO_SMS;
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const MADAO_MODE_ROUTING_PLAN = 'routing_plan';
const MADAO_MODE_DIRECT = 'direct';
const DEFAULT_MADAO_MODE = MADAO_MODE_ROUTING_PLAN;
const PHONE_SMS_PROVIDER_UI_DESCRIPTORS = ${JSON.stringify({
'hero-sms': {
rowKeys: [
'rowHeroSmsCountry',
'rowHeroSmsCountryFallback',
'rowHeroSmsAcquirePriority',
'rowHeroSmsOperator',
'rowHeroSmsApiKey',
'rowHeroSmsMaxPrice',
],
priceControlKeys: [
'rowPhoneSmsPreferredPriceControl',
'rowPhoneSmsReuseControl',
],
},
'5sim': {
rowKeys: [
'rowFiveSimApiKey',
'rowFiveSimCountry',
'rowFiveSimCountryFallback',
'rowFiveSimOperator',
'rowFiveSimProduct',
'rowHeroSmsMaxPrice',
],
},
nexsms: {
rowKeys: [
'rowNexSmsApiKey',
'rowNexSmsCountry',
'rowNexSmsCountryFallback',
'rowNexSmsServiceCode',
],
},
madao: {
rowKeys: [
'rowMaDaoBaseUrl',
'rowMaDaoHttpSecret',
'rowMaDaoMode',
],
routingRowKeys: [
'rowMaDaoRoutingPlanId',
],
directRowKeys: [
'rowMaDaoProviderId',
'rowMaDaoCountry',
'rowMaDaoAutoPickCountry',
'rowMaDaoReusePhone',
'rowMaDaoPriceRange',
],
},
})};
function getSelectedPhoneSmsProvider() { return selectPhoneSmsProvider.value; }
function isFiveSimProviderSelected() { return getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM; }
function normalizePhoneSmsProviderValue(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return DEFAULT_PHONE_SMS_PROVIDER_ORDER.includes(normalized) ? normalized : PHONE_SMS_PROVIDER_HERO_SMS;
}
function normalizeMaDaoModeValue(value = '') { return String(value || '').trim().toLowerCase() === MADAO_MODE_DIRECT ? MADAO_MODE_DIRECT : DEFAULT_MADAO_MODE; }
${extractFunction('getPhoneSmsProviderUiRowMap')}
${extractFunction('getProviderUiRows')}
${extractFunction('getAllProviderUiRows')}
${extractFunction('updateProviderPriceControls')}
function updateHeroSmsPlatformDisplay() {}
function updateSignupMethodUI() {
rowSignupMethod.style.display = inputPhoneVerificationEnabled.checked ? '' : 'none';
@@ -756,6 +856,18 @@ return {
rowNexSmsCountry,
rowNexSmsCountryFallback,
rowNexSmsServiceCode,
rowMaDaoBaseUrl,
rowMaDaoHttpSecret,
rowMaDaoMode,
rowMaDaoRoutingPlanId,
rowMaDaoProviderId,
rowMaDaoCountry,
rowMaDaoAutoPickCountry,
rowMaDaoReusePhone,
rowMaDaoPriceRange,
rowPhoneSmsPreferredPriceControl,
rowPhoneSmsReuseControl,
selectMaDaoMode,
rowHeroSmsRuntimePair,
rowHeroSmsCurrentNumber,
rowHeroSmsCurrentCountdown,
@@ -823,6 +935,15 @@ return {
assert.equal(api.rowNexSmsCountry.style.display, 'none');
assert.equal(api.rowNexSmsCountryFallback.style.display, 'none');
assert.equal(api.rowNexSmsServiceCode.style.display, 'none');
assert.equal(api.rowMaDaoBaseUrl.style.display, 'none');
assert.equal(api.rowMaDaoHttpSecret.style.display, 'none');
assert.equal(api.rowMaDaoMode.style.display, 'none');
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
assert.equal(api.rowMaDaoProviderId.style.display, 'none');
assert.equal(api.rowMaDaoCountry.style.display, 'none');
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
api.inputPhoneVerificationEnabled.checked = true;
api.setLatestState({ signupPhoneNumber: '66959916439' });
@@ -853,6 +974,8 @@ return {
assert.equal(api.rowHeroSmsOperator.style.display, '');
assert.equal(api.rowHeroSmsApiKey.style.display, '');
assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
assert.equal(api.rowPhoneSmsPreferredPriceControl.style.display, '');
assert.equal(api.rowPhoneSmsReuseControl.style.display, '');
assert.equal(api.rowFiveSimOperator.style.display, 'none');
assert.equal(api.rowHeroSmsCurrentNumber.style.display, '');
assert.equal(api.rowHeroSmsCurrentCountdown.style.display, '');
@@ -913,6 +1036,8 @@ return {
assert.equal(api.rowFiveSimCountryFallback.style.display, '');
assert.equal(api.rowFiveSimOperator.style.display, '');
assert.equal(api.rowFiveSimProduct.style.display, '');
assert.equal(api.rowPhoneSmsPreferredPriceControl.style.display, 'none');
assert.equal(api.rowPhoneSmsReuseControl.style.display, 'none');
api.setSelectedPhoneSmsProvider('nexsms');
api.updatePhoneVerificationSettingsUI();
@@ -920,6 +1045,33 @@ return {
assert.equal(api.rowNexSmsCountry.style.display, '');
assert.equal(api.rowNexSmsCountryFallback.style.display, '');
assert.equal(api.rowNexSmsServiceCode.style.display, '');
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
assert.equal(api.rowFiveSimOperator.style.display, 'none');
api.setSelectedPhoneSmsProvider('madao');
api.selectMaDaoMode.value = 'routing_plan';
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowMaDaoBaseUrl.style.display, '');
assert.equal(api.rowMaDaoHttpSecret.style.display, '');
assert.equal(api.rowMaDaoMode.style.display, '');
assert.equal(api.rowMaDaoRoutingPlanId.style.display, '');
assert.equal(api.rowMaDaoProviderId.style.display, 'none');
assert.equal(api.rowMaDaoCountry.style.display, 'none');
assert.equal(api.rowMaDaoAutoPickCountry.style.display, 'none');
assert.equal(api.rowMaDaoReusePhone.style.display, 'none');
assert.equal(api.rowMaDaoPriceRange.style.display, 'none');
api.selectMaDaoMode.value = 'direct';
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowMaDaoBaseUrl.style.display, '');
assert.equal(api.rowMaDaoHttpSecret.style.display, '');
assert.equal(api.rowMaDaoMode.style.display, '');
assert.equal(api.rowMaDaoRoutingPlanId.style.display, 'none');
assert.equal(api.rowMaDaoProviderId.style.display, '');
assert.equal(api.rowMaDaoCountry.style.display, '');
assert.equal(api.rowMaDaoAutoPickCountry.style.display, '');
assert.equal(api.rowMaDaoReusePhone.style.display, '');
assert.equal(api.rowMaDaoPriceRange.style.display, '');
});
test('collectSettingsPayload keeps local helper sync enabled while persisting sms toggle state', () => {
@@ -1005,6 +1157,16 @@ const inputFiveSimOperator = { value: 'any' };
const inputFiveSimProduct = { value: 'openai' };
const inputNexSmsApiKey = { value: 'nex-key' };
const inputNexSmsServiceCode = { value: 'ot' };
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/acquire' };
const inputMaDaoHttpSecret = { value: 'madao-secret' };
const selectMaDaoMode = { value: 'direct' };
const inputMaDaoRoutingPlanId = { value: 'plan-1' };
const inputMaDaoProviderId = { value: 'Local Provider!!' };
const inputMaDaoCountry = { value: 'th' };
const inputMaDaoAutoPickCountry = { checked: false };
const inputMaDaoReusePhone = { checked: true };
const inputMaDaoMinPrice = { value: '0.02' };
const inputMaDaoMaxPrice = { value: '0.2' };
const inputHeroSmsReuseEnabled = { checked: true };
const selectHeroSmsAcquirePriority = { value: 'price' };
const selectHeroSmsOperator = { value: 'AIS!!' };
@@ -1052,9 +1214,15 @@ const PHONE_REPLACEMENT_LIMIT_MAX = 20;
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_HERO = PHONE_SMS_PROVIDER_HERO_SMS;
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
const MADAO_MODE_ROUTING_PLAN = 'routing_plan';
const MADAO_MODE_DIRECT = 'direct';
const DEFAULT_MADAO_MODE = MADAO_MODE_ROUTING_PLAN;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
@@ -1093,6 +1261,12 @@ function normalizePlusAccountAccessStrategy(value = '') { return String(value ||
function resolvePlusAccountAccessStrategyForTarget(value = '') { return normalizePlusAccountAccessStrategy(value); }
${extractFunction('normalizePhoneSmsProvider')}
${extractFunction('normalizePhoneSmsProviderValue')}
${extractFunction('normalizeMaDaoBaseUrlValue')}
${extractFunction('normalizeMaDaoModeValue')}
${extractFunction('normalizeMaDaoIdentifierValue')}
${extractFunction('normalizeMaDaoProviderIdValue')}
${extractFunction('normalizeMaDaoCountry')}
${extractFunction('normalizeMaDaoPriceValue')}
${extractFunction('normalizeFiveSimCountryCode')}
${extractFunction('normalizeFiveSimCountryOrderValue')}
${extractFunction('normalizeFiveSimProductValue')}
@@ -1157,6 +1331,16 @@ return { collectSettingsPayload };
assert.equal(payload.nexSmsApiKey, 'nex-key');
assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]);
assert.equal(payload.nexSmsServiceCode, 'ot');
assert.equal(payload.madaoBaseUrl, 'http://127.0.0.1:7822');
assert.equal(payload.madaoHttpSecret, 'madao-secret');
assert.equal(payload.madaoMode, 'direct');
assert.equal(payload.madaoRoutingPlanId, 'plan-1');
assert.equal(payload.madaoProviderId, 'localprovider');
assert.equal(payload.madaoCountry, 'TH');
assert.equal(payload.madaoAutoPickCountry, false);
assert.equal(payload.madaoReusePhone, true);
assert.equal(payload.madaoMinPrice, '0.02');
assert.equal(payload.madaoMaxPrice, '0.2');
assert.equal(payload.phoneSmsReuseEnabled, false);
assert.equal(payload.heroSmsReuseEnabled, false);
assert.equal(payload.freePhoneReuseEnabled, false);
@@ -1192,8 +1376,22 @@ test('switchPhoneSmsProvider saves API keys independently when the select value
const api = new Function(`
let latestState = {
phoneSmsProvider: 'hero-sms',
phoneSmsProviderOrder: ['hero-sms', '5sim'],
heroSmsApiKey: 'hero-old',
fiveSimApiKey: 'five-old',
nexSmsApiKey: 'nex-old',
nexSmsCountryOrder: [1],
nexSmsServiceCode: 'ot',
madaoBaseUrl: 'http://127.0.0.1:7822',
madaoHttpSecret: 'madao-old-secret',
madaoMode: 'routing_plan',
madaoRoutingPlanId: 'plan-old',
madaoProviderId: 'provider-old',
madaoCountry: 'TH',
madaoAutoPickCountry: true,
madaoReusePhone: true,
madaoMinPrice: '0.01',
madaoMaxPrice: '0.09',
heroSmsMinPrice: '0.04',
heroSmsMaxPrice: '0.11',
fiveSimMinPrice: '0.88',
@@ -1207,10 +1405,21 @@ let latestState = {
fiveSimOperator: 'any',
};
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_HERO = PHONE_SMS_PROVIDER_HERO_SMS;
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const DEFAULT_PHONE_SMS_PROVIDER_ORDER = ['hero-sms', '5sim', 'nexsms', 'madao'];
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
const DEFAULT_MADAO_BASE_URL = 'http://127.0.0.1:7822';
const MADAO_MODE_ROUTING_PLAN = 'routing_plan';
const MADAO_MODE_DIRECT = 'direct';
const DEFAULT_MADAO_MODE = MADAO_MODE_ROUTING_PLAN;
const DEFAULT_HERO_SMS_OPERATOR = 'any';
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
@@ -1218,25 +1427,64 @@ const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vie
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const selectPhoneSmsProvider = { value: 'hero-sms', dataset: { activeProvider: 'hero-sms' } };
const inputHeroSmsApiKey = { value: 'hero-live' };
const inputFiveSimApiKey = { value: 'five-old' };
const inputNexSmsApiKey = { value: 'nex-live' };
const inputNexSmsServiceCode = { value: 'ot' };
const inputMaDaoBaseUrl = { value: 'http://127.0.0.1:7822/api/poll' };
const inputMaDaoHttpSecret = { value: 'madao-live-secret' };
const selectMaDaoMode = { value: 'direct' };
const inputMaDaoRoutingPlanId = { value: 'plan-live' };
const inputMaDaoProviderId = { value: 'Provider Live!' };
const inputMaDaoCountry = { value: 'local' };
const inputMaDaoAutoPickCountry = { checked: false };
const inputMaDaoReusePhone = { checked: false };
const inputMaDaoMinPrice = { value: '0.02' };
const inputMaDaoMaxPrice = { value: '0.2' };
const inputHeroSmsMinPrice = { value: '0.03' };
const inputHeroSmsMaxPrice = { value: '0.22' };
const inputFiveSimOperator = { value: 'any' };
const inputFiveSimProduct = { value: 'openai' };
const selectHeroSmsOperator = { value: 'ais', options: [{ value: 'any' }, { value: 'ais' }] };
const inputHeroSmsPreferredPrice = { value: '' };
const displayHeroSmsPriceTiers = { textContent: '' };
const displayPhoneSmsBalance = { textContent: '' };
const rowHeroSmsPriceTiers = { style: { display: '' } };
let heroSmsCountrySelectionOrder = [];
let phoneSmsProviderOrderSelection = ['hero-sms', '5sim'];
let lastPhoneSmsProviderBeforeChange = null;
let savedPayload = null;
${extractFunction('normalizePhoneSmsProvider')}
${extractFunction('normalizePhoneSmsProviderValue')}
function getPhoneSmsProviderCount() { return DEFAULT_PHONE_SMS_PROVIDER_ORDER.length; }
${extractFunction('normalizePhoneSmsProviderOrderValue')}
${extractFunction('setPhoneSmsProviderSelectValue')}
${extractFunction('getLastAppliedPhoneSmsProvider')}
function getSelectedPhoneSmsProvider() { return normalizePhoneSmsProvider(selectPhoneSmsProvider?.value || latestState?.phoneSmsProvider); }
function applyPhoneSmsProviderOrderSelection(order = [], options = {}) {
phoneSmsProviderOrderSelection = normalizePhoneSmsProviderOrderValue(order, []);
if (options.syncProvider && phoneSmsProviderOrderSelection.length) {
selectPhoneSmsProvider.value = phoneSmsProviderOrderSelection[0];
}
return [...phoneSmsProviderOrderSelection];
}
${extractFunction('setPhoneSmsProviderOrderPrimary')}
${extractFunction('normalizeFiveSimCountryId')}
${extractFunction('normalizeFiveSimCountryLabel')}
${extractFunction('normalizeFiveSimCountryCode')}
${extractFunction('normalizeFiveSimOperator')}
${extractFunction('normalizeFiveSimProductValue')}
${extractFunction('normalizeFiveSimMaxPriceValue')}
${extractFunction('normalizeHeroSmsMaxPriceValue')}
${extractFunction('normalizeNexSmsCountryIdValue')}
${extractFunction('normalizeNexSmsCountryOrderValue')}
${extractFunction('normalizeNexSmsServiceCodeValue')}
${extractFunction('normalizeMaDaoBaseUrlValue')}
${extractFunction('normalizeMaDaoModeValue')}
${extractFunction('normalizeMaDaoIdentifierValue')}
${extractFunction('normalizeMaDaoProviderIdValue')}
${extractFunction('normalizeMaDaoCountry')}
${extractFunction('normalizeMaDaoPriceValue')}
${extractFunction('normalizePhoneSmsMinPriceValue')}
${extractFunction('normalizePhoneSmsMaxPriceValue')}
${extractFunction('normalizeHeroSmsCountryId')}
@@ -1254,11 +1502,18 @@ function syncHeroSmsFallbackSelectionOrderFromSelect() {
? [{ id: 'vietnam', label: '越南 (Vietnam)' }]
: [{ id: 52, label: 'Thailand' }];
}
function getSelectedNexSmsCountries() { return [{ id: 1, label: 'Country #1' }]; }
function syncLatestState(patch) { latestState = { ...latestState, ...patch }; }
function loadHeroSmsCountries() { return Promise.resolve(); }
function loadFiveSimCountries() { return Promise.resolve(); }
function loadNexSmsCountries() { return Promise.resolve(); }
function applyHeroSmsFallbackSelection() {}
function applyFiveSimCountrySelection() {}
function applyNexSmsCountrySelection() {}
function setHeroSmsOperatorSelectValue(operator = latestState?.heroSmsOperator) { selectHeroSmsOperator.value = normalizeHeroSmsOperatorValue(operator); }
function refreshHeroSmsOperatorOptions() { return Promise.resolve(); }
${extractFunction('buildPhoneSmsProviderStatePatch')}
${extractFunction('applyPhoneSmsProviderFieldsToInputs')}
function updatePhoneVerificationSettingsUI() {}
function markSettingsDirty() {}
function saveSettings() { savedPayload = { ...latestState }; return Promise.resolve(); }
@@ -1268,6 +1523,7 @@ ${extractFunction('switchPhoneSmsProvider')}
return {
selectPhoneSmsProvider,
inputHeroSmsApiKey,
inputFiveSimApiKey,
inputHeroSmsMinPrice,
get latestState() { return latestState; },
get savedPayload() { return savedPayload; },
@@ -1284,11 +1540,12 @@ return {
assert.equal(api.latestState.fiveSimApiKey, 'five-old');
assert.equal(api.latestState.heroSmsMinPrice, '0.03');
assert.equal(api.latestState.fiveSimMinPrice, '0.88');
assert.equal(api.inputHeroSmsApiKey.value, 'five-old');
assert.equal(api.inputHeroSmsApiKey.value, 'hero-live');
assert.equal(api.inputFiveSimApiKey.value, 'five-old');
assert.equal(api.inputHeroSmsMinPrice.value, '0.88');
assert.equal(api.selectPhoneSmsProvider.dataset.activeProvider, '5sim');
api.inputHeroSmsApiKey.value = 'five-live';
api.inputFiveSimApiKey.value = 'five-live';
api.selectPhoneSmsProvider.value = 'hero-sms';
await api.switchPhoneSmsProvider(api.selectPhoneSmsProvider.value);
@@ -1355,6 +1612,7 @@ test('previewHeroSmsPriceTiers prefers 5sim products price for buy-compatible an
let latestState = { phoneSmsProvider: '5sim', fiveSimOperator: 'any' };
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_MADAO = 'madao';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';