fix: enhance OAuth session handling and add tests for state resets
This commit is contained in:
+76
-19
@@ -9971,21 +9971,67 @@ function getDownstreamStateResets(step, state = {}) {
|
|||||||
gpcPageStatus: '',
|
gpcPageStatus: '',
|
||||||
gpcPageStatusText: '',
|
gpcPageStatusText: '',
|
||||||
};
|
};
|
||||||
|
const oauthRuntimeResets = {
|
||||||
|
oauthUrl: null,
|
||||||
|
cpaOAuthState: null,
|
||||||
|
cpaManagementOrigin: null,
|
||||||
|
sub2apiSessionId: null,
|
||||||
|
sub2apiOAuthState: null,
|
||||||
|
sub2apiGroupId: null,
|
||||||
|
sub2apiGroupIds: [],
|
||||||
|
sub2apiDraftName: null,
|
||||||
|
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) {
|
if (step <= 1) {
|
||||||
return {
|
return {
|
||||||
...plusRuntimeResets,
|
...plusRuntimeResets,
|
||||||
oauthUrl: null,
|
...oauthRuntimeResets,
|
||||||
cpaOAuthState: null,
|
|
||||||
cpaManagementOrigin: null,
|
|
||||||
sub2apiSessionId: null,
|
|
||||||
sub2apiOAuthState: null,
|
|
||||||
sub2apiGroupId: null,
|
|
||||||
sub2apiGroupIds: [],
|
|
||||||
sub2apiDraftName: null,
|
|
||||||
sub2apiProxyId: null,
|
|
||||||
codex2apiSessionId: null,
|
|
||||||
codex2apiOAuthState: null,
|
|
||||||
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
|
||||||
: (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0
|
: (shouldRestartFromOAuthLoginStep
|
||||||
|
? authChainStartStep
|
||||||
|
: (isBoundEmailReloginTailStep && Number.isFinite(boundEmailReloginStep) && boundEmailReloginStep > 0
|
||||||
? boundEmailReloginStep
|
? boundEmailReloginStep
|
||||||
: authChainStartStep);
|
: authChainStartStep));
|
||||||
if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) {
|
if (isPhoneSmsPlatformRateLimitFailure(errorMessage)) {
|
||||||
return {
|
return {
|
||||||
shouldRestart: false,
|
shouldRestart: false,
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user