469 lines
20 KiB
JavaScript
469 lines
20 KiB
JavaScript
// phone-sms/providers/sms-bower.js — SMS Bower 接码平台适配层
|
||
(function attachSmsBowerProvider(root, factory) {
|
||
root.PhoneSmsBowerProvider = factory();
|
||
})(typeof self !== 'undefined' ? self : globalThis, function createSmsBowerProviderModule() {
|
||
const PROVIDER_ID = 'sms-bower';
|
||
const DEFAULT_BASE_URL = 'https://smsbower.page/stubs/handler_api.php';
|
||
const DEFAULT_SERVICE_CODE = 'ot';
|
||
const DEFAULT_SERVICE_LABEL = 'OpenAI';
|
||
const DEFAULT_COUNTRY_ID = 6;
|
||
const DEFAULT_COUNTRY_LABEL = 'Indonesia';
|
||
const DEFAULT_REQUEST_TIMEOUT_MS = 20000;
|
||
const DEFAULT_POLL_TIMEOUT_MS = 180000;
|
||
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
||
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
|
||
const MAX_PRICE_CANDIDATES = 8;
|
||
|
||
function normalizeText(value = '', fallback = '') {
|
||
return String(value || '').trim() || fallback;
|
||
}
|
||
|
||
function normalizeSmsBowerCountryId(value, fallback = DEFAULT_COUNTRY_ID) {
|
||
const parsed = Math.floor(Number(value));
|
||
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
||
const fallbackParsed = Math.floor(Number(fallback));
|
||
return Number.isFinite(fallbackParsed) && fallbackParsed >= 0 ? fallbackParsed : DEFAULT_COUNTRY_ID;
|
||
}
|
||
|
||
function normalizeSmsBowerCountryLabel(value = '', fallback = DEFAULT_COUNTRY_LABEL) {
|
||
return normalizeText(value, fallback);
|
||
}
|
||
|
||
function normalizeCountryKey(value) {
|
||
const countryId = normalizeSmsBowerCountryId(value, -1);
|
||
return countryId >= 0 ? String(countryId) : '';
|
||
}
|
||
|
||
function normalizeSmsBowerServiceCode(value = '', fallback = DEFAULT_SERVICE_CODE) {
|
||
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||
if (normalized) return normalized;
|
||
const fallbackNormalized = String(fallback || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '');
|
||
return fallbackNormalized || DEFAULT_SERVICE_CODE;
|
||
}
|
||
|
||
function normalizePriceLimit(value) {
|
||
if (value === undefined || value === null || String(value).trim() === '') return null;
|
||
const parsed = Number(value);
|
||
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
||
return Math.round(parsed * 10000) / 10000;
|
||
}
|
||
|
||
function normalizeSmsBowerMaxPrice(value = '') {
|
||
const normalized = normalizePriceLimit(value);
|
||
return normalized === null ? '' : String(normalized);
|
||
}
|
||
|
||
function normalizeCsvIds(value = '') {
|
||
const source = Array.isArray(value) ? value : String(value || '').split(/[\r\n,,;;]+/);
|
||
const seen = new Set();
|
||
return source
|
||
.map((entry) => String(entry || '').trim().replace(/[^0-9a-zA-Z_-]+/g, ''))
|
||
.filter((entry) => {
|
||
if (!entry || seen.has(entry)) return false;
|
||
seen.add(entry);
|
||
return true;
|
||
})
|
||
.join(',');
|
||
}
|
||
|
||
function normalizeCountryOrder(value = []) {
|
||
const source = Array.isArray(value)
|
||
? value
|
||
: String(value || '').split(/[\r\n,,;;]+/).map((entry) => String(entry || '').trim()).filter(Boolean);
|
||
const seen = new Set();
|
||
const normalized = [];
|
||
source.forEach((entry) => {
|
||
let id = -1;
|
||
let label = '';
|
||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||
id = normalizeSmsBowerCountryId(entry.id ?? entry.countryId, -1);
|
||
label = normalizeText(entry.label ?? entry.countryLabel, '');
|
||
} else {
|
||
const text = String(entry || '').trim();
|
||
const structured = text.match(/^(\d+)\s*(?:[:|/-]\s*(.+))?$/);
|
||
id = normalizeSmsBowerCountryId(structured?.[1] || text, -1);
|
||
label = normalizeText(structured?.[2], '');
|
||
}
|
||
if (id < 0 || seen.has(id)) return;
|
||
seen.add(id);
|
||
normalized.push({ id, label: label || `Country #${id}` });
|
||
});
|
||
return normalized.slice(0, 20);
|
||
}
|
||
|
||
function normalizeBaseUrl(value = '') {
|
||
const trimmed = String(value || '').trim() || DEFAULT_BASE_URL;
|
||
try {
|
||
const parsed = new URL(trimmed);
|
||
return parsed.toString();
|
||
} catch {
|
||
return DEFAULT_BASE_URL;
|
||
}
|
||
}
|
||
|
||
function parsePayload(text) {
|
||
const trimmed = String(text || '').trim();
|
||
if (!trimmed) return '';
|
||
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||
try { return JSON.parse(trimmed); } catch { return trimmed; }
|
||
}
|
||
return trimmed;
|
||
}
|
||
|
||
function describePayload(raw) {
|
||
if (typeof raw === 'string') return raw.trim();
|
||
if (raw && typeof raw === 'object') {
|
||
const direct = normalizeText(raw.message || raw.msg || raw.error || raw.title || raw.status, '');
|
||
if (direct) return direct;
|
||
try { return JSON.stringify(raw); } catch { return String(raw); }
|
||
}
|
||
return String(raw || '').trim();
|
||
}
|
||
|
||
function resolveConfig(state = {}, deps = {}) {
|
||
return {
|
||
apiKey: normalizeText(state.smsBowerApiKey),
|
||
baseUrl: normalizeBaseUrl(state.smsBowerBaseUrl || DEFAULT_BASE_URL),
|
||
serviceCode: normalizeSmsBowerServiceCode(state.smsBowerServiceCode, DEFAULT_SERVICE_CODE),
|
||
fetchImpl: deps.fetchImpl || (typeof fetch === 'function' ? fetch.bind(globalThis) : null),
|
||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||
};
|
||
}
|
||
|
||
function buildUrl(config = {}, query = {}) {
|
||
const url = new URL(config.baseUrl || DEFAULT_BASE_URL);
|
||
url.searchParams.set('api_key', config.apiKey);
|
||
Object.entries(query || {}).forEach(([key, value]) => {
|
||
if (value === undefined || value === null || value === '') return;
|
||
url.searchParams.set(key, String(value));
|
||
});
|
||
return url.toString();
|
||
}
|
||
|
||
async function fetchPayload(config = {}, query = {}, actionLabel = 'SMS Bower 请求') {
|
||
if (!config.fetchImpl) throw new Error('SMS Bower 网络请求实现不可用。');
|
||
if (!config.apiKey) throw new Error('SMS Bower API Key 缺失,请先在侧边栏保存接码 API Key。');
|
||
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||
const timeoutId = controller ? setTimeout(() => controller.abort(), Number(config.requestTimeoutMs) || DEFAULT_REQUEST_TIMEOUT_MS) : null;
|
||
try {
|
||
const response = await config.fetchImpl(buildUrl(config, query), {
|
||
method: 'GET',
|
||
signal: controller?.signal,
|
||
headers: { Accept: 'application/json, text/plain, */*' },
|
||
});
|
||
const text = await response.text();
|
||
const payload = parsePayload(text);
|
||
if (!response.ok) {
|
||
const error = new Error(`${actionLabel}失败:${describePayload(payload) || response.status}`);
|
||
error.payload = payload;
|
||
error.status = response.status;
|
||
throw error;
|
||
}
|
||
const message = describePayload(payload);
|
||
if (/^(BAD_KEY|BAD_ACTION|BAD_SERVICE|BAD_STATUS|NO_ACTIVATION|EARLY_CANCEL_DENIED|BAD_COUNTRY)\b/i.test(message)) {
|
||
const error = new Error(`${actionLabel}失败:${message}`);
|
||
error.payload = payload;
|
||
throw error;
|
||
}
|
||
return payload;
|
||
} catch (error) {
|
||
if (error?.name === 'AbortError') throw new Error(`${actionLabel}超时。`);
|
||
throw error;
|
||
} finally {
|
||
if (timeoutId) clearTimeout(timeoutId);
|
||
}
|
||
}
|
||
|
||
function resolveCountryCandidates(state = {}) {
|
||
const candidates = normalizeCountryOrder(state.smsBowerCountryOrder);
|
||
if (candidates.length) return candidates;
|
||
return [{
|
||
id: normalizeSmsBowerCountryId(state.smsBowerCountryId, DEFAULT_COUNTRY_ID),
|
||
label: normalizeSmsBowerCountryLabel(state.smsBowerCountryLabel, DEFAULT_COUNTRY_LABEL),
|
||
}];
|
||
}
|
||
|
||
function resolveCountryLabel(state = {}, countryId = DEFAULT_COUNTRY_ID) {
|
||
const key = normalizeCountryKey(countryId);
|
||
const matched = resolveCountryCandidates(state).find((entry) => normalizeCountryKey(entry.id) === key);
|
||
return matched?.label || (key ? `Country #${key}` : DEFAULT_COUNTRY_LABEL);
|
||
}
|
||
|
||
function resolvePriceRange(state = {}) {
|
||
const minPriceLimit = normalizePriceLimit(state.smsBowerMinPrice);
|
||
const maxPriceLimit = normalizePriceLimit(state.smsBowerMaxPrice);
|
||
return {
|
||
minPriceLimit,
|
||
maxPriceLimit,
|
||
hasMinPriceLimit: minPriceLimit !== null,
|
||
hasMaxPriceLimit: maxPriceLimit !== null,
|
||
invalidRange: minPriceLimit !== null && maxPriceLimit !== null && minPriceLimit > maxPriceLimit,
|
||
};
|
||
}
|
||
|
||
function formatPriceRangeText(minPriceLimit = null, maxPriceLimit = null) {
|
||
const minPrice = normalizePriceLimit(minPriceLimit);
|
||
const maxPrice = normalizePriceLimit(maxPriceLimit);
|
||
if (minPrice !== null && maxPrice !== null) return `${minPrice}~${maxPrice}`;
|
||
if (minPrice !== null) return `${minPrice}~`;
|
||
if (maxPrice !== null) return `~${maxPrice}`;
|
||
return 'unbounded';
|
||
}
|
||
|
||
async function fetchBalance(state = {}, deps = {}) {
|
||
const config = resolveConfig(state, deps);
|
||
const payload = await fetchPayload(config, { action: 'getBalance' }, 'SMS Bower 查询余额');
|
||
const matched = describePayload(payload).match(/ACCESS_BALANCE\s*:\s*([\d.]+)/i);
|
||
return { balance: matched ? Number(matched[1]) : null, raw: payload };
|
||
}
|
||
|
||
async function fetchPrices(state = {}, countryConfig = {}, deps = {}) {
|
||
const config = resolveConfig(state, deps);
|
||
return fetchPayload(config, {
|
||
action: 'getPrices',
|
||
service: config.serviceCode,
|
||
country: normalizeSmsBowerCountryId(countryConfig?.id ?? state.smsBowerCountryId, DEFAULT_COUNTRY_ID),
|
||
}, 'SMS Bower 查询价格');
|
||
}
|
||
|
||
function collectPriceEntries(payload, entries = []) {
|
||
if (Array.isArray(payload)) {
|
||
payload.forEach((entry) => collectPriceEntries(entry, entries));
|
||
return entries;
|
||
}
|
||
if (!payload || typeof payload !== 'object') return entries;
|
||
const cost = Number(payload.cost ?? payload.price ?? payload.Price);
|
||
if (Number.isFinite(cost) && cost >= 0) {
|
||
const count = Number(payload.count ?? payload.qty ?? payload.Qty);
|
||
entries.push({ cost, count: Number.isFinite(count) ? count : 0, inStock: !Number.isFinite(count) || count > 0 });
|
||
}
|
||
Object.values(payload).forEach((entry) => collectPriceEntries(entry, entries));
|
||
return entries;
|
||
}
|
||
|
||
function normalizeActivation(payload, fallback = {}) {
|
||
if (typeof payload === 'string') {
|
||
const matched = payload.match(/^ACCESS_NUMBER\s*:\s*([^:]+)\s*:\s*(.+)$/i);
|
||
if (matched) {
|
||
payload = { activationId: matched[1], phoneNumber: matched[2] };
|
||
}
|
||
}
|
||
const source = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
|
||
const activationId = normalizeText(source.activationId ?? source.id ?? fallback.activationId);
|
||
const phoneNumber = normalizeText(source.phoneNumber ?? source.phone ?? fallback.phoneNumber);
|
||
if (!activationId || !phoneNumber) return null;
|
||
const countryId = normalizeSmsBowerCountryId(source.countryId ?? source.country ?? fallback.countryId, DEFAULT_COUNTRY_ID);
|
||
return {
|
||
activationId,
|
||
phoneNumber,
|
||
provider: PROVIDER_ID,
|
||
serviceCode: normalizeSmsBowerServiceCode(source.serviceCode || fallback.serviceCode || DEFAULT_SERVICE_CODE),
|
||
countryId,
|
||
countryCode: countryId,
|
||
countryLabel: normalizeSmsBowerCountryLabel(source.countryLabel || fallback.countryLabel, `Country #${countryId}`),
|
||
successfulUses: Math.max(0, Math.floor(Number(source.successfulUses ?? fallback.successfulUses) || 0)),
|
||
maxUses: 1,
|
||
};
|
||
}
|
||
|
||
function resolveActivationCountry(activation = {}, state = {}) {
|
||
const normalized = normalizeActivation(activation) || (activation && typeof activation === 'object' ? activation : {});
|
||
const id = normalizeSmsBowerCountryId(normalized.countryId ?? normalized.countryCode ?? normalized.country, DEFAULT_COUNTRY_ID);
|
||
return { id, label: resolveCountryLabel(state, id) };
|
||
}
|
||
|
||
function getActivationCountryKey(activation = {}) {
|
||
return normalizeCountryKey(activation?.countryCode ?? activation?.countryId ?? activation?.country);
|
||
}
|
||
|
||
function getActivationPrice(activation = {}) {
|
||
const price = Number(activation?.selectedPrice ?? activation?.price ?? activation?.maxPrice);
|
||
return Number.isFinite(price) && price >= 0 ? price : null;
|
||
}
|
||
|
||
function buildAcquireQuery(state = {}, config = resolveConfig(state), countryConfig = {}) {
|
||
const range = resolvePriceRange(state);
|
||
if (range.invalidRange) {
|
||
throw new Error(`SMS Bower 价格区间无效:最低购买价 ${range.minPriceLimit} 高于价格上限 ${range.maxPriceLimit}。`);
|
||
}
|
||
return {
|
||
action: 'getNumber',
|
||
service: config.serviceCode,
|
||
country: normalizeSmsBowerCountryId(countryConfig.id, DEFAULT_COUNTRY_ID),
|
||
maxPrice: range.maxPriceLimit,
|
||
minPrice: range.minPriceLimit,
|
||
};
|
||
}
|
||
|
||
async function requestActivation(state = {}, options = {}, deps = {}) {
|
||
const config = resolveConfig(state, deps);
|
||
const blocked = new Set((Array.isArray(options.blockedCountryIds) ? options.blockedCountryIds : []).map((v) => normalizeCountryKey(v)).filter(Boolean));
|
||
const candidates = resolveCountryCandidates(state).filter((entry) => !blocked.has(normalizeCountryKey(entry.id)));
|
||
const countryList = candidates.length ? candidates : resolveCountryCandidates(state);
|
||
const errors = [];
|
||
for (const countryConfig of countryList) {
|
||
try {
|
||
const payload = await fetchPayload(config, buildAcquireQuery(state, config, countryConfig), `SMS Bower 购买手机号(${countryConfig.label || countryConfig.id})`);
|
||
const activation = normalizeActivation(payload, {
|
||
countryId: countryConfig.id,
|
||
countryLabel: countryConfig.label,
|
||
serviceCode: config.serviceCode,
|
||
});
|
||
if (activation) return activation;
|
||
throw new Error(`SMS Bower 购买手机号返回不可用响应:${describePayload(payload) || '空响应'}`);
|
||
} catch (error) {
|
||
errors.push(error.message || String(error));
|
||
}
|
||
}
|
||
throw new Error(`SMS Bower 购买手机号失败:${errors.join(' | ')}`);
|
||
}
|
||
|
||
async function setActivationStatus(state = {}, activation, status, actionLabel, deps = {}) {
|
||
const normalized = normalizeActivation(activation);
|
||
if (!normalized) return '';
|
||
const config = resolveConfig(state, deps);
|
||
const payload = await fetchPayload(config, {
|
||
action: 'setStatus',
|
||
status,
|
||
id: normalized.activationId,
|
||
}, actionLabel);
|
||
return describePayload(payload);
|
||
}
|
||
|
||
async function finishActivation(state = {}, activation, deps = {}) {
|
||
return setActivationStatus(state, activation, 6, 'SMS Bower 完成订单', deps);
|
||
}
|
||
|
||
async function cancelActivation(state = {}, activation, deps = {}) {
|
||
return setActivationStatus(state, activation, 8, 'SMS Bower 取消订单', deps);
|
||
}
|
||
|
||
async function banActivation(state = {}, activation, deps = {}) {
|
||
return cancelActivation(state, activation, deps);
|
||
}
|
||
|
||
async function requestAdditionalSms(state = {}, activation, deps = {}) {
|
||
return setActivationStatus(state, activation, 3, 'SMS Bower 请求下一条短信', deps);
|
||
}
|
||
|
||
async function reuseActivation(state = {}, activation, deps = {}) {
|
||
await requestAdditionalSms(state, activation, deps);
|
||
return normalizeActivation(activation);
|
||
}
|
||
|
||
async function rotateActivation(state = {}, activation, options = {}, deps = {}) {
|
||
await (String(options?.releaseAction || '').trim().toLowerCase() === 'ban'
|
||
? banActivation(state, activation, deps)
|
||
: cancelActivation(state, activation, deps));
|
||
return { currentTicketId: String(activation?.activationId || activation?.id || ''), nextActivation: null };
|
||
}
|
||
|
||
function extractVerificationCode(raw = '') {
|
||
const trimmed = String(raw || '').trim();
|
||
if (!trimmed) return '';
|
||
const matched = trimmed.match(/\b(\d{4,8})\b/);
|
||
return matched?.[1] || trimmed.replace(/^STATUS_OK\s*:\s*/i, '').trim();
|
||
}
|
||
|
||
async function pollActivationCode(state = {}, activation, options = {}, deps = {}) {
|
||
const normalized = normalizeActivation(activation);
|
||
if (!normalized) throw new Error('缺少手机号接码订单。');
|
||
const config = resolveConfig(state, deps);
|
||
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || DEFAULT_POLL_TIMEOUT_MS);
|
||
const intervalMs = Math.max(1000, Number(options.intervalMs) || DEFAULT_POLL_INTERVAL_MS);
|
||
const maxRoundsRaw = Math.floor(Number(options.maxRounds));
|
||
const maxRounds = Number.isFinite(maxRoundsRaw) && maxRoundsRaw > 0 ? maxRoundsRaw : 0;
|
||
const start = Date.now();
|
||
let pollCount = 0;
|
||
let lastResponse = '';
|
||
while (Date.now() - start < timeoutMs) {
|
||
if (maxRounds > 0 && pollCount >= maxRounds) break;
|
||
deps.throwIfStopped?.();
|
||
const payload = await fetchPayload(config, { action: 'getStatus', id: normalized.activationId }, 'SMS Bower 查询验证码');
|
||
pollCount += 1;
|
||
lastResponse = describePayload(payload);
|
||
if (typeof options.onStatus === 'function') {
|
||
await options.onStatus({ activation: normalized, elapsedMs: Date.now() - start, pollCount, statusText: lastResponse, timeoutMs });
|
||
}
|
||
if (/^STATUS_OK\s*:/i.test(lastResponse)) return extractVerificationCode(lastResponse);
|
||
if (/^STATUS_CANCEL\b/i.test(lastResponse)) throw new Error('SMS Bower 查询验证码失败:订单已取消');
|
||
if (typeof options.onWaitingForCode === 'function') {
|
||
await options.onWaitingForCode({ activation: normalized, elapsedMs: Date.now() - start, pollCount, statusText: lastResponse, timeoutMs });
|
||
}
|
||
await deps.sleepWithStop?.(intervalMs);
|
||
}
|
||
throw new Error(`${PHONE_CODE_TIMEOUT_ERROR_PREFIX}等待手机验证码超时。${lastResponse ? ` SMS Bower 最后状态:${lastResponse}` : ''}`);
|
||
}
|
||
|
||
function createProvider(deps = {}) {
|
||
const providerDeps = {
|
||
fetchImpl: deps.fetchImpl,
|
||
sleepWithStop: deps.sleepWithStop,
|
||
throwIfStopped: deps.throwIfStopped,
|
||
requestTimeoutMs: deps.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS,
|
||
};
|
||
const capabilities = Object.freeze({
|
||
supportsReusableActivation: true,
|
||
supportsAutomaticFreeReuse: true,
|
||
supportsFreeReusePreservation: false,
|
||
supportsPageResend: false,
|
||
supportsPageResendProbe: true,
|
||
requiresCountrySelection: true,
|
||
});
|
||
return {
|
||
id: PROVIDER_ID,
|
||
label: 'SMS Bower',
|
||
capabilities,
|
||
defaultCountryId: DEFAULT_COUNTRY_ID,
|
||
defaultCountryLabel: DEFAULT_COUNTRY_LABEL,
|
||
defaultProduct: DEFAULT_SERVICE_LABEL,
|
||
defaultServiceCode: DEFAULT_SERVICE_CODE,
|
||
normalizeCountryId: normalizeSmsBowerCountryId,
|
||
normalizeCountryLabel: normalizeSmsBowerCountryLabel,
|
||
normalizeCountryKey,
|
||
normalizeServiceCode: normalizeSmsBowerServiceCode,
|
||
normalizeMaxPrice: normalizeSmsBowerMaxPrice,
|
||
normalizeCountryFallback: normalizeCountryOrder,
|
||
normalizeActivation,
|
||
resolveCountryCandidates,
|
||
resolveCountryLabel,
|
||
resolveActivationCountry,
|
||
getActivationCountryKey,
|
||
getActivationPrice,
|
||
requestActivation: (state, options) => requestActivation(state, options, providerDeps),
|
||
reuseActivation: (state, activation) => reuseActivation(state, activation, providerDeps),
|
||
finishActivation: (state, activation) => finishActivation(state, activation, providerDeps),
|
||
cancelActivation: (state, activation) => cancelActivation(state, activation, providerDeps),
|
||
banActivation: (state, activation) => banActivation(state, activation, providerDeps),
|
||
requestAdditionalSms: (state, activation) => requestAdditionalSms(state, activation, providerDeps),
|
||
rotateActivation: (state, activation, options) => rotateActivation(state, activation, options, providerDeps),
|
||
pollActivationCode: (state, activation, options) => pollActivationCode(state, activation, options, providerDeps),
|
||
prepareActivationForReuse: async (_state, activation) => ({ activation: normalizeActivation(activation), ignoredPhoneCodeKeys: [] }),
|
||
canPersistReusableActivation: () => false,
|
||
canPreserveActivationForFreeReuse: () => false,
|
||
shouldUsePageResend: () => false,
|
||
shouldProbePageResend: () => true,
|
||
fetchBalance: (state) => fetchBalance(state, providerDeps),
|
||
fetchPrices: (state, countryConfig) => fetchPrices(state, countryConfig, providerDeps),
|
||
resolvePriceRange,
|
||
formatPriceRangeText,
|
||
collectPriceEntries,
|
||
describePayload,
|
||
};
|
||
}
|
||
|
||
return {
|
||
PROVIDER_ID,
|
||
DEFAULT_COUNTRY_ID,
|
||
DEFAULT_COUNTRY_LABEL,
|
||
DEFAULT_SERVICE_CODE,
|
||
createProvider,
|
||
normalizeSmsBowerCountryId,
|
||
normalizeSmsBowerCountryLabel,
|
||
normalizeCountryKey,
|
||
normalizeSmsBowerServiceCode,
|
||
normalizeSmsBowerMaxPrice,
|
||
normalizeActivation,
|
||
};
|
||
});
|