diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js index 5ecd1e7..8b95c47 100644 --- a/background/steps/fetch-login-code.js +++ b/background/steps/fetch-login-code.js @@ -212,7 +212,10 @@ { type: 'SUBMIT_ADD_EMAIL', source: 'background', - payload: { email: resolvedEmail }, + payload: { + email: resolvedEmail, + nodeId: state?.nodeId || activeFetchLoginCodeStepKey || 'fetch-login-code', + }, }, { timeoutMs, diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index 746f126..e8dd1f9 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -307,7 +307,7 @@ 'signup-page', { type: 'EXECUTE_NODE', - nodeId: 'oauth-login', + nodeId: state?.nodeId || 'oauth-login', step: 7, source: 'background', payload: { diff --git a/background/steps/platform-verify.js b/background/steps/platform-verify.js index 842fbaa..ebd8649 100644 --- a/background/steps/platform-verify.js +++ b/background/steps/platform-verify.js @@ -94,15 +94,15 @@ } const code = normalizeString(parsed.searchParams.get('code')); - const state = normalizeString(parsed.searchParams.get('state')); - if (!code || !state) { + const oauthState = normalizeString(parsed.searchParams.get('state')); + if (!code || !oauthState) { throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmOauthStep}。`); } return { url: parsed.toString(), code, - state, + state: oauthState, }; } diff --git a/background/sub2api-api.js b/background/sub2api-api.js index 6e3cf90..5b51825 100644 --- a/background/sub2api-api.js +++ b/background/sub2api-api.js @@ -503,15 +503,15 @@ } const code = normalizeString(parsed.searchParams.get('code')); - const state = normalizeString(parsed.searchParams.get('state')); - if (!code || !state) { + const oauthState = normalizeString(parsed.searchParams.get('state')); + if (!code || !oauthState) { throw new Error('回调 URL 中缺少 code 或 state。'); } return { url: parsed.toString(), code, - state, + state: oauthState, }; } diff --git a/background/verification-flow.js b/background/verification-flow.js index b684a52..27221f5 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -1117,6 +1117,7 @@ } await chrome.tabs.update(signupTabId, { active: true }); + const completionNodeId = await getNodeIdForStep(completionStep); const baseResponseTimeoutMs = await getResponseTimeoutMsForStep( step, step === 8 @@ -1134,6 +1135,7 @@ source: 'background', payload: { code, + ...(completionNodeId ? { nodeId: completionNodeId } : {}), ...(step === 4 && options.signupProfile ? { signupProfile: options.signupProfile } : {}), }, }; @@ -1268,6 +1270,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) { const completionStep = getCompletionStep(step, options); activeVerificationLogStep = completionStep; + const completionNodeId = await getNodeIdForStep(completionStep); const stateKey = getVerificationCodeStateKey(step); const rejectedCodes = new Set(); const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER @@ -1419,7 +1422,6 @@ [stateKey]: result.code, }); - const completionNodeId = await getNodeIdForStep(completionStep); if (!completionNodeId) { throw new Error(`步骤 ${completionStep} 未映射到验证码节点。`); } diff --git a/content/signup-page.js b/content/signup-page.js index 47d375f..086e571 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -78,6 +78,7 @@ const SIGNUP_PAGE_NODE_HANDLERS = Object.freeze({ 'fill-password': (payload) => step3_fillEmailPassword(payload), 'fill-profile': (payload) => step5_fillNameBirthday(payload), 'oauth-login': (payload) => step6_login(payload), + 'relogin-bound-email': (payload) => step6_login(payload), 'confirm-oauth': (_payload) => step8_findAndClick(), }); diff --git a/tests/platform-verify-step-resolution.test.js b/tests/platform-verify-step-resolution.test.js new file mode 100644 index 0000000..efe637b --- /dev/null +++ b/tests/platform-verify-step-resolution.test.js @@ -0,0 +1,138 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8'); +const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({}); + +function createExecutor(overrides = {}) { + const events = { + logs: [], + completed: [], + fetchCalls: [], + }; + + const executor = api.createStep10Executor({ + addLog: async (message, level, options) => { + events.logs.push({ message, level, options }); + }, + chrome: {}, + closeConflictingTabsForSource: async () => {}, + completeNodeFromBackground: async (nodeId, payload) => { + events.completed.push({ nodeId, payload }); + }, + ensureContentScriptReadyOnTab: async () => {}, + getPanelMode: () => 'cpa', + getStepIdByKeyForState: (stepKey) => ({ + 'oauth-login': 7, + 'relogin-bound-email': 11, + 'confirm-oauth': 14, + 'platform-verify': 15, + })[stepKey] || null, + getTabId: async () => 1, + isLocalhostOAuthCallbackUrl: (url) => { + try { + const parsed = new URL(url); + return ['localhost', '127.0.0.1'].includes(parsed.hostname) + && Boolean(parsed.searchParams.get('code')) + && Boolean(parsed.searchParams.get('state')); + } catch { + return false; + } + }, + isTabAlive: async () => true, + normalizeCodex2ApiUrl: (url) => url, + normalizeSub2ApiUrl: (url) => url, + rememberSourceLastUrl: () => {}, + reuseOrCreateTab: async () => 1, + sendToContentScript: async () => ({}), + sendToContentScriptResilient: async () => ({}), + shouldBypassStep9ForLocalCpa: () => false, + SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 30000, + ...overrides, + }); + + return { executor, events }; +} + +test('CPA platform verify resolves relogin tail steps from active definitions', async () => { + const originalFetch = global.fetch; + const { executor, events } = createExecutor(); + global.fetch = async (url, options = {}) => { + events.fetchCalls.push({ url, body: JSON.parse(options.body || '{}') }); + return { + ok: true, + status: 200, + json: async () => ({ message: 'CPA callback accepted' }), + }; + }; + + try { + await executor.executeStep10({ + visibleStep: 15, + nodeId: 'platform-verify', + localhostUrl: 'http://127.0.0.1:8317/codex/callback?code=abc&state=expected', + cpaOAuthState: 'expected', + cpaManagementOrigin: 'http://127.0.0.1:8317', + vpsUrl: 'http://127.0.0.1:8317/management.html#/oauth', + vpsPassword: 'secret', + }); + } finally { + global.fetch = originalFetch; + } + + assert.equal(events.fetchCalls.length, 1); + assert.equal(events.fetchCalls[0].body.redirect_url, 'http://127.0.0.1:8317/codex/callback?code=abc&state=expected'); + assert.deepStrictEqual(events.completed, [ + { + nodeId: 'platform-verify', + payload: { + localhostUrl: 'http://127.0.0.1:8317/codex/callback?code=abc&state=expected', + verifiedStatus: 'CPA callback accepted', + }, + }, + ]); +}); + +test('CPA state mismatch points relogin-enabled flow back to relogin-bound-email step', async () => { + const { executor } = createExecutor(); + + await assert.rejects( + () => executor.executeStep10({ + visibleStep: 15, + nodeId: 'platform-verify', + localhostUrl: 'http://127.0.0.1:8317/codex/callback?code=abc&state=actual', + cpaOAuthState: 'expected', + cpaManagementOrigin: 'http://127.0.0.1:8317', + vpsUrl: 'http://127.0.0.1:8317/management.html#/oauth', + vpsPassword: 'secret', + }), + (error) => { + assert.match(error.message, /11/); + assert.doesNotMatch(error.message, /步骤\s*10|姝ラ.*10/); + return true; + } + ); +}); + +test('CPA invalid localhost callback points to the dynamic confirm-oauth step', async () => { + const { executor } = createExecutor({ + isLocalhostOAuthCallbackUrl: () => false, + }); + + await assert.rejects( + () => executor.executeStep10({ + visibleStep: 15, + nodeId: 'platform-verify', + localhostUrl: 'notaurl', + cpaManagementOrigin: 'http://127.0.0.1:8317', + vpsUrl: 'http://127.0.0.1:8317/management.html#/oauth', + vpsPassword: 'secret', + }), + (error) => { + assert.match(error.message, /14/); + assert.doesNotMatch(error.message, /12/); + return true; + } + ); +});