fix(memory): bound cloud session cache

This commit is contained in:
m4air
2026-09-19 16:31:54 -07:00
parent af7113ac78
commit 4107cd5efa
2 changed files with 35 additions and 5 deletions
@@ -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')
@@ -55,6 +55,19 @@ type CachedOrcaCloudSession = {
}
const memorySessions = new Map<string, CachedOrcaCloudSession>()
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
}