Files
makelore/electron/services/works-square-session.ts

834 lines
27 KiB
TypeScript

import { createHash } from 'node:crypto';
import { WORKS_SQUARE_CONFIG } from '../api/works-config';
import { proxyAwareFetch, runWithDeadline } from '../utils/proxy-fetch';
import { logger } from '../utils/logger';
import { NIANCODE_AUTH_GATEWAY_URL } from '../../shared/auth-public';
import { WORKS_SQUARE_SESSION_IDLE_TIMEOUT_MS } from '../../shared/auth-session';
const TOKEN_REFRESH_SKEW_MS = 30_000;
const WORKS_SQUARE_AUTH_REQUEST_TIMEOUT_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;
/** Opaque Main-owned local storage partition. Never a raw account identifier. */
accountPartitionKey?: string | null;
};
export type WorksSquareTokenPayload = {
access_token?: unknown;
refresh_token?: unknown;
token_type?: unknown;
expires_in?: unknown;
user_id?: unknown;
username?: unknown;
};
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;
accountPartitionKey: string | null;
};
export type WorksSquareAccountBinding = Readonly<{
accountKey: string;
epoch: number;
}>;
export interface WorksSquareSessionPersistence {
discardedInvalidRecord?: boolean;
load(): Promise<WorksSquareSessionInput | null>;
save(session: WorksSquareSessionInput | null): Promise<void>;
}
export interface WorksSquareSecureStorage {
isEncryptionAvailable(): boolean;
getSelectedStorageBackend(): string;
encryptString(plainText: string): Buffer;
decryptString(encrypted: Buffer): string;
}
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 accountEpoch = 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;
}
const ACCOUNT_PARTITION_KEY_PATTERN = /^[0-9a-f]{64}$/;
const ACCOUNT_IDENTITY_MAX_LENGTH = 512;
function asAccountIdentity(value: unknown): string | null {
const identity = asString(value);
return identity && identity.length <= ACCOUNT_IDENTITY_MAX_LENGTH ? identity : null;
}
function accountPartitionKey(kind: 'user_id' | 'username', identity: string): string {
const normalized = kind === 'username' ? identity.toLocaleLowerCase('en-US') : identity;
return createHash('sha256').update(`makelore-learning:v1:${kind}:${normalized}`).digest('hex');
}
function jwtIdentity(accessToken: string): { kind: 'user_id' | 'username'; value: string } | null {
const parts = accessToken.split('.');
if (parts.length !== 3 || parts[1].length > 16_384) return null;
try {
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')) as unknown;
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
const claims = payload as Record<string, unknown>;
const userId = asAccountIdentity(claims.user_id);
if (userId) return { kind: 'user_id', value: userId };
const username = asAccountIdentity(claims.username);
return username ? { kind: 'username', value: username } : null;
} catch {
return null;
}
}
function deriveAccountPartitionKey(
payload: Pick<WorksSquareTokenPayload, 'user_id' | 'username'>,
accessToken: string,
): string | null {
const userId = asAccountIdentity(payload.user_id);
if (userId) return accountPartitionKey('user_id', userId);
const username = asAccountIdentity(payload.username);
if (username) return accountPartitionKey('username', username);
const legacyIdentity = jwtIdentity(accessToken);
return legacyIdentity
? accountPartitionKey(legacyIdentity.kind, legacyIdentity.value)
: null;
}
function assignCurrentSession(session: StoredWorksSquareSession | null): void {
const previousAccountKey = currentSession?.accountPartitionKey ?? null;
const nextAccountKey = session?.accountPartitionKey ?? null;
currentSession = session;
if (previousAccountKey !== nextAccountKey) accountEpoch += 1;
}
function expiresAtFromExpiresIn(expiresIn: unknown, nowMs = Date.now()): number | null {
const seconds = typeof expiresIn === 'number'
? expiresIn
: (typeof expiresIn === 'string' ? Number(expiresIn) : NaN);
return Number.isFinite(seconds) && seconds > 0 ? nowMs + seconds * 1000 : null;
}
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,
accountPartitionKey: typeof input.accountPartitionKey === 'string'
&& ACCOUNT_PARTITION_KEY_PATTERN.test(input.accountPartitionKey)
? input.accountPartitionKey
: deriveAccountPartitionKey({}, accessToken),
};
}
function parseTokenPayload(
payload: WorksSquareTokenPayload,
fallbackRefreshToken: string | null,
nowMs = Date.now(),
lastActiveAt = nowMs,
fallbackAccountPartitionKey: string | null = null,
): StoredWorksSquareSession {
const accessToken = asString(payload.access_token);
if (!accessToken) {
throw new Error('Auth refresh response did not include access_token');
}
return {
accessToken,
refreshToken: asString(payload.refresh_token) ?? fallbackRefreshToken,
tokenType: asString(payload.token_type) ?? 'Bearer',
expiresAt: expiresAtFromExpiresIn(payload.expires_in, nowMs),
lastActiveAt,
accountPartitionKey: fallbackAccountPartitionKey
?? deriveAccountPartitionKey(payload, accessToken),
};
}
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);
assignCurrentSession(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(
safeStorage: WorksSquareSecureStorage,
): Promise<WorksSquareSessionPersistence | null> {
const { default: Store } = await import('electron-store');
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_GATEWAY_URL
) {
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_GATEWAY_URL,
ciphertext: encrypted.toString('base64'),
});
},
};
}
export async function initializeWorksSquareSession(
options: {
persistence?: WorksSquareSessionPersistence;
secureStorage?: WorksSquareSecureStorage | null;
nowMs?: number;
} = {},
): Promise<WorksSquareSessionSnapshot | null> {
persistenceQueue = Promise.resolve();
credentialPersistenceBarrier = null;
startupRuntimeCleanupRequired = false;
const secureStorage = options.secureStorage ?? null;
sessionPersistenceFactory = options.persistence || !secureStorage
? null
: () => createElectronSessionPersistence(secureStorage);
try {
sessionPersistence = options.persistence
?? (sessionPersistenceFactory ? await sessionPersistenceFactory() : null);
} catch (error) {
logger.warn('[works-square-session] Failed to initialize secure session persistence', error);
sessionPersistence = null;
restoreStatus = 'unavailable';
startupRuntimeCleanupRequired = true;
assignCurrentSession(null);
credentialGeneration += 1;
refreshFlight = null;
return null;
}
startupRuntimeCleanupRequired = Boolean(sessionPersistence?.discardedInvalidRecord);
if (!sessionPersistence) {
restoreStatus = 'ready';
assignCurrentSession(null);
credentialGeneration += 1;
refreshFlight = null;
return null;
}
let restored: StoredWorksSquareSession | null;
let hadPersistedSession: boolean;
let persistedInput: WorksSquareSessionInput | null;
try {
const persisted = await sessionPersistence.load();
persistedInput = persisted;
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;
assignCurrentSession(null);
credentialGeneration += 1;
refreshFlight = null;
return null;
}
const nowMs = options.nowMs ?? Date.now();
if (restored && !isSessionIdle(restored, nowMs)) {
restoreStatus = 'ready';
assignCurrentSession(restored);
credentialGeneration += 1;
refreshFlight = null;
if (!persistedInput?.accountPartitionKey && restored.accountPartitionKey) {
await queuePersistence(restored);
}
return toPublicSnapshot(restored);
}
if (!hadPersistedSession) {
restoreStatus = 'ready';
assignCurrentSession(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;
try {
return JSON.parse(text) as unknown;
} catch {
return text;
}
}
export function storeWorksSquareSession(input: WorksSquareSessionInput): void {
const session = normalizeSession(input);
if (!session || isSessionIdle(session, Date.now())) {
clearWorksSquareSession();
return;
}
replaceCredentialSession(session);
}
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, 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 {
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 {
sessionListeners.add(listener);
return () => {
sessionListeners.delete(listener);
};
}
export function getWorksSquareSessionSnapshot(): WorksSquareSessionSnapshot | null {
const snapshot = toPublicSnapshot(currentSession);
return snapshot ? { ...snapshot } : null;
}
export function getWorksSquareAccountBinding(): WorksSquareAccountBinding | null {
const accountKey = currentSession?.accountPartitionKey;
if (!accountKey || !currentSession || isSessionIdle(currentSession, Date.now())) return null;
return Object.freeze({ accountKey, epoch: accountEpoch });
}
export function isCurrentWorksSquareAccountBinding(binding: WorksSquareAccountBinding): boolean {
const current = getWorksSquareAccountBinding();
return Boolean(current
&& current.accountKey === binding.accountKey
&& current.epoch === binding.epoch);
}
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;
let persistedInput: WorksSquareSessionInput | null;
try {
const persisted = await sessionPersistence.load();
persistedInput = persisted;
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';
assignCurrentSession(restored);
credentialGeneration += 1;
refreshFlight = null;
if (!persistedInput?.accountPartitionKey && restored.accountPartitionKey) {
await queuePersistence(restored);
}
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; requestTimeoutMs?: number } = {},
): Promise<string | null> {
if (!session.refreshToken) return null;
const fetchImpl = options.fetchImpl ?? proxyAwareFetch;
const nowMs = options.nowMs ?? Date.now();
const body = JSON.stringify({ refresh_token: session.refreshToken });
const { response, payload } = await runWithDeadline(async (signal) => {
const response = await fetchImpl(
`${WORKS_SQUARE_CONFIG.apiBaseUrl.replace(/\/+$/, '')}/api/auth/refresh`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
signal,
},
);
const payload = response.ok ? await readResponsePayload(response) : null;
return { response, payload };
}, options.requestTimeoutMs ?? WORKS_SQUARE_AUTH_REQUEST_TIMEOUT_MS);
if (!response.ok) {
logger.warn('[works-square-session] Refresh failed', { status: response.status });
if ((response.status === 400 || response.status === 401) && sessionStillMatches(session, generation)) {
await clearWorksSquareSessionPersisted('terminal');
}
return null;
}
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
logger.warn('[works-square-session] Refresh returned an invalid payload');
return null;
}
if (!sessionStillMatches(session, generation)) {
return null;
}
const nextSession = parseTokenPayload(
payload as WorksSquareTokenPayload,
session.refreshToken,
nowMs,
currentSession!.lastActiveAt,
session.accountPartitionKey,
);
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;
requestTimeoutMs?: number;
} = {},
): 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 (!session.refreshToken) {
await clearWorksSquareSessionPersisted('terminal');
return null;
}
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;
accountEpoch += 1;
refreshFlight = null;
sessionPersistence = null;
sessionPersistenceFactory = null;
persistenceQueue = Promise.resolve();
credentialPersistenceBarrier = null;
restoreStatus = 'ready';
startupRuntimeCleanupRequired = false;
sessionListeners.clear();
}