From e507b4453bd0359cedb646e77a04937bfdac3e6d Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:49:04 -0700 Subject: [PATCH] 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. --- .../profile-project-state-file.ts | 19 ++++++++----------- .../normalize-loaded-project-catalog.test.ts | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/orca-profiles/profile-project-state-file.ts b/src/main/orca-profiles/profile-project-state-file.ts index de956e33018..d4f291a09e4 100644 --- a/src/main/orca-profiles/profile-project-state-file.ts +++ b/src/main/orca-profiles/profile-project-state-file.ts @@ -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 { +function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function arrayOrEmpty(value: unknown): T[] { - return Array.isArray(value) ? (value as T[]) : [] + return Array.isArray(value) ? value : [] } function recordOrEmpty(value: unknown): Record { - return isRecord(value) ? (value as Record) : {} + return isRecord(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 + const parsed: Partial = 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 - >) + workspaceSessionsByHostId: isRecord(parsed.workspaceSessionsByHostId) + ? parsed.workspaceSessionsByHostId : {}, sshTargets: arrayOrEmpty(parsed.sshTargets), sshRemotePtyLeases: arrayOrEmpty(parsed.sshRemotePtyLeases), diff --git a/src/main/persistence/loading-store/normalize-loaded-project-catalog.test.ts b/src/main/persistence/loading-store/normalize-loaded-project-catalog.test.ts index 6371c9aa933..8a535b41919 100644 --- a/src/main/persistence/loading-store/normalize-loaded-project-catalog.test.ts +++ b/src/main/persistence/loading-store/normalize-loaded-project-catalog.test.ts @@ -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([]) })