fix: enhance OAuth session handling and add tests for state resets

This commit is contained in:
QLHazyCoder
2026-05-29 21:03:47 +08:00
parent db4af3e49f
commit 3c0bc41794
3 changed files with 182 additions and 20 deletions
+68 -11
View File
@@ -9971,10 +9971,7 @@ function getDownstreamStateResets(step, state = {}) {
gpcPageStatus: '', gpcPageStatus: '',
gpcPageStatusText: '', gpcPageStatusText: '',
}; };
const oauthRuntimeResets = {
if (step <= 1) {
return {
...plusRuntimeResets,
oauthUrl: null, oauthUrl: null,
cpaOAuthState: null, cpaOAuthState: null,
cpaManagementOrigin: null, cpaManagementOrigin: null,
@@ -9986,6 +9983,55 @@ function getDownstreamStateResets(step, state = {}) {
sub2apiProxyId: null, sub2apiProxyId: null,
codex2apiSessionId: null, codex2apiSessionId: null,
codex2apiOAuthState: null, codex2apiOAuthState: null,
};
const getNextStepKey = () => {
const numericStep = Number(step);
const currentNodeId = typeof getNodeIdByStepForState === 'function'
? getNodeIdByStepForState(numericStep, state)
: '';
if (
currentNodeId
&& typeof getNodeIdsForState === 'function'
&& typeof getStepIdByNodeIdForState === 'function'
) {
const nodeIds = getNodeIdsForState(state);
const currentIndex = nodeIds.indexOf(currentNodeId);
const nextNodeId = currentIndex >= 0 ? nodeIds[currentIndex + 1] : '';
const nextStep = nextNodeId ? getStepIdByNodeIdForState(nextNodeId, state) : null;
if (Number.isInteger(nextStep) && nextStep > 0) {
return String(getStepExecutionKeyForState(nextStep, state) || '').trim();
}
}
if (typeof getStepIdsForState === 'function') {
const stepIds = getStepIdsForState(state);
const currentIndex = stepIds.indexOf(numericStep);
const nextStep = currentIndex >= 0 ? stepIds[currentIndex + 1] : null;
if (Number.isInteger(nextStep) && nextStep > 0) {
return String(getStepExecutionKeyForState(nextStep, state) || '').trim();
}
}
return '';
};
const nextStepKey = getNextStepKey();
const isOAuthEntryRestartBoundary = nextStepKey === 'oauth-login' || nextStepKey === 'relogin-bound-email';
const oauthEntryRuntimeResets = {
...oauthRuntimeResets,
lastLoginCode: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
pendingPhoneActivationConfirmation: null,
localhostUrl: null,
currentPhoneVerificationCode: '',
currentPhoneVerificationCountdownEndsAt: 0,
currentPhoneVerificationCountdownWindowIndex: 0,
currentPhoneVerificationCountdownWindowTotal: 0,
};
if (step <= 1) {
return {
...plusRuntimeResets,
...oauthRuntimeResets,
flowStartTime: null, flowStartTime: null,
password: null, password: null,
lastEmailTimestamp: null, lastEmailTimestamp: null,
@@ -10056,6 +10102,7 @@ function getDownstreamStateResets(step, state = {}) {
if (isEarlyRegistrationNode || isBillingNode || isApprovalNode) { if (isEarlyRegistrationNode || isBillingNode || isApprovalNode) {
return { return {
...(isEarlyRegistrationNode ? plusRuntimeResets : {}), ...(isEarlyRegistrationNode ? plusRuntimeResets : {}),
...(isOAuthEntryRestartBoundary ? oauthRuntimeResets : {}),
...(isBillingNode ? { ...(isBillingNode ? {
plusBillingCountryText: '', plusBillingCountryText: '',
plusBillingAddress: null, plusBillingAddress: null,
@@ -10090,6 +10137,7 @@ function getDownstreamStateResets(step, state = {}) {
} }
if (stepKey === 'plus-checkout-return' || stepKey === 'confirm-oauth') { if (stepKey === 'plus-checkout-return' || stepKey === 'confirm-oauth') {
return { return {
...(isOAuthEntryRestartBoundary ? oauthRuntimeResets : {}),
pendingPhoneActivationConfirmation: null, pendingPhoneActivationConfirmation: null,
plusReturnUrl: '', plusReturnUrl: '',
localhostUrl: null, localhostUrl: null,
@@ -10099,12 +10147,10 @@ function getDownstreamStateResets(step, state = {}) {
currentPhoneVerificationCountdownWindowTotal: 0, currentPhoneVerificationCountdownWindowTotal: 0,
}; };
} }
if ( if (stepKey === 'oauth-login' || stepKey === 'relogin-bound-email') {
stepKey === 'oauth-login' return oauthEntryRuntimeResets;
|| stepKey === 'fetch-login-code' }
|| stepKey === 'relogin-bound-email' if (stepKey === 'fetch-login-code' || stepKey === 'fetch-bound-email-login-code') {
|| stepKey === 'fetch-bound-email-login-code'
) {
return { return {
lastLoginCode: null, lastLoginCode: null,
loginVerificationRequestedAt: null, loginVerificationRequestedAt: null,
@@ -10124,6 +10170,9 @@ function getDownstreamStateResets(step, state = {}) {
localhostUrl: null, localhostUrl: null,
}; };
} }
if (isOAuthEntryRestartBoundary) {
return oauthEntryRuntimeResets;
}
return {}; return {};
} }
@@ -14988,6 +15037,10 @@ async function getPostStep6AutoRestartDecision(step, error) {
const hasTransientTokenExchangeSignal = /token_exchange_user_error|invalid request\.?\s*please try again later/i.test(normalizedMessage); const hasTransientTokenExchangeSignal = /token_exchange_user_error|invalid request\.?\s*please try again later/i.test(normalizedMessage);
return mentionsTokenExchange && (hasTransientNetworkSignal || hasTransientTokenExchangeSignal); return mentionsTokenExchange && (hasTransientNetworkSignal || hasTransientTokenExchangeSignal);
}; };
const isPlatformVerifyOAuthSessionExpiredError = (errorMessage = '') => {
const normalizedMessage = String(errorMessage || '');
return /OPENAI_OAUTH_SESSION_NOT_FOUND|session\s+not\s+found\s+or\s+expired|oauth\s+session\s+(?:not\s+found|expired)|missing\s+SUB2API\s+session_id|缺少\s*SUB2API\s*(?:session_id|会话信息)|SUB2API[\s\S]*(?:会话|session)[\s\S]*(?:过期|失效|不存在|not\s+found|expired)/i.test(normalizedMessage);
};
const isPhoneVerificationLocalFailure = (errorMessage = '') => { const isPhoneVerificationLocalFailure = (errorMessage = '') => {
const normalizedMessage = String(errorMessage || ''); const normalizedMessage = String(errorMessage || '');
if (isPhoneSmsPlatformRateLimitFailure(normalizedMessage)) { if (isPhoneSmsPlatformRateLimitFailure(normalizedMessage)) {
@@ -15034,11 +15087,15 @@ async function getPostStep6AutoRestartDecision(step, error) {
&& confirmOauthStep > 0 && confirmOauthStep > 0
&& confirmOauthStep < normalizedStep && confirmOauthStep < normalizedStep
&& isPlatformVerifyTransientRetryError(errorMessage); && isPlatformVerifyTransientRetryError(errorMessage);
const shouldRestartFromOAuthLoginStep = currentNodeKey === 'platform-verify'
&& isPlatformVerifyOAuthSessionExpiredError(errorMessage);
const restartAnchorStep = shouldRetryFromConfirmStep const restartAnchorStep = shouldRetryFromConfirmStep
? confirmOauthStep ? confirmOauthStep
: (shouldRestartFromOAuthLoginStep
? authChainStartStep
: (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0 : (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0
? boundEmailReloginStep ? boundEmailReloginStep
: authChainStartStep); : authChainStartStep));
if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) { if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) {
return { return {
shouldRestart: false, shouldRestart: false,
+47
View File
@@ -159,6 +159,7 @@ const bundle = [
extractFunction('startAutoRunNodeIdleLogWatchdog'), extractFunction('startAutoRunNodeIdleLogWatchdog'),
extractFunction('runAutoNodeActionWithIdleLogWatchdog'), extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'), extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
extractFunction('getDownstreamStateResets'),
extractFunction('getPostStep6AutoRestartDecision'), extractFunction('getPostStep6AutoRestartDecision'),
NODE_COMPAT_HELPERS, NODE_COMPAT_HELPERS,
extractFunction('getAutoRunWorkflowNodeIds'), extractFunction('getAutoRunWorkflowNodeIds'),
@@ -279,6 +280,7 @@ const events = {
steps: [], steps: [],
logs: [], logs: [],
invalidations: [], invalidations: [],
stateUpdates: [],
cancellations: [], cancellations: [],
stopBroadcasts: 0, stopBroadcasts: 0,
}; };
@@ -330,6 +332,10 @@ async function getTabId() {
} }
async function invalidateDownstreamAfterStepRestart(step, options = {}) { async function invalidateDownstreamAfterStepRestart(step, options = {}) {
events.invalidations.push({ step, options }); events.invalidations.push({ step, options });
const resets = getDownstreamStateResets(step, await getState());
if (Object.keys(resets).length > 0) {
events.stateUpdates.push(resets);
}
} }
function cancelPendingCommands(reason = '') { function cancelPendingCommands(reason = '') {
events.cancellations.push(reason); events.cancellations.push(reason);
@@ -576,6 +582,47 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc
assert.ok(events.logs.some(({ message }) => /回到节点 confirm-oauth 重新开始授权流程/.test(message))); assert.ok(events.logs.some(({ message }) => /回到节点 confirm-oauth 重新开始授权流程/.test(message)));
}); });
test('auto-run restarts SUB2API expired oauth session from oauth-login and clears stale session state', async () => {
const harness = createHarness({
failureStep: 10,
failureBudget: 1,
failureMessage: 'session not found or expired',
authState: { state: 'oauth_consent_page', url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent' },
customState: {
panelMode: 'sub2api',
stepStatuses: { 3: 'completed' },
stepsVersion: 'ultra2.0',
visibleStep: 10,
sub2apiSessionId: 'expired-session',
sub2apiOAuthState: 'expired-state',
sub2apiGroupId: 5,
sub2apiGroupIds: [5],
sub2apiDraftName: 'draft',
sub2apiProxyId: 7,
accountContributionEnabled: false,
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [7, 8, 9, 10, 7, 8, 9, 10]);
assert.equal(events.invalidations.length, 1);
assert.deepStrictEqual(events.invalidations[0], {
step: 6,
options: {
logLabel: '节点 platform-verify 报错后准备回到 oauth-login 重试(第 1 次重开)',
},
});
const resetPatch = events.stateUpdates.find((updates) => updates.sub2apiSessionId === null);
assert.ok(resetPatch, 'expected oauth-login restart to clear stale SUB2API OAuth runtime');
assert.equal(resetPatch.sub2apiOAuthState, null);
assert.equal(resetPatch.sub2apiGroupId, null);
assert.deepStrictEqual(resetPatch.sub2apiGroupIds, []);
assert.equal(resetPatch.sub2apiDraftName, null);
assert.equal(resetPatch.sub2apiProxyId, null);
assert.ok(events.logs.some(({ message }) => /回到节点 oauth-login 重新开始授权流程/.test(message)));
});
test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from step 10', async () => { test('auto-run restarts Plus/GPC oauth-login aggregate entry-open failure from step 10', async () => {
const plusGpcSteps = { const plusGpcSteps = {
6: { key: 'plus-checkout-create' }, 6: { key: 'plus-checkout-create' },
+59 -1
View File
@@ -2,6 +2,54 @@ const test = require('node:test');
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const fs = require('node:fs'); const fs = require('node:fs');
function extractFunction(source, name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('background step-1 state plumbing persists and resets cpa oauth runtime keys', () => { test('background step-1 state plumbing persists and resets cpa oauth runtime keys', () => {
const source = fs.readFileSync('background.js', 'utf8'); const source = fs.readFileSync('background.js', 'utf8');
@@ -9,7 +57,17 @@ test('background step-1 state plumbing persists and resets cpa oauth runtime key
assert.match(source, /cpaManagementOrigin:\s*null/); assert.match(source, /cpaManagementOrigin:\s*null/);
assert.match(source, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/); assert.match(source, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/);
assert.match(source, /payload\.cpaManagementOrigin[^\n]*updates\.cpaManagementOrigin/); assert.match(source, /payload\.cpaManagementOrigin[^\n]*updates\.cpaManagementOrigin/);
assert.match(source, /if \(step <= 1\) \{[\s\S]*cpaOAuthState:\s*null,[\s\S]*cpaManagementOrigin:\s*null,/);
const harness = new Function(`
function getStepExecutionKeyForState() {
return '';
}
${extractFunction(source, 'getDownstreamStateResets')}
return { getDownstreamStateResets };
`)();
const resets = harness.getDownstreamStateResets(1, {});
assert.equal(resets.cpaOAuthState, null);
assert.equal(resets.cpaManagementOrigin, null);
}); });
test('message router step-1 handler stores cpa oauth runtime keys', async () => { test('message router step-1 handler stores cpa oauth runtime keys', async () => {