This commit is contained in:
QLHazyCoder
2026-04-28 23:47:54 +08:00
19 changed files with 973 additions and 394 deletions
+20
View File
@@ -481,6 +481,7 @@ const PERSISTED_SETTING_DEFAULTS = {
mail2925Accounts: [], mail2925Accounts: [],
paypalAccounts: [], paypalAccounts: [],
heroSmsApiKey: '', heroSmsApiKey: '',
heroSmsMaxPrice: '',
heroSmsCountryId: HERO_SMS_COUNTRY_ID, heroSmsCountryId: HERO_SMS_COUNTRY_ID,
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL, heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
}; };
@@ -563,6 +564,7 @@ const DEFAULT_STATE = {
currentLuckmailMailCursor: null, currentLuckmailMailCursor: null,
currentPhoneActivation: null, currentPhoneActivation: null,
reusablePhoneActivation: null, reusablePhoneActivation: null,
pendingPhoneActivationConfirmation: null,
autoRunning: false, // 当前是否处于自动运行中。 autoRunning: false, // 当前是否处于自动运行中。
autoRunPhase: 'idle', // 当前自动运行阶段。 autoRunPhase: 'idle', // 当前自动运行阶段。
autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。 autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。
@@ -1325,6 +1327,8 @@ function normalizePersistentSettingValue(key, value) {
return normalizePayPalAccounts(value); return normalizePayPalAccounts(value);
case 'heroSmsApiKey': case 'heroSmsApiKey':
return String(value || ''); return String(value || '');
case 'heroSmsMaxPrice':
return String(value || '').trim();
case 'heroSmsCountryId': case 'heroSmsCountryId':
return Math.max(1, Math.floor(Number(value) || HERO_SMS_COUNTRY_ID)); return Math.max(1, Math.floor(Number(value) || HERO_SMS_COUNTRY_ID));
case 'heroSmsCountryLabel': case 'heroSmsCountryLabel':
@@ -5292,6 +5296,13 @@ async function finalizeIcloudAliasAfterSuccessfulFlow(state) {
} }
} }
async function finalizePhoneActivationAfterSuccessfulFlow(state) {
if (typeof phoneVerificationHelpers?.finalizePendingPhoneActivationConfirmation !== 'function') {
return null;
}
return phoneVerificationHelpers.finalizePendingPhoneActivationConfirmation(state);
}
// ============================================================ // ============================================================
// Tab Registry // Tab Registry
// ============================================================ // ============================================================
@@ -6027,6 +6038,7 @@ function getDownstreamStateResets(step, state = {}) {
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null, oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null, oauthFlowDeadlineSourceUrl: null,
pendingPhoneActivationConfirmation: null,
lastSignupCode: null, lastSignupCode: null,
lastLoginCode: null, lastLoginCode: null,
localhostUrl: null, localhostUrl: null,
@@ -6041,6 +6053,7 @@ function getDownstreamStateResets(step, state = {}) {
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null, oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null, oauthFlowDeadlineSourceUrl: null,
pendingPhoneActivationConfirmation: null,
lastSignupCode: null, lastSignupCode: null,
lastLoginCode: null, lastLoginCode: null,
localhostUrl: null, localhostUrl: null,
@@ -6054,6 +6067,7 @@ function getDownstreamStateResets(step, state = {}) {
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null, oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null, oauthFlowDeadlineSourceUrl: null,
pendingPhoneActivationConfirmation: null,
lastSignupCode: null, lastSignupCode: null,
lastLoginCode: null, lastLoginCode: null,
localhostUrl: null, localhostUrl: null,
@@ -6076,11 +6090,13 @@ function getDownstreamStateResets(step, state = {}) {
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null, oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null, oauthFlowDeadlineSourceUrl: null,
pendingPhoneActivationConfirmation: null,
localhostUrl: null, localhostUrl: null,
}; };
} }
if (step === 9) { if (step === 9) {
return { return {
pendingPhoneActivationConfirmation: null,
plusReturnUrl: '', plusReturnUrl: '',
localhostUrl: null, localhostUrl: null,
}; };
@@ -6091,11 +6107,13 @@ function getDownstreamStateResets(step, state = {}) {
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null, oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null, oauthFlowDeadlineSourceUrl: null,
pendingPhoneActivationConfirmation: null,
localhostUrl: null, localhostUrl: null,
}; };
} }
if (stepKey === 'confirm-oauth') { if (stepKey === 'confirm-oauth') {
return { return {
pendingPhoneActivationConfirmation: null,
localhostUrl: null, localhostUrl: null,
}; };
} }
@@ -6946,6 +6964,7 @@ async function handleStepData(step, payload) {
if ((shouldUseCustomRegistrationEmail(latestState) || shouldClearCustomPoolEmail) && latestState.email) { if ((shouldUseCustomRegistrationEmail(latestState) || shouldClearCustomPoolEmail) && latestState.email) {
await setEmailStateSilently(null); await setEmailStateSilently(null);
} }
await finalizePhoneActivationAfterSuccessfulFlow(latestState);
break; break;
} }
} }
@@ -8824,6 +8843,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
executeStepViaCompletionSignal, executeStepViaCompletionSignal,
exportSettingsBundle, exportSettingsBundle,
fetchGeneratedEmail, fetchGeneratedEmail,
finalizePhoneActivationAfterSuccessfulFlow,
finalizeStep3Completion: async () => { finalizeStep3Completion: async () => {
const currentState = await getState(); const currentState = await getState();
const signupTabId = await getTabId('signup-page'); const signupTabId = await getTabId('signup-page');
+1
View File
@@ -392,6 +392,7 @@
inbucketMailbox: prevState.inbucketMailbox, inbucketMailbox: prevState.inbucketMailbox,
cloudflareDomain: prevState.cloudflareDomain, cloudflareDomain: prevState.cloudflareDomain,
cloudflareDomains: prevState.cloudflareDomains, cloudflareDomains: prevState.cloudflareDomains,
reusablePhoneActivation: prevState.reusablePhoneActivation,
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries), autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
autoRunSessionId: sessionId, autoRunSessionId: sessionId,
tabRegistry: {}, tabRegistry: {},
+4
View File
@@ -32,6 +32,7 @@
executeStepViaCompletionSignal, executeStepViaCompletionSignal,
exportSettingsBundle, exportSettingsBundle,
fetchGeneratedEmail, fetchGeneratedEmail,
finalizePhoneActivationAfterSuccessfulFlow,
finalizeStep3Completion, finalizeStep3Completion,
finalizeIcloudAliasAfterSuccessfulFlow, finalizeIcloudAliasAfterSuccessfulFlow,
findHotmailAccount, findHotmailAccount,
@@ -209,6 +210,9 @@
}); });
} }
await finalizeIcloudAliasAfterSuccessfulFlow(latestState); await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
if (typeof finalizePhoneActivationAfterSuccessfulFlow === 'function') {
await finalizePhoneActivationAfterSuccessfulFlow(latestState);
}
} }
async function handleStepData(step, payload) { async function handleStepData(step, payload) {
+94 -145
View File
@@ -21,13 +21,13 @@
const PHONE_ACTIVATION_STATE_KEY = 'currentPhoneActivation'; const PHONE_ACTIVATION_STATE_KEY = 'currentPhoneActivation';
const REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'reusablePhoneActivation'; const REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'reusablePhoneActivation';
const PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY = 'pendingPhoneActivationConfirmation';
const DEFAULT_PHONE_POLL_INTERVAL_MS = 5000; const DEFAULT_PHONE_POLL_INTERVAL_MS = 5000;
const DEFAULT_PHONE_POLL_TIMEOUT_MS = 180000; const DEFAULT_PHONE_POLL_TIMEOUT_MS = 180000;
const DEFAULT_PHONE_REQUEST_TIMEOUT_MS = 20000; const DEFAULT_PHONE_REQUEST_TIMEOUT_MS = 20000;
const DEFAULT_PHONE_SUBMIT_ATTEMPTS = 3; const DEFAULT_PHONE_SUBMIT_ATTEMPTS = 3;
const DEFAULT_PHONE_CODE_WAIT_WINDOW_MS = 60000; const DEFAULT_PHONE_CODE_WAIT_WINDOW_MS = 60000;
const DEFAULT_PHONE_NUMBER_MAX_USES = 3; 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_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::'; const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
@@ -47,6 +47,18 @@
return String(value || '').trim(); 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) { function normalizeUseCount(value) {
return Math.max(0, Math.floor(Number(value) || 0)); return Math.max(0, Math.floor(Number(value) || 0));
} }
@@ -236,13 +248,19 @@
} }
} }
function resolvePhoneConfig(state = {}) { function resolvePhoneConfig(state = {}, options = {}) {
const apiKey = normalizeApiKey(state.heroSmsApiKey); const apiKey = normalizeApiKey(state.heroSmsApiKey);
if (!apiKey) { if (!apiKey) {
throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.'); throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.');
} }
const requireMaxPrice = Boolean(options.requireMaxPrice);
const maxPrice = normalizeManualHeroSmsMaxPrice(state.heroSmsMaxPrice);
if (requireMaxPrice && !maxPrice) {
throw new Error('HeroSMS maxPrice is missing. Fill it in below the country selector before running the phone flow.');
}
return { return {
apiKey, apiKey,
...(maxPrice ? { maxPrice } : {}),
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL), baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
}; };
} }
@@ -289,91 +307,10 @@
return activation?.statusAction === 'getStatusV2' ? 'getStatusV2' : 'getStatus'; 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) { function isHeroSmsNoNumbersPayload(payload) {
return /\bNO_NUMBERS\b/i.test(describeHeroSmsPayload(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 = {}) { async function fetchPhoneActivationPayload(config, countryConfig, action, options = {}) {
const query = { const query = {
action, action,
@@ -387,49 +324,10 @@
return fetchHeroSmsPayload(config, query, `HeroSMS ${action}`); 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 = {}) { async function requestPhoneActivation(state = {}) {
const config = resolvePhoneConfig(state); const config = resolvePhoneConfig(state, { requireMaxPrice: true });
const countryConfig = resolveCountryConfig(state); const countryConfig = resolveCountryConfig(state);
const maxPrice = await resolveCheapestPhoneActivationPrice(config, countryConfig); const maxPrice = config.maxPrice;
const buildFallbackActivation = (requestAction) => ({ const buildFallbackActivation = (requestAction) => ({
countryId: countryConfig.id, countryId: countryConfig.id,
...(requestAction === 'getNumberV2' ? { statusAction: 'getStatusV2' } : {}), ...(requestAction === 'getNumberV2' ? { statusAction: 'getStatusV2' } : {}),
@@ -438,19 +336,19 @@
let payload; let payload;
try { try {
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice); payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
} catch (error) { } catch (error) {
if (!isHeroSmsNoNumbersPayload(error?.payload || error?.message)) { if (!isHeroSmsNoNumbersPayload(error?.payload || error?.message)) {
throw error; throw error;
} }
requestAction = 'getNumberV2'; requestAction = 'getNumberV2';
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice); payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
} }
let activation = parseActivationPayload(payload, buildFallbackActivation(requestAction)); let activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
if (!activation && requestAction === 'getNumber' && isHeroSmsNoNumbersPayload(payload)) { if (!activation && requestAction === 'getNumber' && isHeroSmsNoNumbersPayload(payload)) {
requestAction = 'getNumberV2'; requestAction = 'getNumberV2';
payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice); payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
activation = parseActivationPayload(payload, buildFallbackActivation(requestAction)); activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
} }
@@ -496,7 +394,7 @@
} }
async function completePhoneActivation(state = {}, activation) { 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) { async function cancelPhoneActivation(state = {}, activation) {
@@ -738,6 +636,18 @@
await persistReusableActivation(null); await persistReusableActivation(null);
} }
function incrementActivationUseCount(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
return null;
}
return {
...normalizedActivation,
successfulUses: Math.min(normalizedActivation.successfulUses + 1, normalizedActivation.maxUses),
};
}
async function acquirePhoneActivation(state = {}) { async function acquirePhoneActivation(state = {}) {
const countryConfig = resolveCountryConfig(state); const countryConfig = resolveCountryConfig(state);
const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]); const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]);
@@ -769,28 +679,53 @@
return activation; return activation;
} }
async function markActivationReusableAfterSuccess(activation) { async function syncReusableActivationAfterUse(activation) {
const normalizedActivation = normalizeActivation(activation); const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) { if (!normalizedActivation) {
await clearReusableActivation(); await clearReusableActivation();
return; return;
} }
const successfulUses = normalizedActivation.successfulUses + 1; if (normalizedActivation.successfulUses >= normalizedActivation.maxUses) {
if (successfulUses >= normalizedActivation.maxUses) {
await clearReusableActivation(); await clearReusableActivation();
return; return;
} }
await persistReusableActivation({ await persistReusableActivation(normalizedActivation);
...normalizedActivation, }
successfulUses,
async function persistPendingPhoneActivationConfirmation(activation) {
await setState({
[PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY]: activation || null,
}); });
} }
async function clearPendingPhoneActivationConfirmation() {
await persistPendingPhoneActivationConfirmation(null);
}
async function finalizePendingPhoneActivationConfirmation(stateOverride = null) {
const state = stateOverride || await getState();
const pendingActivation = normalizeActivation(state[PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY]);
if (!pendingActivation) {
return null;
}
const committedActivation = incrementActivationUseCount(pendingActivation);
if (!committedActivation) {
await clearPendingPhoneActivationConfirmation();
await clearReusableActivation();
return null;
}
await syncReusableActivationAfterUse(committedActivation);
await clearPendingPhoneActivationConfirmation();
return committedActivation;
}
async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) { async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) {
const normalizedActivation = normalizeActivation(activation); let currentActivation = normalizeActivation(activation);
if (!normalizedActivation) { if (!currentActivation) {
throw new Error('Phone activation is missing.'); throw new Error('Phone activation is missing.');
} }
@@ -799,11 +734,11 @@
for (let windowIndex = 1; windowIndex <= 2; windowIndex += 1) { for (let windowIndex = 1; windowIndex <= 2; windowIndex += 1) {
await addLog( 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' 'info'
); );
try { try {
const code = await pollPhoneActivationCode(state, normalizedActivation, { const code = await pollPhoneActivationCode(state, currentActivation, {
actionLabel: windowIndex === 1 actionLabel: windowIndex === 1
? 'poll phone verification code from HeroSMS' ? 'poll phone verification code from HeroSMS'
: 'poll resent phone verification code from HeroSMS', : 'poll resent phone verification code from HeroSMS',
@@ -820,7 +755,7 @@
lastLoggedStatus = statusText; lastLoggedStatus = statusText;
lastLoggedPollCount = pollCount; lastLoggedPollCount = pollCount;
await addLog( 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' 'info'
); );
}, },
@@ -836,10 +771,10 @@
if (windowIndex === 1) { if (windowIndex === 1) {
await addLog( 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' 'warn'
); );
await requestAdditionalPhoneSms(state, normalizedActivation); await requestAdditionalPhoneSms(state, currentActivation);
try { try {
await resendPhoneVerificationCode(tabId); await resendPhoneVerificationCode(tabId);
await addLog('Step 9: clicked "Resend text message" on the phone verification page.', 'info'); await addLog('Step 9: clicked "Resend text message" on the phone verification page.', 'info');
@@ -850,10 +785,10 @@
} }
await addLog( 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' 'warn'
); );
throw buildPhoneRestartStep7Error(normalizedActivation.phoneNumber); throw buildPhoneRestartStep7Error(currentActivation.phoneNumber);
} }
} }
@@ -875,6 +810,9 @@
} }
if (pageState?.addPhonePage) { if (pageState?.addPhonePage) {
if (normalizeActivation(state[PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY])) {
await clearPendingPhoneActivationConfirmation();
}
if (activation) { if (activation) {
await cancelPhoneActivation(state, activation); await cancelPhoneActivation(state, activation);
await clearCurrentActivation(); await clearCurrentActivation();
@@ -965,8 +903,18 @@
continue; continue;
} }
await completePhoneActivation(state, activation); try {
await markActivationReusableAfterSuccess(activation); await completePhoneActivation(state, activation);
await persistReusableActivation(activation);
await persistPendingPhoneActivationConfirmation(activation);
} catch (activationStatusError) {
await clearReusableActivation();
await clearPendingPhoneActivationConfirmation();
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; shouldCancelActivation = false;
await clearCurrentActivation(); await clearCurrentActivation();
await addLog('Step 9: phone verification finished, waiting for OAuth consent.', 'ok'); await addLog('Step 9: phone verification finished, waiting for OAuth consent.', 'ok');
@@ -988,6 +936,7 @@
return { return {
completePhoneVerificationFlow, completePhoneVerificationFlow,
finalizePendingPhoneActivationConfirmation,
normalizeActivation, normalizeActivation,
pollPhoneActivationCode, pollPhoneActivationCode,
reactivatePhoneActivation, reactivatePhoneActivation,
+67 -3
View File
@@ -27,6 +27,7 @@
reuseOrCreateTab, reuseOrCreateTab,
setState, setState,
shouldUseCustomRegistrationEmail, shouldUseCustomRegistrationEmail,
sleepWithStop,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS, STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
throwIfStopped, throwIfStopped,
@@ -167,10 +168,26 @@
}); });
} }
async function runStep8Attempt(state) { function getStep8ResendIntervalMs(state = {}) {
const mail = getMailConfig(state);
if (mail?.provider === LUCKMAIL_PROVIDER) {
return 15000;
}
if (mail?.provider === HOTMAIL_PROVIDER || mail?.provider === '2925') {
return 0;
}
return Math.max(0, Number(STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS) || 0);
}
async function runStep8Attempt(state, runtime = {}) {
const visibleStep = getVisibleStep(state, 8); const visibleStep = getVisibleStep(state, 8);
const mail = getMailConfig(state); const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error); if (mail.error) throw new Error(mail.error);
const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt);
const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
? runtime.onResendRequestedAt
: null;
const stepStartedAt = Date.now(); const stepStartedAt = Date.now();
const verificationFilterAfterTimestamp = mail.provider === '2925' const verificationFilterAfterTimestamp = mail.provider === '2925'
@@ -267,6 +284,16 @@
disableTimeBudgetCap: mail.provider === '2925', disableTimeBudgetCap: mail.provider === '2925',
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep), getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
requestFreshCodeFirst: false, requestFreshCodeFirst: false,
lastResendAt: latestResendAt,
onResendRequestedAt: async (requestedAt) => {
const numericRequestedAt = Number(requestedAt) || 0;
if (numericRequestedAt > 0) {
latestResendAt = Math.max(latestResendAt, numericRequestedAt);
}
if (notifyResendRequestedAt) {
await notifyResendRequestedAt(latestResendAt);
}
},
targetEmail: fixedTargetEmail, targetEmail: fixedTargetEmail,
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
? 15000 ? 15000
@@ -274,6 +301,9 @@
? 0 ? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS), : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
}); });
return {
lastResendAt: latestResendAt,
};
} }
function isStep8RestartStep7Error(error) { function isStep8RestartStep7Error(error) {
@@ -285,10 +315,22 @@
let currentState = state; let currentState = state;
let mailPollingAttempt = 1; let mailPollingAttempt = 1;
let lastMailPollingError = null; let lastMailPollingError = null;
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
while (true) { while (true) {
try { try {
await runStep8Attempt(currentState); const result = await runStep8Attempt(currentState, {
stickyLastResendAt,
onResendRequestedAt: async (requestedAt) => {
const numericRequestedAt = Number(requestedAt) || 0;
if (numericRequestedAt > 0) {
stickyLastResendAt = Math.max(stickyLastResendAt, numericRequestedAt);
}
},
});
if (Number(result?.lastResendAt) > 0) {
stickyLastResendAt = Math.max(stickyLastResendAt, Number(result.lastResendAt) || 0);
}
return; return;
} catch (err) { } catch (err) {
const visibleStep = getVisibleStep(currentState, 8); const visibleStep = getVisibleStep(currentState, 8);
@@ -323,7 +365,29 @@
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}`, `步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}`,
'warn' 'warn'
); );
currentState = await getState(); const latestState = await getState();
const latestStateResendAt = Number(latestState?.loginVerificationRequestedAt) || 0;
if (latestStateResendAt > 0) {
stickyLastResendAt = Math.max(stickyLastResendAt, latestStateResendAt);
}
currentState = latestState;
if (stickyLastResendAt > 0 && (!latestStateResendAt || latestStateResendAt < stickyLastResendAt)) {
currentState = {
...latestState,
loginVerificationRequestedAt: stickyLastResendAt,
};
}
const resendIntervalMs = getStep8ResendIntervalMs(currentState);
const remainingBeforeRetryMs = stickyLastResendAt > 0 && resendIntervalMs > 0
? Math.max(0, resendIntervalMs - (Date.now() - stickyLastResendAt))
: 0;
if (remainingBeforeRetryMs > 0 && typeof sleepWithStop === 'function') {
await addLog(
`步骤 ${visibleStep}:上轮已触发重发验证码,为避免重复重发,先等待 ${Math.ceil(remainingBeforeRetryMs / 1000)} 秒后继续当前链路重试。`,
'info'
);
await sleepWithStop(Math.min(remainingBeforeRetryMs, 3000));
}
continue; continue;
} }
await addLog( await addLog(
+11 -1
View File
@@ -920,8 +920,18 @@
const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15; const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15;
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0); const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
let lastResendAt = Number(options.lastResendAt) || 0; let lastResendAt = Number(options.lastResendAt) || 0;
const externalOnResendRequestedAt = typeof options.onResendRequestedAt === 'function'
? options.onResendRequestedAt
: null;
const updateFilterAfterTimestampForVerificationStep = async (_requestedAt) => { const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
if (externalOnResendRequestedAt) {
try {
await externalOnResendRequestedAt(requestedAt);
} catch (_) {
// Keep resend flow best-effort; state sync callback failures should not break verification.
}
}
return nextFilterAfterTimestamp; return nextFilterAfterTimestamp;
}; };
+50
View File
@@ -123,6 +123,48 @@ def log_info(message):
print(f"[HotmailHelper] {message}", flush=True) print(f"[HotmailHelper] {message}", flush=True)
def is_openai_sender(address):
sender = str(address or "").strip().lower()
return "openai" in sender
def get_message_body_content(message):
body = message.get("body") or {}
if not isinstance(body, dict):
return ""
return str(body.get("content") or "").strip()
def log_openai_messages(messages, transport=""):
for message in messages or []:
sender_info = message.get("from", {}).get("emailAddress", {}) or {}
sender = str(sender_info.get("address") or "").strip()
if not is_openai_sender(sender):
continue
sender_name = str(sender_info.get("name") or "").strip()
mailbox = str(message.get("mailbox") or "").strip() or "INBOX"
subject = str(message.get("subject") or "").strip()
transport_label = str(transport or "").strip() or "unknown"
base = (
f"transport={transport_label} mailbox={mailbox} sender={sender} "
f"senderName={sender_name or '-'} subject={subject}"
)
log_info(f"openai mail received {base}")
body_content = get_message_body_content(message)
if body_content:
log_info(f"openai mail full body start {base}")
print(body_content, flush=True)
log_info("openai mail full body end")
continue
preview = str(message.get("bodyPreview") or "").strip()
if preview:
log_info(f"openai mail preview {base} preview={compact_text(preview, 1000)}")
def get_proxy_debug_context(): def get_proxy_debug_context():
names = ["all_proxy", "http_proxy", "https_proxy", "ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY"] names = ["all_proxy", "http_proxy", "https_proxy", "ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY"]
parts = [] parts = []
@@ -481,6 +523,9 @@ def normalize_message(message_id, raw_bytes, mailbox):
} }
}, },
"bodyPreview": body[:500], "bodyPreview": body[:500],
"body": {
"content": body,
},
"receivedDateTime": to_iso_string(timestamp_ms), "receivedDateTime": to_iso_string(timestamp_ms),
"receivedTimestamp": timestamp_ms, "receivedTimestamp": timestamp_ms,
} }
@@ -680,6 +725,7 @@ def collect_messages(email_addr, client_id, refresh_token, mailboxes, top):
try: try:
log_info(f"message collection start transport={transport_name}") log_info(f"message collection start transport={transport_name}")
result = collector(email_addr, client_id, refresh_token, mailboxes, top) result = collector(email_addr, client_id, refresh_token, mailboxes, top)
log_openai_messages(result.get("messages") or [], transport=transport_name)
log_info( log_info(
f"message collection success transport={transport_name} " f"message collection success transport={transport_name} "
f"tokenEndpoint={result['token_payload'].get('token_endpoint', '')}" f"tokenEndpoint={result['token_payload'].get('token_endpoint', '')}"
@@ -722,6 +768,10 @@ def select_latest_code(messages, sender_filters, subject_filters, exclude_codes,
preview = str(message.get("bodyPreview", "")) preview = str(message.get("bodyPreview", ""))
combined = " ".join([sender, subject.lower(), preview.lower()]) combined = " ".join([sender, subject.lower(), preview.lower()])
code = extract_code(" ".join([subject, preview, sender])) code = extract_code(" ".join([subject, preview, sender]))
if not code:
body_content = get_message_body_content(message)
if body_content:
code = extract_code(" ".join([subject, body_content, sender]))
if not code or code in excluded: if not code or code in excluded:
return None return None
+5
View File
@@ -425,6 +425,11 @@
<option value="52" selected>Thailand</option> <option value="52" selected>Thailand</option>
</select> </select>
</div> </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;"> <div class="data-row" id="row-hero-sms-api-key" style="display:none;">
<span class="data-label">接码 API</span> <span class="data-label">接码 API</span>
<div class="input-with-icon"> <div class="input-with-icon">
+34 -1
View File
@@ -296,8 +296,10 @@ const rowPhoneVerificationEnabled = document.getElementById('row-phone-verificat
const inputPhoneVerificationEnabled = document.getElementById('input-phone-verification-enabled'); const inputPhoneVerificationEnabled = document.getElementById('input-phone-verification-enabled');
const rowHeroSmsPlatform = document.getElementById('row-hero-sms-platform'); const rowHeroSmsPlatform = document.getElementById('row-hero-sms-platform');
const rowHeroSmsCountry = document.getElementById('row-hero-sms-country'); 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 rowHeroSmsApiKey = document.getElementById('row-hero-sms-api-key');
const inputHeroSmsApiKey = document.getElementById('input-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 selectHeroSmsCountry = document.getElementById('select-hero-sms-country');
const displayHeroSmsPlatform = document.getElementById('display-hero-sms-platform'); const displayHeroSmsPlatform = document.getElementById('display-hero-sms-platform');
const rowAccountRunHistoryHelperBaseUrl = document.getElementById('row-account-run-history-helper-base-url'); const rowAccountRunHistoryHelperBaseUrl = document.getElementById('row-account-run-history-helper-base-url');
@@ -2228,6 +2230,9 @@ function collectSettingsPayload() {
const heroSmsApiKeyValue = typeof inputHeroSmsApiKey !== 'undefined' && inputHeroSmsApiKey const heroSmsApiKeyValue = typeof inputHeroSmsApiKey !== 'undefined' && inputHeroSmsApiKey
? (inputHeroSmsApiKey.value || '') ? (inputHeroSmsApiKey.value || '')
: ''; : '';
const heroSmsMaxPriceValue = typeof inputHeroSmsMaxPrice !== 'undefined' && inputHeroSmsMaxPrice
? normalizeHeroSmsMaxPriceValue(inputHeroSmsMaxPrice.value)
: '';
const heroSmsCountry = typeof getSelectedHeroSmsCountryOption === 'function' const heroSmsCountry = typeof getSelectedHeroSmsCountryOption === 'function'
? getSelectedHeroSmsCountryOption() ? getSelectedHeroSmsCountryOption()
: { : {
@@ -2331,6 +2336,7 @@ function collectSettingsPayload() {
DEFAULT_VERIFICATION_RESEND_COUNT DEFAULT_VERIFICATION_RESEND_COUNT
), ),
heroSmsApiKey: heroSmsApiKeyValue, heroSmsApiKey: heroSmsApiKeyValue,
heroSmsMaxPrice: heroSmsMaxPriceValue,
heroSmsCountryId: heroSmsCountry.id, heroSmsCountryId: heroSmsCountry.id,
heroSmsCountryLabel: heroSmsCountry.label, heroSmsCountryLabel: heroSmsCountry.label,
}; };
@@ -2389,6 +2395,18 @@ function normalizeHeroSmsCountryLabel(value = '') {
return String(value || '').trim() || DEFAULT_HERO_SMS_COUNTRY_LABEL; 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() { function getSelectedHeroSmsCountryOption() {
if (!selectHeroSmsCountry) { if (!selectHeroSmsCountry) {
return { return {
@@ -2609,7 +2627,7 @@ function updateAccountRunHistorySettingsUI() {
function updatePhoneVerificationSettingsUI() { function updatePhoneVerificationSettingsUI() {
const enabled = Boolean(inputPhoneVerificationEnabled?.checked); const enabled = Boolean(inputPhoneVerificationEnabled?.checked);
[rowHeroSmsPlatform, rowHeroSmsCountry, rowHeroSmsApiKey].forEach((row) => { [rowHeroSmsPlatform, rowHeroSmsCountry, rowHeroSmsMaxPrice, rowHeroSmsApiKey].forEach((row) => {
if (!row) { if (!row) {
return; return;
} }
@@ -3113,6 +3131,9 @@ function applySettingsState(state) {
if (inputHeroSmsApiKey) { if (inputHeroSmsApiKey) {
inputHeroSmsApiKey.value = state?.heroSmsApiKey || ''; inputHeroSmsApiKey.value = state?.heroSmsApiKey || '';
} }
if (inputHeroSmsMaxPrice) {
inputHeroSmsMaxPrice.value = normalizeHeroSmsMaxPriceValue(state?.heroSmsMaxPrice);
}
if (selectHeroSmsCountry) { if (selectHeroSmsCountry) {
const restoredCountryId = String(normalizeHeroSmsCountryId(state?.heroSmsCountryId)); const restoredCountryId = String(normalizeHeroSmsCountryId(state?.heroSmsCountryId));
if (Array.from(selectHeroSmsCountry.options).some((option) => option.value === restoredCountryId)) { if (Array.from(selectHeroSmsCountry.options).some((option) => option.value === restoredCountryId)) {
@@ -6655,6 +6676,15 @@ inputHeroSmsApiKey?.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { }); 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', () => { selectHeroSmsCountry?.addEventListener('change', () => {
updateHeroSmsPlatformDisplay(getSelectedHeroSmsCountryOption().label); updateHeroSmsPlatformDisplay(getSelectedHeroSmsCountryOption().label);
markSettingsDirty(true); markSettingsDirty(true);
@@ -7057,6 +7087,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.payload.heroSmsApiKey !== undefined && inputHeroSmsApiKey) { if (message.payload.heroSmsApiKey !== undefined && inputHeroSmsApiKey) {
inputHeroSmsApiKey.value = message.payload.heroSmsApiKey || ''; inputHeroSmsApiKey.value = message.payload.heroSmsApiKey || '';
} }
if (message.payload.heroSmsMaxPrice !== undefined && inputHeroSmsMaxPrice) {
inputHeroSmsMaxPrice.value = normalizeHeroSmsMaxPriceValue(message.payload.heroSmsMaxPrice);
}
if (message.payload.phoneVerificationEnabled !== undefined && inputPhoneVerificationEnabled) { if (message.payload.phoneVerificationEnabled !== undefined && inputPhoneVerificationEnabled) {
inputPhoneVerificationEnabled.checked = Boolean(message.payload.phoneVerificationEnabled); inputPhoneVerificationEnabled.checked = Boolean(message.payload.phoneVerificationEnabled);
updatePhoneVerificationSettingsUI(); updatePhoneVerificationSettingsUI();
@@ -114,6 +114,12 @@ let currentState = {
inbucketMailbox: '', inbucketMailbox: '',
cloudflareDomain: '', cloudflareDomain: '',
cloudflareDomains: [], cloudflareDomains: [],
reusablePhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 1,
maxUses: 3,
},
tabRegistry: {}, tabRegistry: {},
sourceLastUrls: {}, sourceLastUrls: {},
}; };
@@ -329,6 +335,16 @@ return {
assert.strictEqual(snapshot.currentState.autoRunSessionId, 0, 'session id should be cleared after completion'); assert.strictEqual(snapshot.currentState.autoRunSessionId, 0, 'session id should be cleared after completion');
assert.strictEqual(snapshot.currentState.gmailBaseEmail, 'demo@gmail.com', 'gmail base email should survive fresh-attempt reset'); assert.strictEqual(snapshot.currentState.gmailBaseEmail, 'demo@gmail.com', 'gmail base email should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.mail2925BaseEmail, 'demo@2925.com', '2925 base email should survive fresh-attempt reset'); assert.strictEqual(snapshot.currentState.mail2925BaseEmail, 'demo@2925.com', '2925 base email should survive fresh-attempt reset');
assert.deepStrictEqual(
snapshot.currentState.reusablePhoneActivation,
{
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 1,
maxUses: 3,
},
'reusable phone activation should survive fresh-attempt reset'
);
console.log('auto-run fresh attempt reset tests passed'); console.log('auto-run fresh attempt reset tests passed');
})().catch((error) => { })().catch((error) => {
+1
View File
@@ -713,6 +713,7 @@ function broadcastDataUpdate() {}
function isLocalhostOAuthCallbackUrl() { function isLocalhostOAuthCallbackUrl() {
return true; return true;
} }
async function finalizePhoneActivationAfterSuccessfulFlow() {}
async function finalizeIcloudAliasAfterSuccessfulFlow() {} async function finalizeIcloudAliasAfterSuccessfulFlow() {}
${bundle} ${bundle}
@@ -12,6 +12,7 @@ function createRouter(overrides = {}) {
stepStatuses: [], stepStatuses: [],
emailStates: [], emailStates: [],
finalizePayloads: [], finalizePayloads: [],
phoneFinalizations: [],
notifyCompletions: [], notifyCompletions: [],
notifyErrors: [], notifyErrors: [],
securityBlocks: [], securityBlocks: [],
@@ -49,10 +50,13 @@ function createRouter(overrides = {}) {
executeStepViaCompletionSignal: async () => {}, executeStepViaCompletionSignal: async () => {},
exportSettingsBundle: async () => ({}), exportSettingsBundle: async () => ({}),
fetchGeneratedEmail: async () => '', fetchGeneratedEmail: async () => '',
finalizePhoneActivationAfterSuccessfulFlow: overrides.finalizePhoneActivationAfterSuccessfulFlow || (async (state) => {
events.phoneFinalizations.push(state);
}),
finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => { finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => {
events.finalizePayloads.push(payload); events.finalizePayloads.push(payload);
}), }),
finalizeIcloudAliasAfterSuccessfulFlow: async () => {}, finalizeIcloudAliasAfterSuccessfulFlow: overrides.finalizeIcloudAliasAfterSuccessfulFlow || (async () => {}),
findHotmailAccount: async () => null, findHotmailAccount: async () => null,
flushCommand: async () => {}, flushCommand: async () => {},
getCurrentLuckmailPurchase: () => null, getCurrentLuckmailPurchase: () => null,
@@ -262,6 +266,68 @@ test('message router finalizes step 3 before marking it completed', async () =>
assert.deepStrictEqual(response, { ok: true }); assert.deepStrictEqual(response, { ok: true });
}); });
test('message router finalizes pending phone activation on platform verify success', async () => {
const state = {
stepStatuses: { 10: 'pending' },
reusablePhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
pendingPhoneActivationConfirmation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
};
const { router, events } = createRouter({
state,
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'platform-verify' : '' }),
});
await router.handleStepData(10, {
localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
});
assert.deepStrictEqual(events.phoneFinalizations, [state]);
});
test('message router does not finalize pending phone activation when icloud finalization fails', async () => {
const state = {
stepStatuses: { 10: 'pending' },
reusablePhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
pendingPhoneActivationConfirmation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
};
const { router, events } = createRouter({
state,
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'platform-verify' : '' }),
finalizeIcloudAliasAfterSuccessfulFlow: async () => {
throw new Error('icloud finalize failed');
},
});
await assert.rejects(
() => router.handleStepData(10, {
localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
}),
/icloud finalize failed/
);
assert.deepStrictEqual(events.phoneFinalizations, []);
});
test('message router marks step 3 failed when post-submit finalize fails', async () => { test('message router marks step 3 failed when post-submit finalize fails', async () => {
const { router, events } = createRouter({ const { router, events } = createRouter({
finalizeStep3Completion: async () => { finalizeStep3Completion: async () => {
+72
View File
@@ -310,6 +310,78 @@ test('step 8 retries in-place when polling fails but auth page still stays on ve
assert.equal(events.ensureCalls >= 3, true); assert.equal(events.ensureCalls >= 3, true);
}); });
test('step 8 keeps resend cooldown timestamp across in-place retries to avoid repeated resend storms', async () => {
const events = {
resolveCalls: 0,
resolveLastResendAts: [],
sleepMs: [],
};
const realDateNow = Date.now;
Date.now = () => 230000;
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: 'user@example.com' }),
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async () => 9000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 9000),
getMailConfig: () => ({
provider: 'qq',
label: 'QQ 邮箱',
source: 'mail-qq',
url: 'https://mail.qq.com',
navigateOnReuse: false,
}),
getState: async () => ({ email: 'user@example.com', password: 'secret', loginVerificationRequestedAt: null }),
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: (error) => /页面通信异常|did not respond/i.test(String(error?.message || error || '')),
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveVerificationStep: async (_step, _state, _mail, options) => {
events.resolveCalls += 1;
events.resolveLastResendAts.push(Number(options?.lastResendAt) || 0);
if (events.resolveCalls === 1) {
await options.onResendRequestedAt(222000);
throw new Error('步骤 8:页面通信异常 did not respond in 1s');
}
},
reuseOrCreateTab: async () => {},
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
sleepWithStop: async (ms) => {
events.sleepMs.push(ms);
},
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
try {
await executor.executeStep8({
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
});
} finally {
Date.now = realDateNow;
}
assert.equal(events.resolveCalls, 2);
assert.deepStrictEqual(events.resolveLastResendAts, [0, 222000]);
assert.equal(events.sleepMs.length >= 1, true);
assert.equal(events.sleepMs[0], 3000);
});
test('step 8 completes when polling fails but recovery probe shows oauth consent page', async () => { test('step 8 completes when polling fails but recovery probe shows oauth consent page', async () => {
const events = { const events = {
ensureCalls: 0, ensureCalls: 0,
+103 -1
View File
@@ -18,6 +18,107 @@ hotmail_helper = load_hotmail_helper()
class HotmailHelperLoggingTest(unittest.TestCase): class HotmailHelperLoggingTest(unittest.TestCase):
def test_select_latest_code_can_use_full_body_when_preview_is_truncated(self):
css_prefix = (
'Your temporary ChatGPT verification code '
'@font-face { font-family: "Söhne"; src: url(https://cdn.openai.com/common/fonts/soehne/soehne-buch.woff2) format("woff2"); } '
'.ExternalClass { width: 100%; } '
'#bodyTable { width: 560px; } '
'body { min-width: 100% !important; } '
) * 8
full_body = (
css_prefix
+ 'Enter this temporary verification code to continue: 272964 '
+ 'Please ignore this email if this was not you.'
)
message = {
"id": "imap-1",
"mailbox": "INBOX",
"subject": "Your temporary ChatGPT verification code",
"from": {
"emailAddress": {
"address": "otp@tm1.openai.com",
"name": "OpenAI",
}
},
"bodyPreview": full_body[:500],
"body": {
"content": full_body,
},
"receivedTimestamp": 200,
}
result = hotmail_helper.select_latest_code(
[message],
['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
[],
0,
)
self.assertEqual(result["code"], "272964")
self.assertEqual(result["message"]["id"], "imap-1")
def test_log_openai_messages_logs_full_body_when_available(self):
messages = [{
"mailbox": "INBOX",
"subject": "Your verification code",
"from": {
"emailAddress": {
"address": "account-security@openai.com",
"name": "OpenAI",
}
},
"bodyPreview": "Use 123456 to continue.",
"body": {
"content": "Hello there\nUse 123456 to continue.",
},
}]
output = io.StringIO()
with redirect_stdout(output):
hotmail_helper.log_openai_messages(messages, transport="imap")
rendered = output.getvalue()
self.assertIn(
"[HotmailHelper] openai mail received transport=imap mailbox=INBOX sender=account-security@openai.com senderName=OpenAI subject=Your verification code",
rendered,
)
self.assertIn(
"[HotmailHelper] openai mail full body start transport=imap mailbox=INBOX sender=account-security@openai.com senderName=OpenAI subject=Your verification code",
rendered,
)
self.assertIn("Hello there\nUse 123456 to continue.", rendered)
self.assertIn("[HotmailHelper] openai mail full body end", rendered)
def test_log_openai_messages_falls_back_to_preview_without_full_body(self):
messages = [{
"mailbox": "Junk",
"subject": "Verify your sign in",
"from": {
"emailAddress": {
"address": "noreply@tm.openai.com",
"name": "ChatGPT",
}
},
"bodyPreview": "Use 654321 to continue.",
}]
output = io.StringIO()
with redirect_stdout(output):
hotmail_helper.log_openai_messages(messages, transport="graph")
rendered = output.getvalue()
self.assertIn(
"[HotmailHelper] openai mail received transport=graph mailbox=Junk sender=noreply@tm.openai.com senderName=ChatGPT subject=Verify your sign in",
rendered,
)
self.assertIn(
"[HotmailHelper] openai mail preview transport=graph mailbox=Junk sender=noreply@tm.openai.com senderName=ChatGPT subject=Verify your sign in preview=Use 654321 to continue.",
rendered,
)
self.assertNotIn("openai mail full body start", rendered)
def test_refresh_access_token_logs_invalid_grant_and_direct_connection_refused_separately(self): def test_refresh_access_token_logs_invalid_grant_and_direct_connection_refused_separately(self):
failures = [ failures = [
{ {
@@ -38,7 +139,8 @@ class HotmailHelperLoggingTest(unittest.TestCase):
}, },
] ]
with mock.patch.object(hotmail_helper, "try_refresh_access_token", side_effect=failures): with mock.patch.object(hotmail_helper, "try_refresh_access_token", side_effect=failures), \
mock.patch.object(hotmail_helper, "get_proxy_debug_context", return_value="direct"):
output = io.StringIO() output = io.StringIO()
with redirect_stdout(output): with redirect_stdout(output):
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
+346 -242
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 requests = [];
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
@@ -40,26 +40,22 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
fetchImpl: async (url) => { fetchImpl: async (url) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push(parsedUrl); requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
return { return {
ok: true, ok: true,
text: async () => 'ACCESS_NUMBER:123456:66959916439', text: async () => 'ACCESS_NUMBER:123456:66959916439',
}; };
}, },
getState: async () => ({ heroSmsApiKey: 'demo-key' }), getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08' }),
sendToContentScriptResilient: async () => ({}), sendToContentScriptResilient: async () => ({}),
setState: async () => {}, setState: async () => {},
sleepWithStop: async () => {}, sleepWithStop: async () => {},
throwIfStopped: () => {}, throwIfStopped: () => {},
}); });
const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' }); const activation = await helpers.requestPhoneActivation({
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
});
assert.deepStrictEqual(activation, { assert.deepStrictEqual(activation, {
activationId: '123456', activationId: '123456',
@@ -70,111 +66,80 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
successfulUses: 0, successfulUses: 0,
maxUses: 3, maxUses: 3,
}); });
assert.equal(requests.length, 2); assert.equal(requests.length, 1);
assert.equal(requests[0].searchParams.get('action'), 'getPrices'); assert.equal(requests[0].searchParams.get('action'), 'getNumber');
assert.equal(requests[0].searchParams.get('service'), 'dr'); assert.equal(requests[0].searchParams.get('service'), 'dr');
assert.equal(requests[0].searchParams.get('country'), '52'); assert.equal(requests[0].searchParams.get('country'), '52');
assert.equal(requests[0].searchParams.get('api_key'), 'demo-key'); assert.equal(requests[0].searchParams.get('api_key'), 'demo-key');
assert.equal(requests[1].searchParams.get('action'), 'getNumber'); assert.equal(requests[0].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08'); assert.equal(requests[0].searchParams.get('fixedPrice'), 'true');
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');
}); });
test('phone verification helper retries HeroSMS getPrices until it receives a usable lowest price', async () => { test('phone verification helper requires manual HeroSMS maxPrice', async () => {
const requests = [];
let getPricesAttempt = 0;
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => { fetchImpl: async () => {
const parsedUrl = new URL(url); throw new Error('should not request HeroSMS without maxPrice');
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}`);
}, },
getState: async () => ({ heroSmsApiKey: 'demo-key' }), getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '' }),
sendToContentScriptResilient: async () => ({}), sendToContentScriptResilient: async () => ({}),
setState: async () => {}, setState: async () => {},
sleepWithStop: async () => {}, sleepWithStop: async () => {},
throwIfStopped: () => {}, throwIfStopped: () => {},
}); });
await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' }); await assert.rejects(
() => helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '' }),
assert.equal(requests.length, 4); /HeroSMS maxPrice is missing/i
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 () => { test('phone verification helper still clears existing activation when maxPrice is missing', async () => {
const requests = []; const requests = [];
let getPricesAttempt = 0; let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '',
currentPhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
successfulUses: 0,
maxUses: 3,
},
reusablePhoneActivation: null,
pendingPhoneActivationConfirmation: null,
};
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => { fetchImpl: async (url) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push(parsedUrl); requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action'); return {
if (action === 'getPrices') { ok: true,
getPricesAttempt += 1; text: async () => 'ACCESS_CANCEL',
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' }), getState: async () => currentState,
sendToContentScriptResilient: async () => ({}), sendToContentScriptResilient: async () => ({}),
setState: async () => {}, setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {}, sleepWithStop: async () => {},
throwIfStopped: () => {}, throwIfStopped: () => {},
}); });
await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' }); await assert.rejects(
() => helpers.completePhoneVerificationFlow(1, { addPhonePage: true }),
/HeroSMS maxPrice is missing/i
);
assert.equal(requests.length, 4); assert.equal(requests.length, 1);
assert.equal(requests[0].searchParams.get('action'), 'getPrices'); assert.equal(requests[0].searchParams.get('action'), 'setStatus');
assert.equal(requests[1].searchParams.get('action'), 'getPrices'); assert.equal(requests[0].searchParams.get('id'), '123456');
assert.equal(requests[2].searchParams.get('action'), 'getPrices'); assert.equal(requests[0].searchParams.get('status'), '8');
assert.equal(requests[2].searchParams.get('service'), 'dr'); assert.equal(requests[0].searchParams.has('maxPrice'), false);
assert.equal(requests[2].searchParams.get('country'), '52'); assert.equal(currentState.currentPhoneActivation, null);
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);
}); });
test('phone verification helper retries with HeroSMS getNumberV2 when getNumber reports NO_NUMBERS', async () => { test('phone verification helper retries with HeroSMS getNumberV2 when getNumber reports NO_NUMBERS', async () => {
@@ -186,12 +151,6 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push(parsedUrl); requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action'); const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload({ country: '16' }),
};
}
if (action === 'getNumber') { if (action === 'getNumber') {
return { return {
ok: true, ok: true,
@@ -209,7 +168,7 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
} }
throw new Error(`Unexpected HeroSMS action: ${action}`); 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 () => ({}), sendToContentScriptResilient: async () => ({}),
setState: async () => {}, setState: async () => {},
sleepWithStop: async () => {}, sleepWithStop: async () => {},
@@ -218,6 +177,7 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
const activation = await helpers.requestPhoneActivation({ const activation = await helpers.requestPhoneActivation({
heroSmsApiKey: 'demo-key', heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16, heroSmsCountryId: 16,
}); });
@@ -231,17 +191,15 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
maxUses: 3, maxUses: 3,
statusAction: 'getStatusV2', statusAction: 'getStatusV2',
}); });
assert.equal(requests.length, 3); assert.equal(requests.length, 2);
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[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('country'), '16');
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08'); assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true'); 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 () => { test('phone verification helper uses HeroSMS getStatusV2 after acquiring a number via getNumberV2', async () => {
@@ -249,6 +207,7 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
const stateUpdates = []; const stateUpdates = [];
let currentState = { let currentState = {
heroSmsApiKey: 'demo-key', heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16, heroSmsCountryId: 16,
heroSmsCountryLabel: 'United Kingdom', heroSmsCountryLabel: 'United Kingdom',
verificationResendCount: 0, verificationResendCount: 0,
@@ -264,12 +223,6 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push(parsedUrl); requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action'); const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload({ country: '16' }),
};
}
if (action === 'getNumber') { if (action === 'getNumber') {
return { return {
ok: true, ok: true,
@@ -361,7 +314,19 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
provider: 'hero-sms', provider: 'hero-sms',
serviceCode: 'dr', serviceCode: 'dr',
countryId: 16, countryId: 16,
successfulUses: 1, successfulUses: 0,
maxUses: 3,
statusAction: 'getStatusV2',
},
},
{
pendingPhoneActivationConfirmation: {
activationId: '654321',
phoneNumber: '447911123456',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 16,
successfulUses: 0,
maxUses: 3, maxUses: 3,
statusAction: 'getStatusV2', statusAction: 'getStatusV2',
}, },
@@ -372,7 +337,6 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
]); ]);
const actions = requests.map((url) => url.searchParams.get('action')); const actions = requests.map((url) => url.searchParams.get('action'));
assert.deepStrictEqual(actions, [ assert.deepStrictEqual(actions, [
'getPrices',
'getNumber', 'getNumber',
'getNumberV2', 'getNumberV2',
'getStatusV2', 'getStatusV2',
@@ -381,117 +345,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 () => { test('phone verification helper keeps the user-provided maxPrice and surfaces HeroSMS price errors', async () => {
const requests = [];
let getNumberAttempt = 0;
const helpers = api.createPhoneVerificationHelpers({ const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {}, addLog: async () => {},
ensureStep8SignupPageReady: async () => {}, ensureStep8SignupPageReady: async () => {},
fetchImpl: async (url) => { fetchImpl: async (url) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action'); const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
if (action === 'getNumber') { if (action === 'getNumber') {
getNumberAttempt += 1; return {
return getNumberAttempt === 1 ok: false,
? { text: async () => 'WRONG_MAX_PRICE:0.09',
ok: false, };
text: async () => 'WRONG_MAX_PRICE:0.09',
}
: {
ok: true,
text: async () => 'ACCESS_NUMBER:123456:66959916439',
};
} }
throw new Error(`Unexpected HeroSMS action: ${action}`); throw new Error(`Unexpected HeroSMS action: ${action}`);
}, },
getState: async () => ({ heroSmsApiKey: 'demo-key' }), getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08' }),
sendToContentScriptResilient: async () => ({}), sendToContentScriptResilient: async () => ({}),
setState: async () => {}, setState: async () => {},
sleepWithStop: async () => {}, sleepWithStop: async () => {},
throwIfStopped: () => {}, throwIfStopped: () => {},
}); });
const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' }); await assert.rejects(
() => helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08' }),
assert.deepStrictEqual(activation, { /WRONG_MAX_PRICE:0.09/
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);
}); });
test('phone verification helper completes add-phone flow, clears current activation, and stores reusable number state', async () => { test('phone verification helper completes add-phone flow, clears current activation, and stores reusable number state', async () => {
@@ -499,6 +378,7 @@ test('phone verification helper completes add-phone flow, clears current activat
const stateUpdates = []; const stateUpdates = [];
let currentState = { let currentState = {
heroSmsApiKey: 'demo-key', heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
verificationResendCount: 1, verificationResendCount: 1,
currentPhoneActivation: null, currentPhoneActivation: null,
reusablePhoneActivation: null, reusablePhoneActivation: null,
@@ -511,12 +391,6 @@ test('phone verification helper completes add-phone flow, clears current activat
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push(parsedUrl); requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action'); const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
if (action === 'getNumber') { if (action === 'getNumber') {
return { return {
ok: true, ok: true,
@@ -593,7 +467,18 @@ test('phone verification helper completes add-phone flow, clears current activat
provider: 'hero-sms', provider: 'hero-sms',
serviceCode: 'dr', serviceCode: 'dr',
countryId: 52, countryId: 52,
successfulUses: 1, successfulUses: 0,
maxUses: 3,
},
},
{
pendingPhoneActivationConfirmation: {
activationId: '123456',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3, maxUses: 3,
}, },
}, },
@@ -603,7 +488,87 @@ test('phone verification helper completes add-phone flow, clears current activat
]); ]);
const actions = requests.map((url) => url.searchParams.get('action')); 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);
assert.deepStrictEqual(currentState.pendingPhoneActivationConfirmation, 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 () => { test('phone verification helper uses the configured HeroSMS country for both number acquisition and add-phone submission', async () => {
@@ -611,6 +576,7 @@ test('phone verification helper uses the configured HeroSMS country for both num
const submittedPayloads = []; const submittedPayloads = [];
let currentState = { let currentState = {
heroSmsApiKey: 'demo-key', heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16, heroSmsCountryId: 16,
heroSmsCountryLabel: 'United Kingdom', heroSmsCountryLabel: 'United Kingdom',
verificationResendCount: 0, verificationResendCount: 0,
@@ -625,12 +591,6 @@ test('phone verification helper uses the configured HeroSMS country for both num
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
requests.push(parsedUrl); requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action'); const action = parsedUrl.searchParams.get('action');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload({ country: '16' }),
};
}
if (action === 'getNumber') { if (action === 'getNumber') {
return { return {
ok: true, ok: true,
@@ -688,12 +648,10 @@ test('phone verification helper uses the configured HeroSMS country for both num
consentReady: true, consentReady: true,
url: 'https://auth.openai.com/authorize', 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[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[1].searchParams.get('country'), '16'); assert.equal(requests[0].searchParams.get('fixedPrice'), 'true');
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
assert.deepStrictEqual(submittedPayloads, [{ assert.deepStrictEqual(submittedPayloads, [{
phoneNumber: '447911123456', phoneNumber: '447911123456',
countryId: 16, countryId: 16,
@@ -704,8 +662,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 () => { test('phone verification helper throws a step-7 restart error after 60 seconds plus one resend window without SMS', async () => {
const requests = []; const requests = [];
const messages = []; const messages = [];
const stateUpdates = [];
let currentState = { let currentState = {
heroSmsApiKey: 'demo-key', heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
verificationResendCount: 0, verificationResendCount: 0,
currentPhoneActivation: null, currentPhoneActivation: null,
reusablePhoneActivation: null, reusablePhoneActivation: null,
@@ -725,13 +685,6 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
const action = parsedUrl.searchParams.get('action'); const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id'); const id = parsedUrl.searchParams.get('id');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
if (action === 'getNumber') { if (action === 'getNumber') {
return { return {
ok: true, ok: true,
@@ -775,6 +728,7 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
throw new Error(`Unexpected content-script message: ${message.type}`); throw new Error(`Unexpected content-script message: ${message.type}`);
}, },
setState: async (updates) => { setState: async (updates) => {
stateUpdates.push(updates);
currentState = { ...currentState, ...updates }; currentState = { ...currentState, ...updates };
}, },
sleepWithStop: async () => { sleepWithStop: async () => {
@@ -799,7 +753,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') || ''}`); const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
assert.deepStrictEqual(actions, [ assert.deepStrictEqual(actions, [
'getPrices:',
'getNumber:', 'getNumber:',
'getStatus:123456', 'getStatus:123456',
'setStatus:123456', 'setStatus:123456',
@@ -807,6 +760,11 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
'setStatus:123456', 'setStatus:123456',
]); ]);
assert.equal(currentState.currentPhoneActivation, null); 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 { } finally {
Date.now = realDateNow; Date.now = realDateNow;
} }
@@ -817,6 +775,7 @@ test('phone verification helper replaces the number when code submission returns
const messages = []; const messages = [];
let currentState = { let currentState = {
heroSmsApiKey: 'demo-key', heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
verificationResendCount: 1, verificationResendCount: 1,
currentPhoneActivation: null, currentPhoneActivation: null,
reusablePhoneActivation: null, reusablePhoneActivation: null,
@@ -838,13 +797,6 @@ test('phone verification helper replaces the number when code submission returns
const action = parsedUrl.searchParams.get('action'); const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id'); const id = parsedUrl.searchParams.get('id');
if (action === 'getPrices') {
return {
ok: true,
text: async () => buildHeroSmsPricesPayload(),
};
}
if (action === 'getNumber') { if (action === 'getNumber') {
const nextNumber = numbers[numberIndex]; const nextNumber = numbers[numberIndex];
numberIndex += 1; numberIndex += 1;
@@ -925,11 +877,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') || ''}`); const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
assert.deepStrictEqual(actions, [ assert.deepStrictEqual(actions, [
'getPrices:',
'getNumber:', 'getNumber:',
'getStatus:111111', 'getStatus:111111',
'setStatus:111111', 'setStatus:111111',
'getPrices:',
'getNumber:', 'getNumber:',
'getStatus:222222', 'getStatus:222222',
'setStatus:222222', 'setStatus:222222',
@@ -941,15 +891,25 @@ test('phone verification helper replaces the number when code submission returns
provider: 'hero-sms', provider: 'hero-sms',
serviceCode: 'dr', serviceCode: 'dr',
countryId: 52, countryId: 52,
successfulUses: 1, successfulUses: 0,
maxUses: 3,
});
assert.deepStrictEqual(currentState.pendingPhoneActivationConfirmation, {
activationId: '222222',
phoneNumber: '66950000002',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3, maxUses: 3,
}); });
}); });
test('phone verification helper reuses the same number up to three successful registrations', async () => { test('phone verification helper defers maxUses accounting for reused activations until the full flow succeeds', async () => {
const requests = []; const requests = [];
let currentState = { let currentState = {
heroSmsApiKey: 'demo-key', heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
verificationResendCount: 0, verificationResendCount: 0,
currentPhoneActivation: null, currentPhoneActivation: null,
reusablePhoneActivation: { reusablePhoneActivation: {
@@ -1031,13 +991,31 @@ test('phone verification helper reuses the same number up to three successful re
}); });
assert.equal(requests[0].searchParams.get('action'), 'reactivate'); assert.equal(requests[0].searchParams.get('action'), 'reactivate');
assert.equal(requests[0].searchParams.get('id'), '123456'); assert.equal(requests[0].searchParams.get('id'), '123456');
assert.deepStrictEqual(currentState.reusablePhoneActivation, null); assert.deepStrictEqual(currentState.reusablePhoneActivation, {
activationId: '222333',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 2,
maxUses: 3,
});
assert.deepStrictEqual(currentState.pendingPhoneActivationConfirmation, {
activationId: '222333',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 2,
maxUses: 3,
});
}); });
test('phone verification helper keeps maxUses behavior for reused V2 activations', async () => { test('phone verification helper defers maxUses accounting for reused V2 activations until the full flow succeeds', async () => {
const requests = []; const requests = [];
let currentState = { let currentState = {
heroSmsApiKey: 'demo-key', heroSmsApiKey: 'demo-key',
heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16, heroSmsCountryId: 16,
heroSmsCountryLabel: 'United Kingdom', heroSmsCountryLabel: 'United Kingdom',
verificationResendCount: 0, verificationResendCount: 0,
@@ -1122,5 +1100,131 @@ test('phone verification helper keeps maxUses behavior for reused V2 activations
}); });
const actions = requests.map((url) => url.searchParams.get('action')); const actions = requests.map((url) => url.searchParams.get('action'));
assert.deepStrictEqual(actions, ['reactivate', 'getStatusV2', 'setStatus']); assert.deepStrictEqual(actions, ['reactivate', 'getStatusV2', 'setStatus']);
assert.deepStrictEqual(currentState.reusablePhoneActivation, null); assert.deepStrictEqual(currentState.reusablePhoneActivation, {
activationId: '222333',
phoneNumber: '447911123456',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 16,
successfulUses: 2,
maxUses: 3,
statusAction: 'getStatusV2',
});
assert.deepStrictEqual(currentState.pendingPhoneActivationConfirmation, {
activationId: '222333',
phoneNumber: '447911123456',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 16,
successfulUses: 2,
maxUses: 3,
statusAction: 'getStatusV2',
});
});
test('phone verification helper finalizes pending phone activation confirmation after the full flow succeeds', async () => {
let currentState = {
reusablePhoneActivation: {
activationId: '123456',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
pendingPhoneActivationConfirmation: {
activationId: '123456',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 0,
maxUses: 3,
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async () => ({}),
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const committedActivation = await helpers.finalizePendingPhoneActivationConfirmation();
assert.deepStrictEqual(committedActivation, {
activationId: '123456',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 1,
maxUses: 3,
});
assert.deepStrictEqual(currentState.reusablePhoneActivation, {
activationId: '123456',
phoneNumber: '66959916439',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
successfulUses: 1,
maxUses: 3,
});
assert.equal(currentState.pendingPhoneActivationConfirmation, null);
});
test('phone verification helper clears reusable activation when final success exhausts maxUses', async () => {
let currentState = {
reusablePhoneActivation: {
activationId: '222333',
phoneNumber: '447911123456',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 16,
successfulUses: 2,
maxUses: 3,
statusAction: 'getStatusV2',
},
pendingPhoneActivationConfirmation: {
activationId: '222333',
phoneNumber: '447911123456',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 16,
successfulUses: 2,
maxUses: 3,
statusAction: 'getStatusV2',
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async () => ({}),
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const committedActivation = await helpers.finalizePendingPhoneActivationConfirmation();
assert.deepStrictEqual(committedActivation, {
activationId: '222333',
phoneNumber: '447911123456',
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 16,
successfulUses: 3,
maxUses: 3,
statusAction: 'getStatusV2',
});
assert.equal(currentState.reusablePhoneActivation, null);
assert.equal(currentState.pendingPhoneActivationConfirmation, null);
}); });
+2
View File
@@ -321,6 +321,7 @@ const inputVerificationResendCount = { value: '' };
const inputPhoneVerificationEnabled = { checked: false }; const inputPhoneVerificationEnabled = { checked: false };
const DEFAULT_PHONE_VERIFICATION_ENABLED = false; const DEFAULT_PHONE_VERIFICATION_ENABLED = false;
const inputHeroSmsApiKey = { value: '' }; const inputHeroSmsApiKey = { value: '' };
const inputHeroSmsMaxPrice = { value: '' };
const selectHeroSmsCountry = { value: '52', options: [{ value: '52' }] }; const selectHeroSmsCountry = { value: '52', options: [{ value: '52' }] };
const inputRunCount = { value: '' }; const inputRunCount = { value: '' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4; const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
@@ -347,6 +348,7 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; } function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function formatAutoStepDelayInputValue(value) { return value == null ? '' : String(value); } function formatAutoStepDelayInputValue(value) { return value == null ? '' : String(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; } function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
function normalizeHeroSmsMaxPriceValue(value) { return String(value ?? '').trim(); }
function normalizeHeroSmsCountryId() { return 52; } function normalizeHeroSmsCountryId() { return 52; }
function getSelectedHeroSmsCountryOption() { return { label: 'Thailand' }; } function getSelectedHeroSmsCountryOption() { return { label: 'Thailand' }; }
function updateHeroSmsPlatformDisplay() {} function updateHeroSmsPlatformDisplay() {}
@@ -63,6 +63,7 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
assert.match(html, /id="input-ip-proxy-enabled"/); assert.match(html, /id="input-ip-proxy-enabled"/);
assert.match(html, /id="row-hero-sms-platform"/); assert.match(html, /id="row-hero-sms-platform"/);
assert.match(html, /id="row-hero-sms-country"/); 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.match(html, /id="row-hero-sms-api-key"/);
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/); assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
}); });
@@ -101,6 +102,7 @@ test('updatePhoneVerificationSettingsUI toggles HeroSMS rows from the sms switch
const inputPhoneVerificationEnabled = { checked: false }; const inputPhoneVerificationEnabled = { checked: false };
const rowHeroSmsPlatform = { style: { display: 'none' } }; const rowHeroSmsPlatform = { style: { display: 'none' } };
const rowHeroSmsCountry = { style: { display: 'none' } }; const rowHeroSmsCountry = { style: { display: 'none' } };
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
const rowHeroSmsApiKey = { style: { display: 'none' } }; const rowHeroSmsApiKey = { style: { display: 'none' } };
${extractFunction('updatePhoneVerificationSettingsUI')} ${extractFunction('updatePhoneVerificationSettingsUI')}
@@ -109,6 +111,7 @@ return {
inputPhoneVerificationEnabled, inputPhoneVerificationEnabled,
rowHeroSmsPlatform, rowHeroSmsPlatform,
rowHeroSmsCountry, rowHeroSmsCountry,
rowHeroSmsMaxPrice,
rowHeroSmsApiKey, rowHeroSmsApiKey,
updatePhoneVerificationSettingsUI, updatePhoneVerificationSettingsUI,
}; };
@@ -117,12 +120,14 @@ return {
api.updatePhoneVerificationSettingsUI(); api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowHeroSmsPlatform.style.display, 'none'); assert.equal(api.rowHeroSmsPlatform.style.display, 'none');
assert.equal(api.rowHeroSmsCountry.style.display, 'none'); assert.equal(api.rowHeroSmsCountry.style.display, 'none');
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
assert.equal(api.rowHeroSmsApiKey.style.display, 'none'); assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
api.inputPhoneVerificationEnabled.checked = true; api.inputPhoneVerificationEnabled.checked = true;
api.updatePhoneVerificationSettingsUI(); api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowHeroSmsPlatform.style.display, ''); assert.equal(api.rowHeroSmsPlatform.style.display, '');
assert.equal(api.rowHeroSmsCountry.style.display, ''); assert.equal(api.rowHeroSmsCountry.style.display, '');
assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
assert.equal(api.rowHeroSmsApiKey.style.display, ''); assert.equal(api.rowHeroSmsApiKey.style.display, '');
}); });
@@ -174,6 +179,7 @@ const inputAutoStepDelaySeconds = { value: '' };
const inputPhoneVerificationEnabled = { checked: true }; const inputPhoneVerificationEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' }; const inputVerificationResendCount = { value: '4' };
const inputHeroSmsApiKey = { value: 'demo-key' }; const inputHeroSmsApiKey = { value: 'demo-key' };
const inputHeroSmsMaxPrice = { value: '0.08' };
const inputAccountRunHistoryHelperBaseUrl = { value: 'http://127.0.0.1:17373' }; const inputAccountRunHistoryHelperBaseUrl = { value: 'http://127.0.0.1:17373' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4; const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_HERO_SMS_COUNTRY_ID = 52; const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
@@ -202,6 +208,7 @@ function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Num
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; } function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${extractFunction('normalizeHeroSmsCountryId')} ${extractFunction('normalizeHeroSmsCountryId')}
${extractFunction('normalizeHeroSmsCountryLabel')} ${extractFunction('normalizeHeroSmsCountryLabel')}
${extractFunction('normalizeHeroSmsMaxPriceValue')}
${extractFunction('getSelectedHeroSmsCountryOption')} ${extractFunction('getSelectedHeroSmsCountryOption')}
${extractFunction('collectSettingsPayload')} ${extractFunction('collectSettingsPayload')}
return { collectSettingsPayload }; return { collectSettingsPayload };
@@ -213,6 +220,7 @@ return { collectSettingsPayload };
assert.equal(payload.accountRunHistoryTextEnabled, true); assert.equal(payload.accountRunHistoryTextEnabled, true);
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373'); assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(payload.heroSmsApiKey, 'demo-key'); assert.equal(payload.heroSmsApiKey, 'demo-key');
assert.equal(payload.heroSmsMaxPrice, '0.08');
assert.equal(payload.heroSmsCountryId, 52); assert.equal(payload.heroSmsCountryId, 52);
assert.equal(payload.heroSmsCountryLabel, 'Thailand'); assert.equal(payload.heroSmsCountryLabel, 'Thailand');
}); });
@@ -114,6 +114,7 @@ function broadcastDataUpdate() {}
async function addLog(message) { async function addLog(message) {
logMessages.push(message); logMessages.push(message);
} }
async function finalizePhoneActivationAfterSuccessfulFlow() {}
async function finalizeIcloudAliasAfterSuccessfulFlow() {} async function finalizeIcloudAliasAfterSuccessfulFlow() {}
function matchesSourceUrlFamily() { function matchesSourceUrlFamily() {
return false; return false;
+71
View File
@@ -1030,6 +1030,77 @@ test('verification flow waits during resend cooldown instead of tight-looping',
assert.ok(sleepCalls[0] >= 1000); assert.ok(sleepCalls[0] >= 1000);
}); });
test('verification flow notifies onResendRequestedAt when resend is triggered', async () => {
const resendRequestedAtCalls = [];
const stateUpdates = [];
let pollCalls = 0;
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'RESEND_VERIFICATION_CODE') {
return { resent: true };
}
return {};
},
sendToMailContentScriptResilient: async (_mail, message) => {
if (message.type !== 'POLL_EMAIL') {
return {};
}
pollCalls += 1;
return pollCalls === 1
? {}
: { code: '654321', emailTimestamp: 123 };
},
setState: async (payload) => {
stateUpdates.push(payload);
},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await helpers.resolveVerificationStep(
8,
{
email: 'user@example.com',
lastLoginCode: null,
},
{ provider: 'qq', label: 'QQ 邮箱' },
{
maxResendRequests: 1,
resendIntervalMs: 25000,
onResendRequestedAt: async (requestedAt) => {
resendRequestedAtCalls.push(Number(requestedAt) || 0);
},
}
);
assert.equal(resendRequestedAtCalls.length >= 1, true);
assert.equal(resendRequestedAtCalls[0] > 0, true);
assert.equal(
stateUpdates.some((payload) => Number(payload?.loginVerificationRequestedAt) > 0),
true
);
});
test('verification flow uses resilient signup-page transport when submitting verification code', async () => { test('verification flow uses resilient signup-page transport when submitting verification code', async () => {
const resilientCalls = []; const resilientCalls = [];