mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(desktop): harden native token store handling
This commit is contained in:
parent
61d8be5cb0
commit
6cb459af9e
2 changed files with 45 additions and 2 deletions
|
|
@ -232,6 +232,29 @@ test('a corrupt store file loads as signed out instead of throwing', () => {
|
|||
assert.deepEqual(disk.logs, [])
|
||||
})
|
||||
|
||||
test('an array store file loads as signed out instead of throwing', () => {
|
||||
const disk = createFakeDisk('[]')
|
||||
|
||||
assert.equal(loadNativeTokenSet(GATEWAY, disk.io), null)
|
||||
assert.deepEqual(disk.logs, [])
|
||||
})
|
||||
|
||||
test('an array store file is replaced by a real map rather than swallowing the write', () => {
|
||||
const disk = createFakeDisk('[]')
|
||||
|
||||
persistNativeTokenSet(GATEWAY, TOKENS, disk.io)
|
||||
|
||||
const written = JSON.parse(disk.fileText()!)
|
||||
|
||||
// Assigning store[baseUrl] on an array sets a non-index property, which
|
||||
// JSON.stringify drops — the write would report success and the tokens would
|
||||
// be gone on the next launch.
|
||||
assert.equal(Array.isArray(written), false)
|
||||
assert.ok(written[GATEWAY], 'the gateway entry must survive serialization')
|
||||
// And it really does come back after a restart.
|
||||
assert.deepEqual(loadNativeTokenSet(GATEWAY, createFakeDisk(disk.fileText()).io), TOKENS)
|
||||
})
|
||||
|
||||
test('a corrupt decrypted blob is reported and loads as signed out', () => {
|
||||
const disk = createFakeDisk(JSON.stringify({ [GATEWAY]: { encoding: 'safeStorage', value: 'bm90LWpzb24=' } }))
|
||||
|
||||
|
|
@ -272,6 +295,19 @@ test('an unwritable store file is logged rather than thrown', () => {
|
|||
assert.match(disk.logs[0], /failed to persist tokens: EACCES/)
|
||||
})
|
||||
|
||||
test('a non-Error write failure keeps its detail in the log', () => {
|
||||
const disk = createFakeDisk(null, {
|
||||
writeStoreText: () => {
|
||||
throw 'disk went away'
|
||||
}
|
||||
})
|
||||
|
||||
// `(error as Error).message` on a thrown string reads as undefined and loses
|
||||
// the only diagnostic there was.
|
||||
assert.doesNotThrow(() => persistNativeTokenSet(GATEWAY, TOKENS, disk.io))
|
||||
assert.equal(disk.logs[0], '[native-oauth] failed to persist tokens: disk went away')
|
||||
})
|
||||
|
||||
test('an unusable keychain fails the write loudly and writes nothing', () => {
|
||||
const existing = createFakeDisk()
|
||||
|
||||
|
|
|
|||
|
|
@ -48,12 +48,17 @@ export interface NativeTokenStoreIo {
|
|||
/**
|
||||
* baseUrl → encrypted payload. A missing, unreadable, or hand-mangled store
|
||||
* reads as empty rather than throwing: a failed *read* falls to the next rung.
|
||||
*
|
||||
* Arrays are rejected alongside every other non-object shape: assigning
|
||||
* store[baseUrl] on an array would set a non-index property, which
|
||||
* JSON.stringify drops on the way back out — the write would look like it
|
||||
* succeeded and the tokens would be gone on the next launch.
|
||||
*/
|
||||
function readStore(io: NativeTokenStoreIo): Record<string, any> {
|
||||
try {
|
||||
const parsed = JSON.parse(io.readStoreText())
|
||||
|
||||
return parsed && typeof parsed === 'object' ? parsed : {}
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
|
|
@ -79,7 +84,9 @@ export function persistNativeTokenSet(baseUrl: string, tokens: NativeTokenSet |
|
|||
try {
|
||||
io.writeStoreText(JSON.stringify(store))
|
||||
} catch (error) {
|
||||
io.rememberLog?.(`[native-oauth] failed to persist tokens: ${(error as Error).message}`)
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
|
||||
io.rememberLog?.(`[native-oauth] failed to persist tokens: ${detail}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue