Add OAuth flow timeout toggle

This commit is contained in:
initiatione
2026-05-01 11:41:12 +08:00
parent be23c53269
commit 6ae4e0f5a0
13 changed files with 476 additions and 9 deletions
+17
View File
@@ -490,6 +490,7 @@ const PERSISTED_SETTING_DEFAULTS = {
currentPayPalAccountId: '',
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
oauthFlowTimeoutEnabled: true,
autoRunDelayEnabled: false,
autoRunDelayMinutes: 30,
autoStepDelaySeconds: null,
@@ -1466,6 +1467,7 @@ function normalizePersistentSettingValue(key, value) {
case 'plusPaymentMethod':
return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal';
case 'autoRunSkipFailures':
case 'oauthFlowTimeoutEnabled':
case 'autoRunDelayEnabled':
case 'phoneVerificationEnabled':
case 'plusModeEnabled':
@@ -8915,6 +8917,7 @@ const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.crea
DEFAULT_PHONE_CODE_POLL_INTERVAL_SECONDS,
DEFAULT_PHONE_CODE_POLL_ROUNDS,
ensureStep8SignupPageReady,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
getState,
HERO_SMS_COUNTRY_ID,
@@ -9670,6 +9673,16 @@ function normalizeOAuthFlowSourceUrl(value) {
async function startOAuthFlowTimeoutWindow(options = {}) {
const step = Number(options.step) || 7;
const state = options.state || await getState();
if (state?.oauthFlowTimeoutEnabled === false) {
await setState({
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
});
await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,授权后链总超时已关闭,仅保留各步骤本地等待超时。`, 'info');
return null;
}
const deadlineAt = Date.now() + OAUTH_FLOW_TIMEOUT_MS;
await setState({
oauthFlowDeadlineAt: deadlineAt,
@@ -9683,6 +9696,10 @@ async function getOAuthFlowRemainingMs(options = {}) {
const step = Number(options.step) || 7;
const actionLabel = String(options.actionLabel || '后续授权流程').trim() || '后续授权流程';
const state = options.state || await getState();
if (state?.oauthFlowTimeoutEnabled === false) {
return null;
}
const deadlineAt = normalizeOAuthFlowDeadlineAt(state?.oauthFlowDeadlineAt);
const deadlineSourceUrl = normalizeOAuthFlowSourceUrl(state?.oauthFlowDeadlineSourceUrl);
const currentOauthUrl = normalizeOAuthFlowSourceUrl(options.oauthUrl !== undefined ? options.oauthUrl : state?.oauthUrl);
+6
View File
@@ -705,10 +705,16 @@
? Boolean(updates.plusModeEnabled)
: Boolean(currentState?.plusModeEnabled);
const stepModeChanged = modeChanged || (nextPlusModeEnabled && plusPaymentChanged);
const oauthFlowTimeoutDisabled = Object.prototype.hasOwnProperty.call(updates, 'oauthFlowTimeoutEnabled')
&& updates.oauthFlowTimeoutEnabled === false;
await setPersistentSettings(updates);
const stateUpdates = {
...updates,
...sessionUpdates,
...(oauthFlowTimeoutDisabled ? {
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
} : {}),
};
if (stepModeChanged && typeof getStepIdsForState === 'function') {
const nextStateForSteps = { ...currentState, ...stateUpdates };
+46 -8
View File
@@ -9,6 +9,7 @@
clickWithDebugger,
completeStepFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
@@ -34,6 +35,9 @@
setStep8TabUpdatedListener,
} = deps;
const LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS = 240000;
const CALLBACK_TIMEOUT_CHECK_INTERVAL_MS = 1000;
function getVisibleStep(state, fallback = 9) {
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
return visibleStep > 0 ? visibleStep : fallback;
@@ -54,17 +58,17 @@
await addLog(`步骤 ${visibleStep}:正在监听 localhost 回调地址...`);
let callbackTimeoutMs = 240000;
let callbackTimeoutMs = LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS;
let timeoutRecoveryAttempted = false;
while (true) {
try {
callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(240000, {
? await getOAuthFlowStepTimeoutMs(LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS, {
step: visibleStep,
actionLabel: 'OAuth localhost 回调',
oauthUrl: activeState?.oauthUrl || '',
})
: 240000;
: LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS;
break;
} catch (error) {
if (timeoutRecoveryAttempted || typeof recoverOAuthLocalhostTimeout !== 'function') {
@@ -86,8 +90,14 @@
return new Promise((resolve, reject) => {
let resolved = false;
let signupTabId = null;
const callbackWaitStartedAt = Date.now();
let timeoutCheckTimer = null;
const cleanupListener = () => {
if (timeoutCheckTimer) {
clearTimeout(timeoutCheckTimer);
timeoutCheckTimer = null;
}
cleanupStep8NavigationListeners();
setStep8PendingReject(null);
};
@@ -95,7 +105,6 @@
const rejectStep9 = (error) => {
if (resolved) return;
resolved = true;
clearTimeout(timeout);
cleanupListener();
reject(error);
};
@@ -105,7 +114,6 @@
resolved = true;
cleanupListener();
clearTimeout(timeout);
addLog(`步骤 ${visibleStep}:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
return completeStepFromBackground(visibleStep, { localhostUrl: callbackUrl });
@@ -116,9 +124,39 @@
});
};
const timeout = setTimeout(() => {
rejectStep9(new Error(`${Math.round(callbackTimeoutMs / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
}, callbackTimeoutMs);
const checkCallbackTimeout = async () => {
if (resolved) {
return;
}
const elapsedMs = Date.now() - callbackWaitStartedAt;
if (elapsedMs >= LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS) {
rejectStep9(new Error(`${Math.round(LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
return;
}
if (typeof getOAuthFlowRemainingMs === 'function') {
try {
await getOAuthFlowRemainingMs({
step: visibleStep,
actionLabel: 'OAuth localhost 回调',
oauthUrl: activeState?.oauthUrl || '',
});
} catch (error) {
rejectStep9(error);
return;
}
} else if (elapsedMs >= callbackTimeoutMs) {
rejectStep9(new Error(`${Math.round(callbackTimeoutMs / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
return;
}
timeoutCheckTimer = setTimeout(checkCallbackTimeout, CALLBACK_TIMEOUT_CHECK_INTERVAL_MS);
};
timeoutCheckTimer = setTimeout(
checkCallbackTimeout,
Math.min(CALLBACK_TIMEOUT_CHECK_INTERVAL_MS, Math.max(1, callbackTimeoutMs))
);
setStep8PendingReject((error) => {
rejectStep9(error);
+14
View File
@@ -394,6 +394,20 @@
</div>
</div>
</div>
<div class="data-row" id="row-oauth-flow-timeout">
<span class="data-label">授权总超时</span>
<div class="data-inline">
<label class="toggle-switch" for="input-oauth-flow-timeout-enabled"
title="关闭后不再用 5 分钟总预算重启授权链,但页面、点击、回调等本地等待超时仍会生效">
<input type="checkbox" id="input-oauth-flow-timeout-enabled" checked />
<span class="toggle-switch-track" aria-hidden="true">
<span class="toggle-switch-thumb"></span>
</span>
<span>启用</span>
</label>
<span class="setting-caption">关闭后只取消 Step 7 后链总预算</span>
</div>
</div>
<div class="data-row module-divider-start" id="row-oauth-display">
<span class="data-label">OAuth</span>
<span id="display-oauth-url" class="data-value mono truncate">等待中...</span>
+17
View File
@@ -288,6 +288,7 @@ const inputAutoSkipFailuresThreadIntervalMinutes = document.getElementById('inpu
const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled');
const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes');
const inputAutoStepDelaySeconds = document.getElementById('input-auto-step-delay-seconds');
const inputOAuthFlowTimeoutEnabled = document.getElementById('input-oauth-flow-timeout-enabled');
const inputVerificationResendCount = document.getElementById('input-verification-resend-count');
const rowPhoneVerificationEnabled = document.getElementById('row-phone-verification-enabled');
const btnTogglePhoneVerificationSection = document.getElementById('btn-toggle-phone-verification-section');
@@ -2650,6 +2651,9 @@ function collectSettingsPayload() {
autoRunDelayEnabled: inputAutoDelayEnabled.checked,
autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value),
autoStepDelaySeconds: normalizeAutoStepDelaySeconds(inputAutoStepDelaySeconds.value),
oauthFlowTimeoutEnabled: inputOAuthFlowTimeoutEnabled
? Boolean(inputOAuthFlowTimeoutEnabled.checked)
: true,
phoneVerificationEnabled: Boolean(inputPhoneVerificationEnabled?.checked),
verificationResendCount: normalizeVerificationResendCount(
inputVerificationResendCount?.value,
@@ -4202,6 +4206,11 @@ function applySettingsState(state) {
inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled);
inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes));
inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(state?.autoStepDelaySeconds);
if (inputOAuthFlowTimeoutEnabled) {
inputOAuthFlowTimeoutEnabled.checked = state?.oauthFlowTimeoutEnabled !== undefined
? Boolean(state.oauthFlowTimeoutEnabled)
: true;
}
if (inputVerificationResendCount) {
const restoredVerificationResendCount = state?.verificationResendCount !== undefined
? state.verificationResendCount
@@ -7765,6 +7774,11 @@ inputAutoDelayMinutes.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
inputOAuthFlowTimeoutEnabled?.addEventListener('change', () => {
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
inputPhoneVerificationEnabled?.addEventListener('change', () => {
if (inputPhoneVerificationEnabled.checked) {
setPhoneVerificationSectionExpanded(true);
@@ -8357,6 +8371,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.payload.autoStepDelaySeconds !== undefined) {
inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(message.payload.autoStepDelaySeconds);
}
if (message.payload.oauthFlowTimeoutEnabled !== undefined && inputOAuthFlowTimeoutEnabled) {
inputOAuthFlowTimeoutEnabled.checked = Boolean(message.payload.oauthFlowTimeoutEnabled);
}
if (
(
message.payload.verificationResendCount !== undefined
+96
View File
@@ -296,3 +296,99 @@ return {
assert.equal(timeoutMs, 15000);
});
test('oauth timeout budget clamps local timeout when enabled by default', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${extractFunction('buildOAuthFlowTimeoutError')}
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
${extractFunction('getOAuthFlowStepTimeoutMs')}
return {
getOAuthFlowStepTimeoutMs,
};
`)();
const timeoutMs = await api.getOAuthFlowStepTimeoutMs(15000, {
step: 8,
actionLabel: '登录验证码流程',
state: {
oauthUrl: 'https://oauth.example/current',
oauthFlowDeadlineAt: Date.now() + 1200,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/current',
},
});
assert(timeoutMs <= 1200);
assert(timeoutMs >= 1000);
});
test('oauth timeout budget disabled mode ignores active deadlines', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${extractFunction('buildOAuthFlowTimeoutError')}
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
${extractFunction('getOAuthFlowStepTimeoutMs')}
return {
getOAuthFlowStepTimeoutMs,
};
`)();
const timeoutMs = await api.getOAuthFlowStepTimeoutMs(15000, {
step: 9,
actionLabel: 'OAuth localhost 回调',
state: {
oauthFlowTimeoutEnabled: false,
oauthUrl: 'https://oauth.example/current',
oauthFlowDeadlineAt: Date.now() - 1000,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/current',
},
});
assert.equal(timeoutMs, 15000);
});
test('startOAuthFlowTimeoutWindow clears stale deadline when timeout is disabled', async () => {
const events = {
stateUpdates: [],
logs: [],
};
const api = new Function('events', `
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
async function getState() {
return {
oauthFlowTimeoutEnabled: false,
oauthFlowDeadlineAt: Date.now() - 1000,
oauthFlowDeadlineSourceUrl: 'https://oauth.example/old',
};
}
async function setState(update) {
events.stateUpdates.push(update);
}
async function addLog(message, level) {
events.logs.push({ message, level });
}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('startOAuthFlowTimeoutWindow')}
return {
startOAuthFlowTimeoutWindow,
};
`)(events);
const result = await api.startOAuthFlowTimeoutWindow({
step: 7,
oauthUrl: 'https://oauth.example/current',
});
assert.equal(result, null);
assert.deepStrictEqual(events.stateUpdates, [{
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
}]);
assert.match(events.logs[0].message, /授权后链总超时已关闭/);
});
@@ -143,3 +143,81 @@ return {
domains: ['mail.example.com'],
});
});
test('oauth flow timeout setting is persisted and normalized as boolean', () => {
const api = new Function(`
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PERSISTED_SETTING_DEFAULTS = {
panelMode: 'cpa',
oauthFlowTimeoutEnabled: true,
autoStepDelaySeconds: null,
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
mailProvider: '163',
mail2925Mode: 'provide',
emailGenerator: 'duck',
autoDeleteUsedIcloudAlias: false,
accountRunHistoryTextEnabled: false,
cloudflareTempEmailUseRandomSubdomain: false,
cloudflareTempEmailDomain: '',
cloudflareTempEmailDomains: [],
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value, fallback = null) { return value == null || value === '' ? fallback : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; }
function normalizeMail2925Mode(value) { return String(value || '').trim().toLowerCase() === 'receive' ? 'receive' : 'provide'; }
function normalizeEmailGenerator(value) { return String(value || '').trim().toLowerCase() || 'duck'; }
function normalizeIcloudHost(value) { return ''; }
function normalizeIcloudTargetMailboxType(value) { return String(value || '').trim() || 'icloud-inbox'; }
function normalizeIcloudForwardMailProvider(value) { return String(value || '').trim() || 'qq'; }
function normalizeIcloudFetchMode(value) { return String(value || '').trim() === 'always_new' ? 'always_new' : 'reuse_existing'; }
function normalizeAccountRunHistoryHelperBaseUrl(value) { return String(value || '').trim(); }
function normalizeHotmailServiceMode(value) { return String(value || '').trim().toLowerCase() === 'remote' ? 'remote' : 'local'; }
function normalizeHotmailRemoteBaseUrl(value) { return String(value || '').trim(); }
function normalizeHotmailLocalBaseUrl(value) { return String(value || '').trim(); }
function normalizeLuckmailBaseUrl(value) { return String(value || '').trim(); }
function normalizeLuckmailEmailType(value) { return String(value || '').trim(); }
function normalizeCloudflareDomain(value) { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareDomains(value) { return Array.isArray(value) ? value.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean) : []; }
function normalizeCustomEmailPool(value) { return Array.isArray(value) ? value : []; }
function normalizeCloudflareTempEmailBaseUrl(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailAddress(value = '') { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareTempEmailReceiveMailbox(value = '') { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareTempEmailDomain(value) { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareTempEmailDomains(value) { return Array.isArray(value) ? value.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean) : []; }
function normalizeHotmailAccounts(value) { return Array.isArray(value) ? value : []; }
function normalizeMail2925Accounts(value) { return Array.isArray(value) ? value : []; }
function normalizePayPalAccounts(value) { return Array.isArray(value) ? value : []; }
function normalizeHeroSmsAcquirePriority(value) { return String(value || '').trim() === 'price' ? 'price' : 'country'; }
function normalizeHeroSmsMaxPrice(value) { return String(value || '').trim(); }
function normalizeHeroSmsCountryFallback(value) { return Array.isArray(value) ? value : []; }
function normalizePhoneVerificationReplacementLimit(value) { return Number(value) || 3; }
function normalizePhoneCodeWaitSeconds(value) { return Number(value) || 60; }
function normalizePhoneCodeTimeoutWindows(value) { return Number(value) || 2; }
function normalizePhoneCodePollIntervalSeconds(value) { return Number(value) || 5; }
function normalizePhoneCodePollMaxRounds(value) { return Number(value) || 4; }
function resolveLegacyAutoStepDelaySeconds() { return undefined; }
${extractFunction('normalizePersistentSettingValue')}
${extractFunction('buildPersistentSettingsPayload')}
return {
buildPersistentSettingsPayload,
normalizePersistentSettingValue,
};
`)();
assert.equal(api.normalizePersistentSettingValue('oauthFlowTimeoutEnabled', 0), false);
assert.equal(api.normalizePersistentSettingValue('oauthFlowTimeoutEnabled', 1), true);
assert.deepEqual(api.buildPersistentSettingsPayload({
oauthFlowTimeoutEnabled: false,
}), {
oauthFlowTimeoutEnabled: false,
});
const defaults = api.buildPersistentSettingsPayload({}, { fillDefaults: true });
assert.equal(defaults.oauthFlowTimeoutEnabled, true);
});
@@ -220,6 +220,7 @@ const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: true };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '10' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '6' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
+2
View File
@@ -128,6 +128,7 @@ const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
@@ -375,6 +376,7 @@ const inputAutoSkipFailuresThreadIntervalMinutes = { value: '' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '' };
const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '' };
const inputPhoneVerificationEnabled = { checked: false };
const DEFAULT_PHONE_VERIFICATION_ENABLED = false;
@@ -199,6 +199,7 @@ const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_PHONE_VERIFICATION_REPLACEMENT_LIMIT = 3;
@@ -77,6 +77,9 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
assert.match(html, /id="row-phone-code-timeout-windows"/);
assert.match(html, /id="row-phone-code-poll-interval-seconds"/);
assert.match(html, /id="row-phone-code-poll-max-rounds"/);
assert.match(html, /id="row-oauth-flow-timeout"/);
assert.match(html, /id="input-oauth-flow-timeout-enabled"/);
assert.match(html, /只取消 Step 7 后链总预算/);
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
});
@@ -222,6 +225,7 @@ const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputOAuthFlowTimeoutEnabled = { checked: false };
const inputPhoneVerificationEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' };
const inputHeroSmsApiKey = { value: 'demo-key' };
@@ -299,6 +303,7 @@ return { collectSettingsPayload };
const payload = api.collectSettingsPayload();
assert.equal(payload.phoneVerificationEnabled, true);
assert.equal(payload.oauthFlowTimeoutEnabled, false);
assert.equal(payload.accountRunHistoryTextEnabled, true);
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(payload.heroSmsApiKey, 'demo-key');
+184
View File
@@ -0,0 +1,184 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const step9ModuleSource = fs.readFileSync('background/steps/confirm-oauth.js', 'utf8');
test('step9 observes disabled oauth timeout while waiting for localhost callback', async () => {
const api = new Function('step9ModuleSource', `
const self = {};
let webNavListener = null;
let webNavCommittedListener = null;
let step8TabUpdatedListener = null;
let step8PendingReject = null;
let cleanupCalls = 0;
let remainingCalls = 0;
let recoveryCalls = 0;
let completePayload = null;
const callbackUrl = 'http://localhost:1455/auth/callback?code=abc&state=xyz';
const chrome = {
webNavigation: {
onBeforeNavigate: {
addListener(listener) {
webNavListener = listener;
setTimeout(() => {
if (typeof webNavListener === 'function') {
webNavListener({ tabId: 123, url: callbackUrl });
}
}, 25);
},
removeListener() {},
},
onCommitted: {
addListener(listener) {
webNavCommittedListener = listener;
},
removeListener() {},
},
},
tabs: {
onUpdated: {
addListener(listener) {
step8TabUpdatedListener = listener;
},
removeListener() {},
},
async update() {},
},
};
async function addLog() {}
function cleanupStep8NavigationListeners() {
cleanupCalls += 1;
webNavListener = null;
webNavCommittedListener = null;
step8TabUpdatedListener = null;
}
function throwIfStep8SettledOrStopped(resolved) {
if (resolved) throw new Error('already resolved');
}
async function getTabId() { return 123; }
async function isTabAlive() { return true; }
async function ensureStep8SignupPageReady() {}
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
if (options.actionLabel === 'OAuth localhost 回调') {
return 1;
}
return defaultTimeoutMs;
}
async function getOAuthFlowRemainingMs() {
remainingCalls += 1;
return null;
}
async function recoverOAuthLocalhostTimeout() {
recoveryCalls += 1;
throw new Error('should not recover when timeout has been disabled');
}
async function waitForStep8Ready() {
return {
consentReady: true,
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
};
}
async function triggerStep8ContentStrategy() {}
async function waitForStep8ClickEffect() {
return { progressed: true, reason: 'url_changed', url: 'https://chatgpt.com/' };
}
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
return Number(details?.tabId) === Number(signupTabId) ? details.url : '';
}
function getStep8CallbackUrlFromTabUpdate() { return ''; }
function getStep8EffectLabel() { return 'URL 已变化'; }
async function prepareStep8DebuggerClick() { return { rect: { centerX: 10, centerY: 10 } }; }
async function clickWithDebugger() {}
async function reloadStep8ConsentPage() {}
async function reuseOrCreateTab() { return 123; }
async function sleepWithStop() {}
function setWebNavListener(listener) { webNavListener = listener; }
function getWebNavListener() { return webNavListener; }
function setWebNavCommittedListener(listener) { webNavCommittedListener = listener; }
function getWebNavCommittedListener() { return webNavCommittedListener; }
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
function setStep8PendingReject(handler) { step8PendingReject = handler; }
async function completeStepFromBackground(step, payload) {
completePayload = { step, payload };
}
const STEP8_CLICK_RETRY_DELAY_MS = 1;
const STEP8_READY_WAIT_TIMEOUT_MS = 5;
const STEP8_MAX_ROUNDS = 1;
const STEP8_STRATEGIES = [
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
];
${step9ModuleSource}
const executor = self.MultiPageBackgroundStep9.createStep9Executor({
addLog,
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeStepFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
getStep8EffectLabel,
getTabId,
getWebNavCommittedListener,
getWebNavListener,
getStep8TabUpdatedListener,
isTabAlive,
prepareStep8DebuggerClick,
recoverOAuthLocalhostTimeout,
reloadStep8ConsentPage,
reuseOrCreateTab,
setStep8PendingReject,
setStep8TabUpdatedListener,
setWebNavCommittedListener,
setWebNavListener,
sleepWithStop,
STEP8_CLICK_RETRY_DELAY_MS,
STEP8_MAX_ROUNDS,
STEP8_READY_WAIT_TIMEOUT_MS,
STEP8_STRATEGIES,
throwIfStep8SettledOrStopped,
triggerStep8ContentStrategy,
waitForStep8ClickEffect,
waitForStep8Ready,
});
return {
executeStep9: executor.executeStep9,
snapshot() {
return {
cleanupCalls,
remainingCalls,
recoveryCalls,
completePayload,
hasPendingReject: Boolean(step8PendingReject),
};
},
};
`)(step9ModuleSource);
await api.executeStep9({
oauthUrl: 'https://auth.openai.com/original-oauth',
visibleStep: 9,
});
const snapshot = api.snapshot();
assert.equal(snapshot.recoveryCalls, 0);
assert.equal(snapshot.remainingCalls >= 1, true);
assert.equal(snapshot.cleanupCalls >= 1, true);
assert.equal(snapshot.hasPendingReject, false);
assert.deepEqual(snapshot.completePayload, {
step: 9,
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
},
});
});
+9 -1
View File
@@ -11,6 +11,7 @@ let step8TabUpdatedListener = null;
let step8PendingReject = null;
let cleanupCalls = 0;
let timeoutCalls = 0;
let remainingCalls = 0;
let recoveryCalls = 0;
let completePayload = null;
const logs = [];
@@ -25,7 +26,7 @@ const chrome = {
if (typeof webNavListener === 'function') {
webNavListener({ tabId: 123, url: callbackUrl });
}
}, 0);
}, 25);
},
removeListener() {},
},
@@ -107,6 +108,11 @@ async function recoverOAuthLocalhostTimeout(details = {}) {
};
}
async function getOAuthFlowRemainingMs() {
remainingCalls += 1;
return null;
}
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
if (
Number(signupTabId) === Number(details?.tabId)
@@ -163,6 +169,7 @@ const executor = self.MultiPageBackgroundStep9.createStep9Executor({
clickWithDebugger,
completeStepFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
@@ -197,6 +204,7 @@ return {
return {
cleanupCalls,
timeoutCalls,
remainingCalls,
recoveryCalls,
completePayload,
hasPendingReject: Boolean(step8PendingReject),