Files
hermes-agent/apps/desktop/electron/oauth-session-response.ts
T
brooklyn! 8d092c2f71 fix(desktop): validate remote OAuth through ticket minting
Handle truncated OAuth responses and keep confirmed auth recovery stable. Preserve the current attempt guard before latching failures; the older pre-guard latch proposal is not carried forward.

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Co-authored-by: Ugo Enyioha <ugo.enyioha@outlook.com>
Co-authored-by: Bartok9 <259807879+Bartok9@users.noreply.github.com>
2026-09-11 07:14:55 -05:00

83 lines
2.1 KiB
TypeScript

import { Buffer } from 'node:buffer'
import { httpStatusError } from './api-transport'
// Response-stream extraction based on Bartok9's #99135 (original issue #72530).
export interface OauthResponseLike {
on(event: 'data', cb: (chunk: Buffer) => void): void
on(event: 'error', cb: (error: Error) => void): void
on(event: 'end', cb: () => void): void
statusCode?: number
headers: Record<string, string | string[] | undefined>
}
export interface WireOauthResponseOptions {
url: string
isTimedOut: () => boolean
clearTimer: () => void
resolve: (value: unknown) => void
reject: (error: Error) => void
}
export function wireOauthSessionResponse(res: OauthResponseLike, opts: WireOauthResponseOptions): void {
const { url, isTimedOut, clearTimer, resolve, reject } = opts
const chunks: Buffer[] = []
let settled = false
res.on('data', chunk => {
if (!settled && !isTimedOut()) {
chunks.push(Buffer.from(chunk))
}
})
// Electron emits post-header loader failures on the response, not the request.
// Keep the listener after settlement: a late error must not become uncaught.
res.on('error', error => {
if (settled || isTimedOut()) {
return
}
settled = true
chunks.length = 0
clearTimer()
reject(error)
})
res.on('end', () => {
if (settled || isTimedOut()) {
return
}
settled = true
clearTimer()
const text = Buffer.concat(chunks).toString('utf8')
chunks.length = 0
const statusCode = res.statusCode || 500
if (statusCode >= 400) {
reject(httpStatusError(statusCode, text))
return
}
if (!text) {
resolve(null)
return
}
const looksHtml = /^\s*<(?:!doctype|html)/i.test(text)
const contentType = String(res.headers['content-type'] || res.headers['Content-Type'] || '')
if (looksHtml || contentType.includes('text/html')) {
reject(new Error(`Expected JSON from ${url} but got HTML (status ${statusCode}).`))
return
}
try {
resolve(JSON.parse(text))
} catch {
reject(new Error(`Invalid JSON from ${url} (status ${statusCode}): ${text.slice(0, 200)}`))
}
})
}