From 61e9b40d85ed72285c0b8d1aec1fb20d6cac60b4 Mon Sep 17 00:00:00 2001 From: QLHazyCoder Date: Sun, 3 May 2026 15:14:31 +0800 Subject: [PATCH] fix: harden GoPay and 5sim PR paths --- background.js | 7 +++-- background/phone-verification-flow.js | 20 +++++++++---- background/steps/gopay-approve.js | 17 ++++++----- phone-sms/providers/five-sim.js | 32 ++++++++++++++------- tests/background-step-registry.test.js | 5 ++++ tests/five-sim-provider.test.js | 40 ++++++++++++++++++++------ tests/gopay-approve-source.test.js | 11 ++++++- tests/phone-verification-flow.test.js | 37 ++++++++++++++++++++++++ 8 files changed, 134 insertions(+), 35 deletions(-) diff --git a/background.js b/background.js index ae5fc87..11cac6d 100644 --- a/background.js +++ b/background.js @@ -84,6 +84,7 @@ const PLUS_GOPAY_STEP_IDS = PLUS_GOPAY_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite) .sort((left, right) => left - right); +const PLUS_STEP_IDS = PLUS_PAYPAL_STEP_IDS; const LAST_STEP_ID = Math.max( NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10, PLUS_PAYPAL_STEP_IDS[PLUS_PAYPAL_STEP_IDS.length - 1] || 10, @@ -445,7 +446,7 @@ function getStepDefinitionsForState(state = {}) { if (!isPlusModeState(state)) { return NORMAL_STEP_DEFINITIONS; } - return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay' + return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS; } @@ -454,7 +455,7 @@ function getStepIdsForState(state = {}) { if (!isPlusModeState(state)) { return NORMAL_STEP_IDS; } - return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay' + return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_IDS : PLUS_PAYPAL_STEP_IDS; } @@ -10054,7 +10055,7 @@ function getStepRegistryForState(state = {}) { if (!isPlusModeState(state)) { return normalStepRegistry; } - return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay' + return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY ? plusGoPayStepRegistry : plusPayPalStepRegistry; } diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index 0b558f1..8bcec48 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -275,6 +275,13 @@ return Math.max(500, Math.min(30000, parsed)); } + function assertFiveSimMaxPriceCompatibleWithOperator(operator, maxPriceLimit) { + const normalizedOperator = normalizeFiveSimCountryCode(operator, DEFAULT_FIVE_SIM_OPERATOR); + if (maxPriceLimit !== null && maxPriceLimit !== undefined && normalizedOperator !== DEFAULT_FIVE_SIM_OPERATOR) { + throw new Error('5sim maxPrice only works when operator is "any"; clear the price limit or switch operator to any before buying a number.'); + } + } + function normalizeHeroSmsPriceLimit(value) { if (value === undefined || value === null || String(value).trim() === '') { return null; @@ -1381,16 +1388,19 @@ if (!apiKey) { throw new Error('5sim API key is missing. Save it in the side panel before running the phone flow.'); } - const configuredMaxPrice = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice); + const configuredMaxPrice = normalizeHeroSmsPriceLimit(state.fiveSimMaxPrice); + const operator = normalizeFiveSimCountryCode(state.fiveSimOperator, DEFAULT_FIVE_SIM_OPERATOR); + const maxPriceLimit = configuredMaxPrice !== null + ? configuredMaxPrice + : normalizeHeroSmsPriceLimit(state.heroSmsMaxPrice); + assertFiveSimMaxPriceCompatibleWithOperator(operator, maxPriceLimit); return { provider, apiKey, baseUrl: normalizeUrl(state.fiveSimBaseUrl, DEFAULT_FIVE_SIM_BASE_URL).replace(/\/+$/, ''), - operator: normalizeFiveSimCountryCode(state.fiveSimOperator, DEFAULT_FIVE_SIM_OPERATOR), + operator, product: normalizeFiveSimCountryCode(state.fiveSimProduct, DEFAULT_FIVE_SIM_PRODUCT), - maxPriceLimit: configuredMaxPrice !== null - ? configuredMaxPrice - : normalizeHeroSmsPriceLimit(state.heroSmsMaxPrice), + maxPriceLimit, countryCandidates: resolveFiveSimCountryCandidates(state), }; } diff --git a/background/steps/gopay-approve.js b/background/steps/gopay-approve.js index e34bde8..3403659 100644 --- a/background/steps/gopay-approve.js +++ b/background/steps/gopay-approve.js @@ -910,13 +910,16 @@ const target = Number.isInteger(frameId) ? await sendGoPayFrameCommand(tabId, frameId, targetMessageType, {}) : await sendGoPayCommand(tabId, targetMessageType, {}); - const rect = target?.rect || null; - if (!target?.found || !rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) { - return { clicked: false, reason: 'target_not_found', clickTarget: target?.target || '' }; - } - await clickWithDebugger(tabId, rect); - return { clicked: true, clickTarget: target.target || '' }; - } + const rect = target?.rect || null; + if (!target?.found || !rect || !Number.isFinite(rect.centerX) || !Number.isFinite(rect.centerY)) { + return { clicked: false, reason: 'target_not_found', clickTarget: target?.target || '' }; + } + if (Number.isInteger(frameId)) { + return { clicked: false, reason: 'debugger_click_skipped_for_frame_target', clickTarget: target.target || '' }; + } + await clickWithDebugger(tabId, rect); + return { clicked: true, clickTarget: target.target || '' }; + } async function clickGoPayContinueWithDebugger(tabId, frameId = null) { diff --git a/phone-sms/providers/five-sim.js b/phone-sms/providers/five-sim.js index 4fb5e7b..77a57c6 100644 --- a/phone-sms/providers/five-sim.js +++ b/phone-sms/providers/five-sim.js @@ -569,14 +569,22 @@ return new Error(`${FIVE_SIM_RATE_LIMIT_ERROR_PREFIX}5sim 购买接口触发限流,请稍后再试${suffix}`); } - function isTerminalError(payloadOrMessage) { - const text = describePayload(payloadOrMessage); - return /not\s+enough\s+(?:user\s+)?balance|not\s+enough\s+rating|unauthorized|invalid\s+token|banned|bad\s+(?:country|operator)|no\s+product|server\s+offline/i.test(text); - } - - async function buyActivationWithPrice(state = {}, countryConfig, maxPrice, deps = {}) { - const config = resolveConfig(state, deps); - const operator = normalizeFiveSimOperator(state.fiveSimOperator); + function isTerminalError(payloadOrMessage) { + const text = describePayload(payloadOrMessage); + return /not\s+enough\s+(?:user\s+)?balance|not\s+enough\s+rating|unauthorized|invalid\s+token|banned|bad\s+(?:country|operator)|no\s+product|server\s+offline/i.test(text); + } + + function assertMaxPriceCompatibleWithOperator(state = {}) { + const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice); + const operator = normalizeFiveSimOperator(state.fiveSimOperator); + if (maxPrice && operator !== DEFAULT_OPERATOR) { + throw new Error('5sim maxPrice only works when operator is "any"; clear the price limit or switch operator to any before buying a number.'); + } + } + + async function buyActivationWithPrice(state = {}, countryConfig, maxPrice, deps = {}) { + const config = resolveConfig(state, deps); + const operator = normalizeFiveSimOperator(state.fiveSimOperator); const query = {}; if (maxPrice !== null && maxPrice !== undefined) { query.maxPrice = maxPrice; @@ -605,9 +613,11 @@ return activation; } - async function requestActivation(state = {}, options = {}, deps = {}) { - const allCountryCandidates = resolveCountryCandidates(state); - const blockedCountryIds = new Set( + async function requestActivation(state = {}, options = {}, deps = {}) { + assertMaxPriceCompatibleWithOperator(state); + + const allCountryCandidates = resolveCountryCandidates(state); + const blockedCountryIds = new Set( (Array.isArray(options?.blockedCountryIds) ? options.blockedCountryIds : []) .map((value) => normalizeFiveSimCountryId(value, '')) .filter(Boolean) diff --git a/tests/background-step-registry.test.js b/tests/background-step-registry.test.js index d49bcba..e8541ab 100644 --- a/tests/background-step-registry.test.js +++ b/tests/background-step-registry.test.js @@ -8,6 +8,11 @@ test('background imports step registry and shared step definitions', () => { assert.match(source, /data\/step-definitions\.js/); assert.match(source, /MultiPageStepDefinitions\?\.getSteps/); assert.match(source, /getStepRegistryForState\(state\)/); + assert.match(source, /PLUS_PAYPAL_STEP_DEFINITIONS/); + assert.match(source, /PLUS_GOPAY_STEP_DEFINITIONS/); + assert.match(source, /plusPayPalStepRegistry/); + assert.match(source, /plusGoPayStepRegistry/); + assert.match(source, /normalizePlusPaymentMethod\(state\?\.plusPaymentMethod\) === PLUS_PAYMENT_METHOD_GOPAY/); assert.match(source, /activeStepRegistry\.executeStep\(step,\s*\{/); assert.match(source, /background\/steps\/create-plus-checkout\.js/); assert.match(source, /background\/steps\/fill-plus-checkout\.js/); diff --git a/tests/five-sim-provider.test.js b/tests/five-sim-provider.test.js index 86f00eb..9f8118c 100644 --- a/tests/five-sim-provider.test.js +++ b/tests/five-sim-provider.test.js @@ -127,9 +127,9 @@ test('5sim provider buys, checks, finishes, cancels, bans, and reuses activation ); }); -test('5sim provider prefers buy-compatible products price over operator detail price', async () => { - const requests = []; - const provider = api.createProvider({ +test('5sim provider prefers buy-compatible products price over operator detail price', async () => { + const requests = []; + const provider = api.createProvider({ fetchImpl: async (url, options = {}) => { const parsed = new URL(url); requests.push({ url: parsed, options }); @@ -172,11 +172,35 @@ test('5sim provider prefers buy-compatible products price over operator detail p '/v1/guest/prices', '/v1/user/buy/activation/vietnam/any/openai', ] - ); -}); - -test('5sim provider reports raw buy payload when HTTP 200 response has no activation', async () => { - const provider = api.createProvider({ + ); +}); + +test('5sim provider rejects maxPrice with custom operator before buying', async () => { + const requests = []; + const provider = api.createProvider({ + fetchImpl: async (url) => { + requests.push(url); + throw new Error(`unexpected request ${url}`); + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => provider.requestActivation({ + fiveSimApiKey: 'demo-key', + fiveSimCountryId: 'vietnam', + fiveSimCountryLabel: '瓒婂崡 (Vietnam)', + fiveSimMaxPrice: '12', + fiveSimOperator: 'virtual21', + }), + /maxPrice only works when operator is "any"/ + ); + assert.deepStrictEqual(requests, []); +}); + +test('5sim provider reports raw buy payload when HTTP 200 response has no activation', async () => { + const provider = api.createProvider({ fetchImpl: async (url) => { const parsed = new URL(url); if (parsed.pathname === '/v1/guest/products/vietnam/any') { diff --git a/tests/gopay-approve-source.test.js b/tests/gopay-approve-source.test.js index 02d1cad..a7e3593 100644 --- a/tests/gopay-approve-source.test.js +++ b/tests/gopay-approve-source.test.js @@ -69,7 +69,16 @@ test('GoPay approve handles final payment details iframe as an action frame', () assert.match(source, /最终 Bayar 确认/); }); -test('GoPay approve treats merchant validate-pin iframe as PIN entry frame', () => { +test('GoPay debugger click does not reuse iframe-relative rects as top-level coordinates', () => { + const body = extractFunction('clickGoPayTargetWithDebugger'); + assert.match(body, /Number\.isInteger\(frameId\)/); + assert.match(body, /debugger_click_skipped_for_frame_target/); + assert.ok( + body.indexOf('debugger_click_skipped_for_frame_target') < body.indexOf('clickWithDebugger(tabId, rect)') + ); +}); + +test('GoPay approve treats merchant validate-pin iframe as PIN entry frame', () => { assert.match(source, /GOPAY_PIN_FRAME_URL_PATTERN/); assert.match(source, /payment\\\/validate-pin/); assert.match(source, /kind: 'pin'/); diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index 76c7ac7..e07496c 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -969,6 +969,43 @@ test('phone verification helper acquires a number from 5sim with fallback countr assert.equal(requests[3].pathname, '/v1/user/buy/activation/england/any/openai'); }); +test('phone verification helper rejects 5sim maxPrice with custom operator before buying', async () => { + const requests = []; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + requests.push(url); + throw new Error(`Unexpected 5sim request: ${url}`); + }, + getState: async () => ({ + phoneSmsProvider: '5sim', + fiveSimApiKey: 'five-token', + fiveSimCountryOrder: ['vietnam'], + fiveSimOperator: 'virtual21', + fiveSimMaxPrice: '0.1', + heroSmsActivationRetryRounds: 1, + }), + sendToContentScriptResilient: async () => ({}), + setState: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => helpers.requestPhoneActivation({ + phoneSmsProvider: '5sim', + fiveSimApiKey: 'five-token', + fiveSimCountryOrder: ['vietnam'], + fiveSimOperator: 'virtual21', + fiveSimMaxPrice: '0.1', + heroSmsActivationRetryRounds: 1, + }), + /maxPrice only works when operator is "any"/ + ); + assert.deepStrictEqual(requests, []); +}); + test('phone verification helper honors price-priority ordering for 5sim countries', async () => { const requests = []; const helpers = api.createPhoneVerificationHelpers({