diff --git a/src/main/orca-profiles/profile-cloud-session-store.test.ts b/src/main/orca-profiles/profile-cloud-session-store.test.ts index ca32f0529f1..2a7153c901d 100644 --- a/src/main/orca-profiles/profile-cloud-session-store.test.ts +++ b/src/main/orca-profiles/profile-cloud-session-store.test.ts @@ -136,6 +136,17 @@ describe('Orca cloud session store', () => { } }) + it('bounds memory-session cache churn', async () => { + const store = await loadSessionStore() + const session = makeSession() + + for (let index = 0; index < store.MAX_MEMORY_CLOUD_SESSIONS + 4; index += 1) { + store.saveOrcaCloudSession(`profile-${index}`, userDataPath, session) + } + + expect(store.getOrcaCloudMemorySessionCountForTests()).toBe(store.MAX_MEMORY_CLOUD_SESSIONS) + }) + it('writes explicit dev plaintext only when the dev escape hatch is enabled', async () => { safeStorageMock.isEncryptionAvailable.mockReturnValue(false) vi.stubEnv('ORCA_CLOUD_ALLOW_PLAINTEXT_SESSION', '1') diff --git a/src/main/orca-profiles/profile-cloud-session-store.ts b/src/main/orca-profiles/profile-cloud-session-store.ts index d1334d96b6b..4741a471544 100644 --- a/src/main/orca-profiles/profile-cloud-session-store.ts +++ b/src/main/orca-profiles/profile-cloud-session-store.ts @@ -55,6 +55,19 @@ type CachedOrcaCloudSession = { } const memorySessions = new Map() +export const MAX_MEMORY_CLOUD_SESSIONS = 64 + +function rememberMemorySession(key: string, session: CachedOrcaCloudSession): void { + memorySessions.delete(key) + memorySessions.set(key, session) + while (memorySessions.size > MAX_MEMORY_CLOUD_SESSIONS) { + const oldest = memorySessions.keys().next() + if (oldest.done || oldest.value === key) { + break + } + memorySessions.delete(oldest.value) + } +} function sessionCacheKey(profileId: string, userDataPath: string): string { return `${userDataPath}\0${profileId}` @@ -119,7 +132,7 @@ export function saveOrcaCloudSession( ciphertext: safeStorage.encryptString(JSON.stringify(session)).toString('base64') } writeSecureJsonFile(getOrcaCloudSessionPath(profileId, userDataPath), encrypted) - memorySessions.set(cacheKey, { session, persistence: 'encrypted' }) + rememberMemorySession(cacheKey, { session, persistence: 'encrypted' }) return 'encrypted' } @@ -131,13 +144,13 @@ export function saveOrcaCloudSession( session } writeSecureJsonFile(getOrcaCloudSessionPath(profileId, userDataPath), plaintext) - memorySessions.set(cacheKey, { session, persistence: 'dev-plaintext' }) + rememberMemorySession(cacheKey, { session, persistence: 'dev-plaintext' }) return 'dev-plaintext' } // Why: Orca account refresh tokens must not silently fall back to plaintext // in production. Memory-only keeps cloud features usable until restart. - memorySessions.set(cacheKey, { session, persistence: 'memory-only' }) + rememberMemorySession(cacheKey, { session, persistence: 'memory-only' }) return 'memory-only' } @@ -177,6 +190,8 @@ export function readOrcaCloudSession( const cacheKey = sessionCacheKey(profileId, userDataPath) const memorySession = memorySessions.get(cacheKey) if (memorySession) { + memorySessions.delete(cacheKey) + memorySessions.set(cacheKey, memorySession) return { status: 'found', session: memorySession.session, @@ -209,14 +224,14 @@ export function readOrcaCloudSession( if (!isOrcaCloudSession(session)) { return { status: 'decrypt-failed', persistence: 'none', error: 'Invalid saved session.' } } - memorySessions.set(cacheKey, { session, persistence: 'encrypted' }) + rememberMemorySession(cacheKey, { session, persistence: 'encrypted' }) return { status: 'found', session, persistence: 'encrypted' } } if (parsed.format === 'dev-plaintext-v1' && allowsPlaintextOrcaCloudSession()) { if (!isOrcaCloudSession(parsed.session)) { return { status: 'decrypt-failed', persistence: 'none', error: 'Invalid saved session.' } } - memorySessions.set(cacheKey, { session: parsed.session, persistence: 'dev-plaintext' }) + rememberMemorySession(cacheKey, { session: parsed.session, persistence: 'dev-plaintext' }) return { status: 'found', session: parsed.session, persistence: 'dev-plaintext' } } return { status: 'decrypt-failed', persistence: 'none', error: 'Unsafe session format.' } @@ -240,3 +255,7 @@ export function clearOrcaCloudSession(profileId: string, userDataPath: string): memorySessions.delete(sessionCacheKey(profileId, userDataPath)) rmSync(getOrcaCloudSessionPath(profileId, userDataPath), { force: true }) } + +export function getOrcaCloudMemorySessionCountForTests(): number { + return memorySessions.size +}