fix: enhance OAuth session handling and add tests for state resets
This commit is contained in:
+68
-11
@@ -9971,10 +9971,7 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
gpcPageStatus: '',
|
||||
gpcPageStatusText: '',
|
||||
};
|
||||
|
||||
if (step <= 1) {
|
||||
return {
|
||||
...plusRuntimeResets,
|
||||
const oauthRuntimeResets = {
|
||||
oauthUrl: null,
|
||||
cpaOAuthState: null,
|
||||
cpaManagementOrigin: null,
|
||||
@@ -9986,6 +9983,55 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
sub2apiProxyId: null,
|
||||
codex2apiSessionId: 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,
|
||||
password: null,
|
||||
lastEmailTimestamp: null,
|
||||
@@ -10056,6 +10102,7 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
if (isEarlyRegistrationNode || isBillingNode || isApprovalNode) {
|
||||
return {
|
||||
...(isEarlyRegistrationNode ? plusRuntimeResets : {}),
|
||||
...(isOAuthEntryRestartBoundary ? oauthRuntimeResets : {}),
|
||||
...(isBillingNode ? {
|
||||
plusBillingCountryText: '',
|
||||
plusBillingAddress: null,
|
||||
@@ -10090,6 +10137,7 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
}
|
||||
if (stepKey === 'plus-checkout-return' || stepKey === 'confirm-oauth') {
|
||||
return {
|
||||
...(isOAuthEntryRestartBoundary ? oauthRuntimeResets : {}),
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
plusReturnUrl: '',
|
||||
localhostUrl: null,
|
||||
@@ -10099,12 +10147,10 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
currentPhoneVerificationCountdownWindowTotal: 0,
|
||||
};
|
||||
}
|
||||
if (
|
||||
stepKey === 'oauth-login'
|
||||
|| stepKey === 'fetch-login-code'
|
||||
|| stepKey === 'relogin-bound-email'
|
||||
|| stepKey === 'fetch-bound-email-login-code'
|
||||
) {
|
||||
if (stepKey === 'oauth-login' || stepKey === 'relogin-bound-email') {
|
||||
return oauthEntryRuntimeResets;
|
||||
}
|
||||
if (stepKey === 'fetch-login-code' || stepKey === 'fetch-bound-email-login-code') {
|
||||
return {
|
||||
lastLoginCode: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
@@ -10124,6 +10170,9 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
localhostUrl: null,
|
||||
};
|
||||
}
|
||||
if (isOAuthEntryRestartBoundary) {
|
||||
return oauthEntryRuntimeResets;
|
||||
}
|
||||
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);
|
||||
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 normalizedMessage = String(errorMessage || '');
|
||||
if (isPhoneSmsPlatformRateLimitFailure(normalizedMessage)) {
|
||||
@@ -15034,11 +15087,15 @@ async function getPostStep6AutoRestartDecision(step, error) {
|
||||
&& confirmOauthStep > 0
|
||||
&& confirmOauthStep < normalizedStep
|
||||
&& isPlatformVerifyTransientRetryError(errorMessage);
|
||||
const shouldRestartFromOAuthLoginStep = currentNodeKey === 'platform-verify'
|
||||
&& isPlatformVerifyOAuthSessionExpiredError(errorMessage);
|
||||
const restartAnchorStep = shouldRetryFromConfirmStep
|
||||
? confirmOauthStep
|
||||
: (shouldRestartFromOAuthLoginStep
|
||||
? authChainStartStep
|
||||
: (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0
|
||||
? boundEmailReloginStep
|
||||
: authChainStartStep);
|
||||
: authChainStartStep));
|
||||
if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) {
|
||||
return {
|
||||
shouldRestart: false,
|
||||
|
||||
@@ -159,6 +159,7 @@ const bundle = [
|
||||
extractFunction('startAutoRunNodeIdleLogWatchdog'),
|
||||
extractFunction('runAutoNodeActionWithIdleLogWatchdog'),
|
||||
extractFunction('executeNodeAndWaitWithAutoRunIdleLogWatchdog'),
|
||||
extractFunction('getDownstreamStateResets'),
|
||||
extractFunction('getPostStep6AutoRestartDecision'),
|
||||
NODE_COMPAT_HELPERS,
|
||||
extractFunction('getAutoRunWorkflowNodeIds'),
|
||||
@@ -279,6 +280,7 @@ const events = {
|
||||
steps: [],
|
||||
logs: [],
|
||||
invalidations: [],
|
||||
stateUpdates: [],
|
||||
cancellations: [],
|
||||
stopBroadcasts: 0,
|
||||
};
|
||||
@@ -330,6 +332,10 @@ async function getTabId() {
|
||||
}
|
||||
async function invalidateDownstreamAfterStepRestart(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 = '') {
|
||||
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)));
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const plusGpcSteps = {
|
||||
6: { key: 'plus-checkout-create' },
|
||||
|
||||
@@ -2,6 +2,54 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
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', () => {
|
||||
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, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/);
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user