实现客户端登录七天滑动续期

需求:解决短效访问令牌到期后客户端一小时掉登录的问题。

实现:由 Electron Main 加密管理并轮换刷新凭据,按真实用户活动续期,七天闲置后清理会话,并补齐并发、迁移和终态回归测试。
This commit is contained in:
2026-08-07 16:11:10 +08:00
parent 7b23cce67a
commit 86ece3a430
20 changed files with 2941 additions and 326 deletions

View File

@@ -8,12 +8,21 @@ import { NIANCODE_AUTH_CONFIG } from '../auth-config';
import { WORKS_SQUARE_CONFIG } from '../works-config';
import {
clearWorksSquareSession,
storeWorksSquareSession,
storeWorksSquareSessionFromTokenPayload,
commitWorksSquareSession,
commitWorksSquareSessionFromTokenPayload,
discardUnrestorableWorksSquareSession,
flushWorksSquareSessionPersistence,
getValidWorksSquareAccessToken,
getWorksSquareSessionRestoreStatus,
getWorksSquareSessionSnapshot,
markWorksSquareSessionActive,
retryWorksSquareSessionRestore,
} from '../../services/works-square-session';
import { clearWorksSquareAIGatewayCredential } from '../../services/works-square-ai-gateway';
import { getProviderService } from '../../services/providers/provider-service';
import { NIANCODE_USER_MODEL_ACCOUNT_ID } from '../../../shared/user-model-config';
import {
clearManagedWorksSquareRuntime,
clearManagedWorksSquareRuntimeBestEffort,
ensureManagedWorksSquareRuntimeClean,
} from '../../services/works-square-runtime';
import { logger } from '../../utils/logger';
import type { WorksSquareTokenPayload } from '../../services/works-square-session';
@@ -31,10 +40,6 @@ type PasswordLoginInput = AuthClientInput & {
scope?: unknown;
};
type RefreshInput = AuthClientInput & {
refreshToken?: unknown;
};
type LogoutInput = {
authBase?: unknown;
accessToken?: unknown;
@@ -45,6 +50,11 @@ type SessionSyncInput = {
refreshToken?: unknown;
tokenType?: unknown;
expiresAt?: unknown;
lastActiveAt?: unknown;
};
type SessionRefreshInput = {
forceRefresh?: unknown;
};
type DesktopAuthStartPayload = {
@@ -85,6 +95,16 @@ function readOptionalNumber(value: unknown): number | null {
return null;
}
function readOptionalBoolean(value: unknown, fallback: boolean): boolean {
return typeof value === 'boolean' ? value : fallback;
}
function withoutRefreshToken(payload: unknown): unknown {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload;
const { refresh_token: _refreshToken, ...publicPayload } = payload as Record<string, unknown>;
return publicPayload;
}
function normalizeAuthBase(value: unknown = NIANCODE_AUTH_CONFIG.gatewayAuthUrl): string {
const authBase = readOptionalString(value, NIANCODE_AUTH_CONFIG.gatewayAuthUrl).replace(/\/+$/, '');
if (!/^https?:\/\//i.test(authBase)) {
@@ -231,7 +251,11 @@ async function pollDesktopAuthToken(
throw new Error('Authorization timed out');
}
async function handleBrowserAuthorization(res: ServerResponse): Promise<void> {
async function handleBrowserAuthorization(
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
if (!await ensureRuntimeReadyForLogin(res, ctx)) return;
const response = await proxyAwareFetch(createWorksUrl('/api/auth/desktop/start').toString(), {
method: 'POST',
});
@@ -257,13 +281,19 @@ async function handleBrowserAuthorization(res: ServerResponse): Promise<void> {
await shell.openExternal(authorizeUrl);
const token = await pollDesktopAuthToken(requestId, deviceSecret, pollIntervalMs);
let session = null;
if (token && typeof token === 'object' && !Array.isArray(token)) {
storeWorksSquareSessionFromTokenPayload(token as WorksSquareTokenPayload);
session = await commitWorksSquareSessionFromTokenPayload(token as WorksSquareTokenPayload);
}
sendJson(res, 200, { success: true, token });
sendJson(res, 200, { success: true, token: withoutRefreshToken(token), session });
}
async function handlePasswordLogin(req: IncomingMessage, res: ServerResponse): Promise<void> {
async function handlePasswordLogin(
req: IncomingMessage,
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
if (!await ensureRuntimeReadyForLogin(res, ctx)) return;
const body = await parseJsonBody<PasswordLoginInput>(req);
const authBase = normalizeAuthBase(body.authBase);
const clientId = readOptionalString(body.clientId, NIANCODE_AUTH_CONFIG.clientId);
@@ -303,91 +333,171 @@ async function handlePasswordLogin(req: IncomingMessage, res: ServerResponse): P
return;
}
let session = null;
if (
tokenResult.payload
&& typeof tokenResult.payload === 'object'
&& !Array.isArray(tokenResult.payload)
) {
storeWorksSquareSessionFromTokenPayload(tokenResult.payload as WorksSquareTokenPayload);
session = await commitWorksSquareSessionFromTokenPayload(
tokenResult.payload as WorksSquareTokenPayload,
);
}
sendJson(res, 200, { success: true, token: tokenResult.payload });
sendJson(res, 200, {
success: true,
token: withoutRefreshToken(tokenResult.payload),
session,
});
}
async function handleRefresh(req: IncomingMessage, res: ServerResponse): Promise<void> {
const body = await parseJsonBody<RefreshInput>(req);
const authBase = normalizeAuthBase(body.authBase);
const clientId = readOptionalString(body.clientId, NIANCODE_AUTH_CONFIG.clientId);
const clientSecret = readOptionalString(body.clientSecret, NIANCODE_AUTH_CONFIG.clientSecret);
const refreshToken = readRequiredString(body.refreshToken, 'refreshToken');
const tokenResult = await requestToken(
authBase,
clientId,
clientSecret,
createTokenRequestBody({
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
);
if (!tokenResult.ok) {
sendJson(res, tokenResult.status === 401 ? 401 : 502, {
success: false,
error: getErrorMessage(tokenResult.payload, `Refresh failed (${tokenResult.status})`),
});
async function handleSessionSync(
req: IncomingMessage,
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
if (!await ensureWorksSquareSessionRestored()) {
sendJson(res, 503, { success: false, error: '登录状态暂时无法恢复,请稍后重试。' });
return;
}
if (
tokenResult.payload
&& typeof tokenResult.payload === 'object'
&& !Array.isArray(tokenResult.payload)
) {
storeWorksSquareSessionFromTokenPayload(tokenResult.payload as WorksSquareTokenPayload, refreshToken);
try {
await flushWorksSquareSessionPersistence();
} catch (error) {
logger.warn('[auth] Session persistence is temporarily unavailable during sync', error);
sendJson(res, 503, { success: false, error: '登录状态暂时无法同步,请稍后重试。' });
return;
}
sendJson(res, 200, { success: true, token: tokenResult.payload });
}
async function handleSessionSync(req: IncomingMessage, res: ServerResponse): Promise<void> {
const body = await parseJsonBody<SessionSyncInput>(req);
storeWorksSquareSession({
accessToken: readRequiredString(body.accessToken, 'accessToken'),
refreshToken: readOptionalTrimmedString(body.refreshToken),
tokenType: readOptionalTrimmedString(body.tokenType),
expiresAt: readOptionalNumber(body.expiresAt),
});
let session = getWorksSquareSessionSnapshot();
const legacyAccessToken = readOptionalTrimmedString(body.accessToken);
if (!session && legacyAccessToken) {
try {
await ensureManagedWorksSquareRuntimeClean(ctx);
session = await commitWorksSquareSession({
accessToken: legacyAccessToken,
refreshToken: readOptionalTrimmedString(body.refreshToken),
tokenType: readOptionalTrimmedString(body.tokenType),
expiresAt: readOptionalNumber(body.expiresAt),
lastActiveAt: readOptionalNumber(body.lastActiveAt),
});
} catch (error) {
logger.warn('[auth] Failed to persist a migrated Renderer session', error);
sendJson(res, 503, { success: false, error: '登录状态暂时无法同步,请稍后重试。' });
return;
}
}
sendJson(res, 200, { success: true });
if (!session) {
sendJson(res, 200, { success: true, session: null });
return;
}
sendJson(res, 200, { success: true, session });
}
async function clearManagedWorksSquareRuntime(ctx: HostApiContext): Promise<void> {
const errors: string[] = [];
async function handleSessionRefresh(
req: IncomingMessage,
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
if (!await ensureWorksSquareSessionRestored()) {
sendJson(res, 503, { success: false, error: '登录状态暂时无法恢复,请稍后重试。' });
return;
}
const body = await parseJsonBody<SessionRefreshInput>(req);
try {
await ctx.imageWorkspace?.closeEventSessions?.();
const accessToken = await getValidWorksSquareAccessToken({
forceRefresh: readOptionalBoolean(body.forceRefresh, true),
});
const session = getWorksSquareSessionSnapshot();
if (accessToken && session) {
sendJson(res, 200, { success: true, session });
return;
}
if (!session) {
await clearManagedWorksSquareRuntimeBestEffort(ctx, 'terminal session refresh');
sendJson(res, 401, { success: false, error: '登录已过期,请重新授权。' });
return;
}
sendJson(res, 502, { success: false, error: '登录续期暂时失败,请稍后重试。' });
} catch (error) {
errors.push(error instanceof Error ? error.message : String(error));
logger.warn('[auth] Failed to refresh the managed Works Square session', error);
sendJson(res, 502, { success: false, error: '登录续期暂时失败,请稍后重试。' });
}
}
async function handleSessionActivity(
res: ServerResponse,
ctx: HostApiContext,
): Promise<void> {
if (!await ensureWorksSquareSessionRestored()) {
sendJson(res, 503, { success: false, error: '登录状态暂时无法恢复,请稍后重试。' });
return;
}
let session;
try {
await ctx.opencodeManager.stop();
session = await markWorksSquareSessionActive();
} catch (error) {
errors.push(error instanceof Error ? error.message : String(error));
logger.warn('[auth] Failed to persist Works Square session activity', error);
sendJson(res, 503, { success: false, error: '登录活动暂时无法保存,请稍后重试。' });
return;
}
if (!session) {
await clearManagedWorksSquareRuntimeBestEffort(ctx, 'idle session activity');
sendJson(res, 401, { success: false, error: '登录已过期,请重新授权。' });
return;
}
sendJson(res, 200, {
success: true,
session,
});
}
clearWorksSquareAIGatewayCredential();
async function ensureWorksSquareSessionRestored(): Promise<boolean> {
if (getWorksSquareSessionRestoreStatus() === 'unavailable') {
await retryWorksSquareSessionRestore();
}
return getWorksSquareSessionRestoreStatus() === 'ready';
}
async function ensureRuntimeReadyForLogin(
res: ServerResponse,
ctx: HostApiContext,
): Promise<boolean> {
let discardedUnrestorableSession = false;
if (!await ensureWorksSquareSessionRestored()) {
if (!await discardUnrestorableWorksSquareSession()) {
sendJson(res, 503, {
success: false,
error: '登录状态暂时无法恢复,请稍后重试。',
});
return false;
}
discardedUnrestorableSession = true;
}
try {
await getProviderService().deleteAccountApiKey(NIANCODE_USER_MODEL_ACCOUNT_ID);
if (discardedUnrestorableSession) {
await clearManagedWorksSquareRuntime(ctx, undefined, true);
}
await ensureManagedWorksSquareRuntimeClean(ctx);
return true;
} catch (error) {
errors.push(error instanceof Error ? error.message : String(error));
logger.error('[auth] Failed to clean the previous runtime before login', error);
sendJson(res, 503, {
success: false,
error: '上次登录的本地运行环境尚未清理完成,请稍后重试。',
});
return false;
}
}
if (errors.length > 0) {
throw new Error(`Failed to clear managed Works Square runtime state: ${errors.join('; ')}`);
}
async function handleSessionClear(res: ServerResponse, ctx: HostApiContext): Promise<void> {
const accessToken = getWorksSquareSessionSnapshot()?.accessToken;
clearWorksSquareSession();
await Promise.all([
flushWorksSquareSessionPersistence(),
clearManagedWorksSquareRuntime(ctx, accessToken),
]);
sendJson(res, 200, { success: true });
}
async function handleLogout(
@@ -397,16 +507,24 @@ async function handleLogout(
): Promise<void> {
const body = await parseJsonBody<LogoutInput>(req);
const authBase = normalizeAuthBase(body.authBase);
const accessToken = readRequiredString(body.accessToken, 'accessToken');
const rendererAccessToken = readOptionalTrimmedString(body.accessToken);
const accessToken = getWorksSquareSessionSnapshot()?.accessToken
?? readRequiredString(rendererAccessToken, 'accessToken');
let cleanupError: Error | null = null;
clearWorksSquareSession();
try {
await clearManagedWorksSquareRuntime(ctx);
await clearManagedWorksSquareRuntime(ctx, accessToken);
} catch (error) {
cleanupError = error instanceof Error ? error : new Error(String(error));
logger.error('[auth] Failed to clear managed Works Square runtime state during logout', cleanupError);
}
clearWorksSquareSession();
try {
await flushWorksSquareSessionPersistence();
} catch (error) {
cleanupError = error instanceof Error ? error : new Error(String(error));
logger.error('[auth] Failed to persist the local session clear during logout', cleanupError);
}
let response: Response;
try {
@@ -459,22 +577,32 @@ export async function handleAuthRoutes(
try {
if (url.pathname === '/api/auth/browser/start' && req.method === 'POST') {
await handleBrowserAuthorization(res);
await handleBrowserAuthorization(res, ctx);
return true;
}
if (url.pathname === '/api/auth/login' && req.method === 'POST') {
await handlePasswordLogin(req, res);
return true;
}
if (url.pathname === '/api/auth/refresh' && req.method === 'POST') {
await handleRefresh(req, res);
await handlePasswordLogin(req, res, ctx);
return true;
}
if (url.pathname === '/api/auth/session/sync' && req.method === 'POST') {
await handleSessionSync(req, res);
await handleSessionSync(req, res, ctx);
return true;
}
if (url.pathname === '/api/auth/session/refresh' && req.method === 'POST') {
await handleSessionRefresh(req, res, ctx);
return true;
}
if (url.pathname === '/api/auth/session/activity' && req.method === 'POST') {
await handleSessionActivity(res, ctx);
return true;
}
if (url.pathname === '/api/auth/session/clear' && req.method === 'POST') {
await handleSessionClear(res, ctx);
return true;
}

View File

@@ -22,6 +22,11 @@ export type DesignWorkspaceEventSubscriptionInput = {
afterEventId?: string;
};
export type CloseEventSessionsOptions = {
accessToken?: string;
tolerateRemoteFailure?: boolean;
};
export class DesignWorkspaceModuleError extends Error {
readonly status: number;
readonly code: string;
@@ -47,7 +52,7 @@ export interface DesignWorkspaceModule {
openWorkspaceEvents?(
input: DesignWorkspaceEventSubscriptionInput,
): Promise<DesignWorkspaceEventSubscription>;
closeEventSessions?(): Promise<void>;
closeEventSessions?(options?: CloseEventSessionsOptions): Promise<void>;
openAssetContent(workspaceId: string, assetId: string, range?: string): Promise<Response>;
reset?(): Promise<DesignWorkspaceBootstrap>;
}

View File

@@ -26,6 +26,7 @@ import { proxyAwareFetch } from '../utils/proxy-fetch';
import { getValidWorksSquareAccessToken } from '../services/works-square-session';
import {
DesignWorkspaceModuleError,
type CloseEventSessionsOptions,
type DesignWorkspaceEventSubscription,
type DesignWorkspaceEventSubscriptionInput,
type DesignWorkspaceModule,
@@ -1114,7 +1115,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
};
}
async closeEventSessions(): Promise<void> {
async closeEventSessions(options: CloseEventSessionsOptions = {}): Promise<void> {
this.eventSessionsEnabled = false;
const activeSubscriptions = [...this.eventSubscriptionClosers.values()]
.flatMap((closers) => [...closers]);
@@ -1122,27 +1123,42 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
for (const close of activeSubscriptions) close();
const pendingSessions = [...this.eventSessions.entries()];
this.eventSessions.clear();
const sessions = await Promise.allSettled(
pendingSessions.map(async ([workspaceId, pending]) => ({
workspaceId,
session: await pending,
})),
);
const closeResults = await Promise.allSettled(
sessions.flatMap((result) => (
result.status === 'fulfilled'
? [this.closeEventSession(result.value.session.session_id)
.then(() => this.rotateEventSession(result.value.workspaceId))]
: []
)),
);
const uncertainCreations = sessions.filter((result) => (
result.status === 'rejected'
&& !(result.reason instanceof DesignWorkspaceModuleError
&& result.reason.code === 'DESIGN_EVENT_SESSION_CLOSED')
const sessions = await Promise.all(pendingSessions.map(async ([workspaceId, pending]) => {
try {
return { workspaceId, session: await pending, creationError: null as unknown };
} catch (error) {
return { workspaceId, session: null, creationError: error };
}
}));
const closeResults = await Promise.all(sessions.map(async ({
workspaceId,
session,
creationError,
}) => {
let remoteError = creationError;
if (session) {
try {
await this.closeEventSession(session.session_id, options.accessToken);
} catch (error) {
remoteError = error;
}
}
let rotationError: unknown = null;
try {
await this.rotateEventSession(workspaceId);
} catch (error) {
rotationError = error;
}
return { remoteError, rotationError };
}));
const failed = closeResults.filter(({ remoteError, rotationError }) => (
Boolean(rotationError)
|| (!options.tolerateRemoteFailure
&& Boolean(remoteError)
&& !(remoteError instanceof DesignWorkspaceModuleError
&& remoteError.code === 'DESIGN_EVENT_SESSION_CLOSED'))
)).length;
const failedCloses = closeResults.filter((result) => result.status === 'rejected').length;
const failed = uncertainCreations + failedCloses;
if (failed > 0) {
throw new Error(`Failed to close ${failed} AI design Agent Session(s)`);
}
@@ -1155,7 +1171,11 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
);
}
private async requestJson<T>(path: string, init: RequestInit = {}): Promise<T> {
private async requestJson<T>(
path: string,
init: RequestInit = {},
accessToken?: string,
): Promise<T> {
const response = await this.authorizedFetch(path, {
...init,
headers: {
@@ -1165,7 +1185,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
: {}),
...(init.headers ?? {}),
},
});
}, accessToken);
const payload = await readPayload(response);
if (!response.ok) {
const detail = asErrorDetail(payload);
@@ -1184,7 +1204,12 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
return payload as T;
}
private async authorizedFetch(path: string, init: RequestInit = {}): Promise<Response> {
private async authorizedFetch(
path: string,
init: RequestInit = {},
accessToken?: string,
): Promise<Response> {
if (accessToken) return this.fetchWithToken(path, accessToken, init);
let token = await getValidWorksSquareAccessToken({ fetchImpl: this.fetchImpl });
if (!token) {
throw new DesignWorkspaceModuleError(401, 'AUTH_REQUIRED', '请先登录后再使用 AI 设计');
@@ -1314,11 +1339,12 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
for (const resolve of waiters) resolve(run);
}
private async closeEventSession(sessionId: string): Promise<void> {
private async closeEventSession(sessionId: string, accessToken?: string): Promise<void> {
try {
await this.requestJson<ServerAgentSession>(
`/api/agents/sessions/${encodeURIComponent(sessionId)}`,
{ method: 'DELETE' },
accessToken,
);
} catch (error) {
if (error instanceof DesignWorkspaceModuleError

View File

@@ -70,6 +70,15 @@ import { AgentBrowserModule, ElectronAgentBrowserAdapter } from '../agent-browse
import { browserOAuthManager } from '../utils/browser-oauth';
import { createProjectProgressSync } from '../services/project-progress-sync';
import { createWorksCloudDeployment } from '../services/works-cloud-deployment';
import {
consumeWorksSquareStartupRuntimeCleanupRequired,
getWorksSquareSessionRestoreStatus,
getWorksSquareSessionSnapshot,
initializeWorksSquareSession,
subscribeWorksSquareSession,
type WorksSquareSessionChangeReason,
} from '../services/works-square-session';
import { clearManagedWorksSquareRuntimeBestEffort } from '../services/works-square-runtime';
import { getPort } from '../utils/config';
import { initializeMeowaGameAssetsCredential } from '../api/routes/meowa-game-assets';
import {
@@ -318,6 +327,26 @@ function registerMakeloreProtocolClient(): void {
function createMainWindow(): BrowserWindow {
const win = createWindow();
const sendAuthSession = (
session: ReturnType<typeof getWorksSquareSessionSnapshot>,
reason: WorksSquareSessionChangeReason = 'changed',
previousSession: ReturnType<typeof getWorksSquareSessionSnapshot> = null,
) => {
if (!session && getWorksSquareSessionRestoreStatus() === 'unavailable') return;
if (!win.isDestroyed() && !win.webContents.isDestroyed()) {
win.webContents.send('auth:session-changed', session);
}
if (!session && reason === 'terminal') {
void clearManagedWorksSquareRuntimeBestEffort({
opencodeManager,
imageWorkspace: imageWorkspaceModule ?? undefined,
}, 'terminal session invalidation', previousSession?.accessToken);
}
};
const unsubscribeAuthSession = subscribeWorksSquareSession(sendAuthSession);
win.webContents.on('did-finish-load', () => {
sendAuthSession(getWorksSquareSessionSnapshot());
});
const closeAgentBrowserForHostRenderer = (reason: string): void => {
void agentBrowser?.close().catch((error) => {
@@ -358,6 +387,7 @@ function createMainWindow(): BrowserWindow {
});
win.on('closed', () => {
unsubscribeAuthSession();
const browser = agentBrowser;
agentBrowser = null;
void browser?.dispose().catch((error) => {
@@ -409,14 +439,15 @@ async function initialize(): Promise<void> {
logger.info('Running in E2E mode: startup side effects minimized');
}
// Restore authentication before any background service or renderer can request a token.
await initializeWorksSquareSession();
opencodeProjectStore = createProjectStore(await createElectronProjectStorage());
if (!isE2EMode) {
projectProgressSync = createProjectProgressSync(opencodeProjectStore);
void projectProgressSync.start();
}
worksCloudDeployment = createWorksCloudDeployment(opencodeProjectStore);
if (!isE2EMode) void worksCloudDeployment.start();
const localImageWorkspaceEnabled = isLocalImageWorkspaceDevelopmentEnabled({
isPackaged: app.isPackaged,
@@ -432,6 +463,12 @@ async function initialize(): Promise<void> {
},
});
imageWorkspaceModule = imageWorkspace;
if (consumeWorksSquareStartupRuntimeCleanupRequired()) {
await clearManagedWorksSquareRuntimeBestEffort({
opencodeManager,
imageWorkspace,
}, 'expired persisted session during startup');
}
if (localImageWorkspaceEnabled) {
logger.info('AI painting workspace is using local development storage');
} else {
@@ -444,6 +481,10 @@ async function initialize(): Promise<void> {
// Create the main window
const window = createMainWindow();
agentBrowser = new AgentBrowserModule(new ElectronAgentBrowserAdapter(window));
if (!isE2EMode) {
void projectProgressSync?.start();
void worksCloudDeployment.start();
}
// Create system tray
if (!isE2EMode) {

View File

@@ -80,6 +80,7 @@ const validEventChannels = [
'oauth:error',
'agent-browser:show',
'agent-browser:state',
'auth:session-changed',
];
/**

View File

@@ -0,0 +1,103 @@
import { NIANCODE_USER_MODEL_ACCOUNT_ID } from '../../shared/user-model-config';
import { logger } from '../utils/logger';
import { clearWorksSquareAIGatewayCredential } from './works-square-ai-gateway';
import { getProviderService } from './providers/provider-service';
export type WorksSquareRuntimeContext = {
opencodeManager: {
stop(): Promise<unknown>;
};
imageWorkspace?: {
closeEventSessions?(options?: {
accessToken?: string;
tolerateRemoteFailure?: boolean;
}): Promise<unknown>;
};
};
let cleanupFlight: Promise<void> | null = null;
let cleanupRequired = false;
async function runManagedWorksSquareRuntimeCleanup(
ctx: WorksSquareRuntimeContext,
accessToken?: string,
tolerateRemoteFailure = false,
): Promise<void> {
// Revoke the in-memory derived credential synchronously; slower resource cleanup follows.
clearWorksSquareAIGatewayCredential();
const results = await Promise.allSettled([
(async () => await ctx.imageWorkspace?.closeEventSessions?.({
accessToken,
tolerateRemoteFailure,
}))(),
(async () => await ctx.opencodeManager.stop())(),
(async () => {
const deleted = await getProviderService().deleteAccountApiKey(
NIANCODE_USER_MODEL_ACCOUNT_ID,
);
if (!deleted) throw new Error('provider API key storage rejected deletion');
})(),
]);
const errors = results
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
.map((result) => (
result.reason instanceof Error ? result.reason.message : String(result.reason)
));
if (errors.length > 0) {
throw new Error(`Failed to clear managed Works Square runtime state: ${errors.join('; ')}`);
}
}
export function clearManagedWorksSquareRuntime(
ctx: WorksSquareRuntimeContext,
accessToken?: string,
tolerateRemoteFailure = false,
): Promise<void> {
if (cleanupFlight) return cleanupFlight;
cleanupRequired = true;
const flight = runManagedWorksSquareRuntimeCleanup(
ctx,
accessToken,
tolerateRemoteFailure,
)
.then(() => {
cleanupRequired = false;
})
.finally(() => {
if (cleanupFlight === flight) cleanupFlight = null;
});
cleanupFlight = flight;
return flight;
}
export async function ensureManagedWorksSquareRuntimeClean(
ctx: WorksSquareRuntimeContext,
): Promise<void> {
if (cleanupFlight) {
try {
await cleanupFlight;
} catch {
// Retry below with the current runtime context.
}
}
if (cleanupRequired) await clearManagedWorksSquareRuntime(ctx);
}
export async function clearManagedWorksSquareRuntimeBestEffort(
ctx: WorksSquareRuntimeContext,
reason: string,
accessToken?: string,
): Promise<void> {
try {
await clearManagedWorksSquareRuntime(ctx, accessToken, true);
} catch (error) {
logger.error(`[auth] Failed to clear managed runtime after ${reason}`, error);
}
}
export function resetManagedWorksSquareRuntimeForTests(): void {
cleanupFlight = null;
cleanupRequired = false;
}

View File

@@ -1,14 +1,18 @@
import { NIANCODE_AUTH_CONFIG } from '../api/auth-config';
import { proxyAwareFetch } from '../utils/proxy-fetch';
import { logger } from '../utils/logger';
import { WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS } from '../../shared/auth-session';
const TOKEN_REFRESH_SKEW_MS = 30_000;
const SESSION_STORE_SCHEMA_VERSION = 1;
export { WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS } from '../../shared/auth-session';
export type WorksSquareSessionInput = {
accessToken: string;
refreshToken?: string | null;
tokenType?: string | null;
expiresAt?: number | null;
lastActiveAt?: number | null;
};
export type WorksSquareTokenPayload = {
@@ -19,29 +23,51 @@ export type WorksSquareTokenPayload = {
};
export type WorksSquareSessionSnapshot = {
accessToken: string;
tokenType: string;
expiresAt: number | null;
lastActiveAt: number;
canRefresh: boolean;
};
type StoredWorksSquareSession = {
accessToken: string;
refreshToken: string | null;
tokenType: string;
expiresAt: number | null;
lastActiveAt: number;
};
export type WorksSquareSessionListener = (session: WorksSquareSessionSnapshot | null) => void;
let currentSession: WorksSquareSessionSnapshot | null = null;
let refreshPromise: Promise<string | null> | null = null;
const sessionListeners = new Set<WorksSquareSessionListener>();
function notifySessionListeners(): void {
const snapshot = currentSession ? { ...currentSession } : null;
for (const listener of sessionListeners) {
try {
listener(snapshot);
} catch {
// Session observers must not affect authentication state changes.
}
}
export interface WorksSquareSessionPersistence {
discardedInvalidRecord?: boolean;
load(): Promise<WorksSquareSessionInput | null>;
save(session: WorksSquareSessionInput | null): Promise<void>;
}
export type WorksSquareSessionChangeReason = 'changed' | 'terminal';
export type WorksSquareSessionListener = (
session: WorksSquareSessionSnapshot | null,
reason: WorksSquareSessionChangeReason,
previousSession?: WorksSquareSessionSnapshot | null,
) => void;
export type WorksSquareSessionRestoreStatus = 'ready' | 'unavailable';
type RefreshFlight = {
generation: number;
promise: Promise<string | null>;
};
let currentSession: StoredWorksSquareSession | null = null;
let credentialGeneration = 0;
let refreshFlight: RefreshFlight | null = null;
let sessionPersistence: WorksSquareSessionPersistence | null = null;
let sessionPersistenceFactory: (() => Promise<WorksSquareSessionPersistence | null>) | null = null;
let persistenceQueue: Promise<void> = Promise.resolve();
let credentialPersistenceBarrier: Promise<void> | null = null;
let restoreStatus: WorksSquareSessionRestoreStatus = 'ready';
let startupRuntimeCleanupRequired = false;
const sessionListeners = new Set<WorksSquareSessionListener>();
function asString(value: unknown): string | null {
return typeof value === 'string' && value.trim() ? value.trim() : null;
}
@@ -57,11 +83,57 @@ function createBasicAuthHeader(clientId: string, clientSecret: string): string {
return `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
}
function toPublicSnapshot(
session: StoredWorksSquareSession | null,
): WorksSquareSessionSnapshot | null {
if (!session) return null;
return {
accessToken: session.accessToken,
tokenType: session.tokenType,
expiresAt: session.expiresAt,
lastActiveAt: session.lastActiveAt,
canRefresh: Boolean(session.refreshToken),
};
}
function toPersistenceInput(
session: StoredWorksSquareSession | null,
): WorksSquareSessionInput | null {
return session ? { ...session } : null;
}
function normalizeSession(
input: WorksSquareSessionInput,
nowMs = Date.now(),
requireLastActiveAt = false,
): StoredWorksSquareSession | null {
const accessToken = asString(input.accessToken);
if (!accessToken) return null;
if (
requireLastActiveAt
&& (typeof input.lastActiveAt !== 'number' || !Number.isFinite(input.lastActiveAt))
) {
return null;
}
return {
accessToken,
refreshToken: asString(input.refreshToken),
tokenType: asString(input.tokenType) ?? 'Bearer',
expiresAt: typeof input.expiresAt === 'number' && Number.isFinite(input.expiresAt)
? input.expiresAt
: null,
lastActiveAt: typeof input.lastActiveAt === 'number' && Number.isFinite(input.lastActiveAt)
? Math.min(input.lastActiveAt, nowMs)
: nowMs,
};
}
function parseTokenPayload(
payload: WorksSquareTokenPayload,
fallbackRefreshToken: string | null,
nowMs = Date.now(),
): WorksSquareSessionSnapshot {
lastActiveAt = nowMs,
): StoredWorksSquareSession {
const accessToken = asString(payload.access_token);
if (!accessToken) {
throw new Error('Auth refresh response did not include access_token');
@@ -72,9 +144,217 @@ function parseTokenPayload(
refreshToken: asString(payload.refresh_token) ?? fallbackRefreshToken,
tokenType: asString(payload.token_type) ?? 'Bearer',
expiresAt: expiresAtFromExpiresIn(payload.expires_in, nowMs),
lastActiveAt,
};
}
function isSessionIdle(session: StoredWorksSquareSession, nowMs: number): boolean {
return nowMs - session.lastActiveAt >= WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS;
}
function notifySessionListeners(
reason: WorksSquareSessionChangeReason = 'changed',
previousSession: WorksSquareSessionSnapshot | null = null,
): void {
const snapshot = toPublicSnapshot(currentSession);
for (const listener of sessionListeners) {
try {
listener(
snapshot ? { ...snapshot } : null,
reason,
previousSession ? { ...previousSession } : null,
);
} catch {
// Session observers must not affect authentication state changes.
}
}
}
function queuePersistence(session: StoredWorksSquareSession | null): Promise<void> {
const persistence = sessionPersistence;
if (!persistence) return Promise.resolve();
const snapshot = toPersistenceInput(session);
const operation = persistenceQueue
.catch(() => undefined)
.then(() => persistence.save(snapshot));
persistenceQueue = operation;
return operation;
}
function replaceCredentialSession(
session: StoredWorksSquareSession | null,
notify = true,
): {
generation: number;
persisted: Promise<void>;
previousSession: WorksSquareSessionSnapshot | null;
} {
const previousSession = toPublicSnapshot(currentSession);
currentSession = session;
credentialGeneration += 1;
refreshFlight = null;
const generation = credentialGeneration;
const persisted = queuePersistence(session);
if (sessionPersistence) {
credentialPersistenceBarrier = persisted;
void persisted.then(
() => {
if (credentialPersistenceBarrier === persisted) credentialPersistenceBarrier = null;
},
() => {
if (credentialPersistenceBarrier === persisted) credentialPersistenceBarrier = null;
},
);
}
if (notify) notifySessionListeners('changed', previousSession);
return { generation, persisted, previousSession };
}
async function createElectronSessionPersistence(): Promise<WorksSquareSessionPersistence | null> {
const [{ default: Store }, { safeStorage }] = await Promise.all([
import('electron-store'),
import('electron'),
]);
const usesUnprotectedLinuxBackend = process.platform === 'linux'
&& safeStorage.getSelectedStorageBackend() === 'basic_text';
if (!safeStorage.isEncryptionAvailable() || usesUnprotectedLinuxBackend) {
logger.warn('[works-square-session] OS credential encryption is unavailable; session restore disabled');
return null;
}
type EncryptedSessionRecord = {
version: number;
authBase: string;
ciphertext: string;
};
const storeOptions = {
name: 'works-square-session',
configFileMode: 0o600,
} as const;
let discardedInvalidRecord = false;
let store: Store<{ record?: EncryptedSessionRecord }>;
try {
store = new Store<{ record?: EncryptedSessionRecord }>(storeOptions);
} catch (error) {
logger.warn('[works-square-session] Invalid session store config; discarding it', error);
store = new Store<{ record?: EncryptedSessionRecord }>({
...storeOptions,
clearInvalidConfig: true,
});
discardedInvalidRecord = true;
}
return {
discardedInvalidRecord,
async load() {
const record = store.get('record');
if (!record) return null;
if (
record.version !== SESSION_STORE_SCHEMA_VERSION
|| record.authBase !== NIANCODE_AUTH_CONFIG.gatewayAuthUrl
) {
store.delete('record');
return null;
}
return JSON.parse(
safeStorage.decryptString(Buffer.from(record.ciphertext, 'base64')),
) as WorksSquareSessionInput;
},
async save(session) {
if (!session) {
store.delete('record');
return;
}
const encrypted = safeStorage.encryptString(JSON.stringify(session));
store.set('record', {
version: SESSION_STORE_SCHEMA_VERSION,
authBase: NIANCODE_AUTH_CONFIG.gatewayAuthUrl,
ciphertext: encrypted.toString('base64'),
});
},
};
}
export async function initializeWorksSquareSession(
options: {
persistence?: WorksSquareSessionPersistence;
nowMs?: number;
} = {},
): Promise<WorksSquareSessionSnapshot | null> {
persistenceQueue = Promise.resolve();
credentialPersistenceBarrier = null;
startupRuntimeCleanupRequired = false;
sessionPersistenceFactory = options.persistence ? null : createElectronSessionPersistence;
try {
sessionPersistence = options.persistence
?? await sessionPersistenceFactory!();
} catch (error) {
logger.warn('[works-square-session] Failed to initialize secure session persistence', error);
sessionPersistence = null;
restoreStatus = 'unavailable';
startupRuntimeCleanupRequired = true;
currentSession = null;
credentialGeneration += 1;
refreshFlight = null;
return null;
}
startupRuntimeCleanupRequired = Boolean(sessionPersistence?.discardedInvalidRecord);
if (!sessionPersistence) {
restoreStatus = 'ready';
currentSession = null;
credentialGeneration += 1;
refreshFlight = null;
return null;
}
let restored: StoredWorksSquareSession | null;
let hadPersistedSession: boolean;
try {
const persisted = await sessionPersistence.load();
hadPersistedSession = Boolean(persisted);
restored = persisted
? normalizeSession(persisted, options.nowMs ?? Date.now(), true)
: null;
} catch (error) {
logger.warn('[works-square-session] Failed to restore the persisted session', error);
restoreStatus = 'unavailable';
startupRuntimeCleanupRequired = true;
currentSession = null;
credentialGeneration += 1;
refreshFlight = null;
return null;
}
const nowMs = options.nowMs ?? Date.now();
if (restored && !isSessionIdle(restored, nowMs)) {
restoreStatus = 'ready';
currentSession = restored;
credentialGeneration += 1;
refreshFlight = null;
return toPublicSnapshot(restored);
}
if (!hadPersistedSession) {
restoreStatus = 'ready';
currentSession = null;
credentialGeneration += 1;
refreshFlight = null;
return null;
}
startupRuntimeCleanupRequired = hadPersistedSession;
restoreStatus = 'ready';
const cleared = replaceCredentialSession(null, false);
try {
await cleared.persisted;
} catch (error) {
restoreStatus = 'unavailable';
logger.warn('[works-square-session] Failed to clear an invalid persisted session', error);
}
return null;
}
async function readResponsePayload(response: Response): Promise<unknown> {
const text = await response.text();
if (!text.trim()) return null;
@@ -86,38 +366,84 @@ async function readResponsePayload(response: Response): Promise<unknown> {
}
export function storeWorksSquareSession(input: WorksSquareSessionInput): void {
const accessToken = input.accessToken.trim();
if (!accessToken) {
const session = normalizeSession(input);
if (!session || isSessionIdle(session, Date.now())) {
clearWorksSquareSession();
return;
}
replaceCredentialSession(session);
}
currentSession = {
accessToken,
refreshToken: input.refreshToken?.trim() || null,
tokenType: input.tokenType?.trim() || 'Bearer',
expiresAt: typeof input.expiresAt === 'number' && Number.isFinite(input.expiresAt)
? input.expiresAt
: null,
};
notifySessionListeners();
export async function commitWorksSquareSession(
input: WorksSquareSessionInput,
): Promise<WorksSquareSessionSnapshot | null> {
const session = normalizeSession(input);
if (!session || isSessionIdle(session, Date.now())) {
await clearWorksSquareSessionPersisted('terminal');
return null;
}
const update = replaceCredentialSession(session, false);
try {
await update.persisted;
} catch (error) {
await failClosedAfterPersistenceError(update.generation, error);
throw error;
}
restoreStatus = 'ready';
if (credentialGeneration !== update.generation) return null;
notifySessionListeners('changed', update.previousSession);
return getWorksSquareSessionSnapshot();
}
export function storeWorksSquareSessionFromTokenPayload(
payload: WorksSquareTokenPayload,
fallbackRefreshToken: string | null = null,
nowMs = Date.now(),
lastActiveAt = nowMs,
): WorksSquareSessionSnapshot {
const session = parseTokenPayload(payload, fallbackRefreshToken, nowMs);
currentSession = session;
notifySessionListeners();
return session;
const session = parseTokenPayload(payload, fallbackRefreshToken, nowMs, lastActiveAt);
replaceCredentialSession(session);
return toPublicSnapshot(session)!;
}
export async function commitWorksSquareSessionFromTokenPayload(
payload: WorksSquareTokenPayload,
fallbackRefreshToken: string | null = null,
nowMs = Date.now(),
lastActiveAt = nowMs,
): Promise<WorksSquareSessionSnapshot> {
const session = parseTokenPayload(payload, fallbackRefreshToken, nowMs, lastActiveAt);
const update = replaceCredentialSession(session, false);
try {
await update.persisted;
} catch (error) {
await failClosedAfterPersistenceError(update.generation, error);
throw error;
}
restoreStatus = 'ready';
if (credentialGeneration !== update.generation) {
throw new Error('Session changed while credentials were being persisted');
}
notifySessionListeners('changed', update.previousSession);
return toPublicSnapshot(session)!;
}
export function clearWorksSquareSession(): void {
currentSession = null;
refreshPromise = null;
notifySessionListeners();
replaceCredentialSession(null);
}
async function clearWorksSquareSessionPersisted(
reason: WorksSquareSessionChangeReason = 'changed',
): Promise<void> {
const update = replaceCredentialSession(null, false);
if (credentialGeneration === update.generation) {
notifySessionListeners(reason, update.previousSession);
}
await update.persisted;
}
export async function flushWorksSquareSessionPersistence(): Promise<void> {
await persistenceQueue;
}
export function subscribeWorksSquareSession(listener: WorksSquareSessionListener): () => void {
@@ -128,14 +454,169 @@ export function subscribeWorksSquareSession(listener: WorksSquareSessionListener
}
export function getWorksSquareSessionSnapshot(): WorksSquareSessionSnapshot | null {
return currentSession ? { ...currentSession } : null;
const snapshot = toPublicSnapshot(currentSession);
return snapshot ? { ...snapshot } : null;
}
export function getWorksSquareSessionRestoreStatus(): WorksSquareSessionRestoreStatus {
return restoreStatus;
}
export function consumeWorksSquareStartupRuntimeCleanupRequired(): boolean {
const required = startupRuntimeCleanupRequired;
startupRuntimeCleanupRequired = false;
return required;
}
export async function retryWorksSquareSessionRestore(
nowMs = Date.now(),
): Promise<WorksSquareSessionSnapshot | null> {
if (restoreStatus !== 'unavailable') {
return getWorksSquareSessionSnapshot();
}
if (!sessionPersistence && sessionPersistenceFactory) {
try {
sessionPersistence = await sessionPersistenceFactory();
startupRuntimeCleanupRequired ||= Boolean(sessionPersistence?.discardedInvalidRecord);
} catch (error) {
logger.warn('[works-square-session] Secure session persistence is still unavailable', error);
return null;
}
}
if (!sessionPersistence) {
restoreStatus = 'ready';
return null;
}
let restored: StoredWorksSquareSession | null;
let hadPersistedSession: boolean;
try {
const persisted = await sessionPersistence.load();
hadPersistedSession = Boolean(persisted);
restored = persisted ? normalizeSession(persisted, nowMs, true) : null;
} catch (error) {
logger.warn('[works-square-session] Persisted session restore is still unavailable', error);
startupRuntimeCleanupRequired = true;
return null;
}
if (restored && !isSessionIdle(restored, nowMs)) {
restoreStatus = 'ready';
currentSession = restored;
credentialGeneration += 1;
refreshFlight = null;
notifySessionListeners();
return getWorksSquareSessionSnapshot();
}
if (!hadPersistedSession) {
restoreStatus = 'ready';
return null;
}
restoreStatus = 'ready';
try {
await clearWorksSquareSessionPersisted('terminal');
restoreStatus = 'ready';
} catch (error) {
restoreStatus = 'unavailable';
logger.warn('[works-square-session] Failed to clear an unusable persisted session', error);
}
return null;
}
export async function discardUnrestorableWorksSquareSession(): Promise<boolean> {
if (restoreStatus !== 'unavailable') return true;
const persistence = sessionPersistence;
if (!persistence) return false;
const cleared = replaceCredentialSession(null, false);
try {
await cleared.persisted;
} catch (error) {
restoreStatus = 'unavailable';
logger.warn('[works-square-session] Failed to discard the unrestorable session', error);
return false;
}
restoreStatus = 'ready';
startupRuntimeCleanupRequired = true;
if (credentialGeneration === cleared.generation) {
notifySessionListeners('terminal', cleared.previousSession);
}
return true;
}
export async function markWorksSquareSessionActive(
nowMs = Date.now(),
): Promise<WorksSquareSessionSnapshot | null> {
const session = currentSession;
if (!session) return null;
if (isSessionIdle(session, nowMs)) {
await clearWorksSquareSessionPersisted('terminal');
return null;
}
const generation = credentialGeneration;
const previousLastActiveAt = session.lastActiveAt;
currentSession = { ...session, lastActiveAt: nowMs };
try {
await queuePersistence(currentSession);
} catch (error) {
if (
credentialGeneration === generation
&& currentSession?.lastActiveAt === nowMs
) {
currentSession = { ...currentSession, lastActiveAt: previousLastActiveAt };
}
throw error;
}
return getWorksSquareSessionSnapshot();
}
async function failClosedAfterPersistenceError(
generation: number,
error: unknown,
): Promise<void> {
logger.error('[works-square-session] Failed to persist rotated credentials; clearing session', error);
if (credentialGeneration !== generation) return;
const cleared = replaceCredentialSession(null, false);
if (credentialGeneration === cleared.generation) {
notifySessionListeners('terminal', cleared.previousSession);
}
try {
await cleared.persisted;
} catch (clearError) {
logger.error('[works-square-session] Failed to persist the fail-closed session clear', clearError);
}
}
function sessionStillMatches(
session: StoredWorksSquareSession,
generation: number,
): boolean {
return credentialGeneration === generation
&& currentSession?.accessToken === session.accessToken
&& currentSession.refreshToken === session.refreshToken;
}
async function waitForCredentialPersistence(): Promise<boolean> {
const barrier = credentialPersistenceBarrier;
if (!barrier) return true;
try {
await barrier;
return true;
} catch {
return false;
}
}
async function refreshWorksSquareSession(
session: StoredWorksSquareSession,
generation: number,
options: { fetchImpl?: typeof fetch; nowMs?: number } = {},
): Promise<string | null> {
const session = currentSession;
if (!session?.refreshToken) return null;
if (!session.refreshToken) return null;
const fetchImpl = options.fetchImpl ?? proxyAwareFetch;
const nowMs = options.nowMs ?? Date.now();
@@ -157,41 +638,95 @@ async function refreshWorksSquareSession(
if (!response.ok) {
logger.warn('[works-square-session] Refresh failed', { status: response.status });
clearWorksSquareSession();
if ((response.status === 400 || response.status === 401) && sessionStillMatches(session, generation)) {
await clearWorksSquareSessionPersisted('terminal');
}
return null;
}
const payload = await readResponsePayload(response);
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
clearWorksSquareSession();
logger.warn('[works-square-session] Refresh returned an invalid payload');
return null;
}
const nextSession = storeWorksSquareSessionFromTokenPayload(
if (!sessionStillMatches(session, generation)) {
return null;
}
const nextSession = parseTokenPayload(
payload as WorksSquareTokenPayload,
session.refreshToken,
nowMs,
currentSession!.lastActiveAt,
);
return nextSession.accessToken;
const update = replaceCredentialSession(nextSession, false);
try {
await update.persisted;
} catch (error) {
await failClosedAfterPersistenceError(update.generation, error);
return null;
}
if (credentialGeneration !== update.generation) return null;
notifySessionListeners('changed', update.previousSession);
return currentSession?.accessToken ?? null;
}
export async function getValidWorksSquareAccessToken(
options: { fetchImpl?: typeof fetch; nowMs?: number; forceRefresh?: boolean } = {},
options: {
fetchImpl?: typeof fetch;
nowMs?: number;
forceRefresh?: boolean;
} = {},
): Promise<string | null> {
if (restoreStatus === 'unavailable') {
await retryWorksSquareSessionRestore(options.nowMs ?? Date.now());
if (restoreStatus === 'unavailable') return null;
}
if (credentialPersistenceBarrier && !await waitForCredentialPersistence()) return null;
const session = currentSession;
if (!session) return null;
const nowMs = options.nowMs ?? Date.now();
if (isSessionIdle(session, nowMs)) {
await clearWorksSquareSessionPersisted('terminal');
return null;
}
const nearExpiry = Boolean(
session.expiresAt
&& session.expiresAt <= nowMs + TOKEN_REFRESH_SKEW_MS,
);
if (!nearExpiry && !options.forceRefresh) return session.accessToken;
if (!refreshPromise) {
refreshPromise = refreshWorksSquareSession(options).finally(() => {
refreshPromise = null;
});
if (!session.refreshToken) {
await clearWorksSquareSessionPersisted('terminal');
return null;
}
return await refreshPromise;
const generation = credentialGeneration;
if (refreshFlight?.generation === generation) {
return await refreshFlight.promise;
}
const flight: RefreshFlight = {
generation,
promise: Promise.resolve(null),
};
flight.promise = refreshWorksSquareSession(session, generation, options).finally(() => {
if (refreshFlight === flight) refreshFlight = null;
});
refreshFlight = flight;
return await flight.promise;
}
export function resetWorksSquareSessionForTests(): void {
currentSession = null;
credentialGeneration += 1;
refreshFlight = null;
sessionPersistence = null;
sessionPersistenceFactory = null;
persistenceQueue = Promise.resolve();
credentialPersistenceBarrier = null;
restoreStatus = 'ready';
startupRuntimeCleanupRequired = false;
sessionListeners.clear();
}