refactor(profiles): validate untrusted profile JSON instead of asserting it

Removes the last introduced cast and the unsound pre-existing ones in the
file this change already touches.

The test cast is gone because narrowing normalizeLoadedProjectCatalog to the
two fields it reads made `{}` assignable on its own.

arrayOrEmpty and recordOrEmpty checked the shape and then asserted the
element type, which is the same "declared type is a promise the data does not
keep" problem this change exists to fix. Array.isArray already narrows on its
own, and a generic isRecord narrows the value part, so both assertions delete
outright. JSON.parse returns any, so annotating the binding beats asserting
its result.

The two remaining `as const` in project-host-setup-actions are untouched and
deliberate: a literal assertion narrows a type rather than overriding it.
This commit is contained in:
Jinjing
2026-08-27 09:49:04 -07:00
parent 157d01c69e
commit e507b4453b
2 changed files with 9 additions and 12 deletions
@@ -3,7 +3,6 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from '
import { homedir } from 'node:os'
import { dirname } from 'node:path'
import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../shared/constants'
import type { ExecutionHostId } from '../../shared/execution-host'
import { projectHostSetupProjectionFromRepos } from '../../shared/project-host-setup-projection'
import {
normalizeProjectHostSetupRows,
@@ -20,16 +19,16 @@ import { getOrcaProfileDataFile } from './profile-index-store'
export type TransferProfileState = PersistedState
function isRecord(value: unknown): value is Record<string, unknown> {
function isRecord<T>(value: unknown): value is Record<string, T> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function arrayOrEmpty<T>(value: unknown): T[] {
return Array.isArray(value) ? (value as T[]) : []
return Array.isArray(value) ? value : []
}
function recordOrEmpty<T>(value: unknown): Record<string, T> {
return isRecord(value) ? (value as Record<string, T>) : {}
return isRecord<T>(value) ? value : {}
}
export function readProfileState(profileId: string, userDataPath: string): TransferProfileState {
@@ -38,7 +37,7 @@ export function readProfileState(profileId: string, userDataPath: string): Trans
if (!existsSync(dataFile)) {
return structuredClone(defaults)
}
const parsed = JSON.parse(readFileSync(dataFile, 'utf-8')) as Partial<PersistedState>
const parsed: Partial<PersistedState> = JSON.parse(readFileSync(dataFile, 'utf-8'))
return rebuildRepoBackedProjectState({
...defaults,
...parsed,
@@ -67,17 +66,15 @@ export function readProfileState(profileId: string, userDataPath: string): Trans
ui: isRecord(parsed.ui) ? { ...defaults.ui, ...parsed.ui } : defaults.ui,
githubCache: isRecord(parsed.githubCache)
? {
pr: recordOrEmpty((parsed.githubCache as PersistedState['githubCache']).pr),
issue: recordOrEmpty((parsed.githubCache as PersistedState['githubCache']).issue)
pr: recordOrEmpty(parsed.githubCache.pr),
issue: recordOrEmpty(parsed.githubCache.issue)
}
: defaults.githubCache,
workspaceSession: isRecord(parsed.workspaceSession)
? { ...getDefaultWorkspaceSession(), ...parsed.workspaceSession }
: defaults.workspaceSession,
workspaceSessionsByHostId: isRecord(parsed.workspaceSessionsByHostId)
? (parsed.workspaceSessionsByHostId as Partial<
Record<ExecutionHostId, WorkspaceSessionState>
>)
workspaceSessionsByHostId: isRecord<WorkspaceSessionState>(parsed.workspaceSessionsByHostId)
? parsed.workspaceSessionsByHostId
: {},
sshTargets: arrayOrEmpty(parsed.sshTargets),
sshRemotePtyLeases: arrayOrEmpty(parsed.sshRemotePtyLeases),
@@ -60,7 +60,7 @@ describe('normalizeLoadedProjectCatalog', () => {
})
it('tolerates missing or non-array collections', () => {
const result = normalizeLoadedProjectCatalog({} as PersistedState, vi.fn())
const result = normalizeLoadedProjectCatalog({}, vi.fn())
expect(result.projects).toEqual([])
expect(result.projectHostSetups).toEqual([])
})