From 2c58cbeb7f82444de6388ae02b2c65da8fddd3f5 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 7 Sep 2026 18:13:18 -0400 Subject: [PATCH] refactor(page): parse the file list with its schema instead of a sanitizer The desktop wrapper already types and redacts the search result; the page strips unknown fields with the contract schema like readDir does, and the last shell-era sanitizer module goes. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- .../src/mobile-web-file-host-reads.test.ts | 2 +- .../src/mobile-web-file-request-client.ts | 19 ++++++-- .../mobile-web/file-list-presentation.ts | 46 ------------------- 3 files changed, 17 insertions(+), 50 deletions(-) delete mode 100644 src/shared/mobile-web/file-list-presentation.ts diff --git a/src/mobile-web/src/mobile-web-file-host-reads.test.ts b/src/mobile-web/src/mobile-web-file-host-reads.test.ts index ca7bab8e151..048f401bd3a 100644 --- a/src/mobile-web/src/mobile-web-file-host-reads.test.ts +++ b/src/mobile-web/src/mobile-web-file-host-reads.test.ts @@ -165,7 +165,7 @@ describe('page-safe file listing and text', () => { } }) respond({ - files: [{ relativePath: 'docs/report.md', kind: 'text' }], + files: [{ relativePath: 'docs/report.md', basename: 'report.md', kind: 'text' }], totalCount: 1, truncated: false, futureField: true diff --git a/src/mobile-web/src/mobile-web-file-request-client.ts b/src/mobile-web/src/mobile-web-file-request-client.ts index 8c42bdfd964..ff763873f1e 100644 --- a/src/mobile-web/src/mobile-web-file-request-client.ts +++ b/src/mobile-web/src/mobile-web-file-request-client.ts @@ -1,9 +1,9 @@ import { z } from 'zod' -import { sanitizeListResult } from '../../shared/mobile-web/file-list-presentation' import { projectMobileWebHostFileContent } from './mobile-web-host-file-content' import { MOBILE_WEB_FILE_CHUNK_MAX_BYTES, MobileWebFileListPayloadSchema, + MobileWebFileListResultSchema, MobileWebFileOpenPayloadSchema, MobileWebFileReadPayloadSchema, MobileWebFileSearchPayloadSchema, @@ -47,6 +47,19 @@ const WriteResultSchema = z.union([ z.object({ outcome: z.enum(['conflict', 'too_large']) }) ]) +// The desktop already types and redacts the list; the page only strips fields it does not know. +function projectListResult( + result: unknown, + workspaceId: string, + limit: number +): MobileWebFileListResult { + const parsed = MobileWebFileListResultSchema.omit({ workspaceId: true }).strip().safeParse(result) + if (!parsed.success || parsed.data.files.length > limit) { + throw new MobileWebBridgeClientError('invalid_message', false) + } + return { ...parsed.data, workspaceId } +} + export class MobileWebFileRequestClient extends MobileWebFileReadClient { list( payload: MobileWebFileListPayload, @@ -59,7 +72,7 @@ export class MobileWebFileRequestClient extends MobileWebFileReadClient { 'mobileWeb.files.searchPaths', payload.workspaceId, { query: '', limit: payload.limit }, - (result) => sanitizeListResult(result, payload.workspaceId, undefined, payload.limit), + (result) => projectListResult(result, payload.workspaceId, payload.limit), options ) } @@ -75,7 +88,7 @@ export class MobileWebFileRequestClient extends MobileWebFileReadClient { 'mobileWeb.files.searchPaths', payload.workspaceId, { query: payload.query, limit: payload.limit }, - (result) => sanitizeListResult(result, payload.workspaceId, undefined, payload.limit), + (result) => projectListResult(result, payload.workspaceId, payload.limit), options ) } diff --git a/src/shared/mobile-web/file-list-presentation.ts b/src/shared/mobile-web/file-list-presentation.ts deleted file mode 100644 index 6a47943105d..00000000000 --- a/src/shared/mobile-web/file-list-presentation.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - MobileWebFileEntrySchema, - MobileWebFileListResultSchema, - type MobileWebFileEntry, - type MobileWebFileListResult -} from './file-operation-contract' -import { MobileWebBrokerError } from './bridge-operation-error' - -export function sanitizeListResult( - result: unknown, - pageWorkspaceId: string, - hostWorkspaceId: string | undefined, - limit: number -): MobileWebFileListResult { - if (!isRecord(result) || result.worktree !== hostWorkspaceId || !Array.isArray(result.files)) { - throw new MobileWebBrokerError('host_error') - } - const files = result.files.slice(0, limit).flatMap((value): MobileWebFileEntry[] => { - if (!isRecord(value) || typeof value.relativePath !== 'string') { - return [] - } - const parsed = MobileWebFileEntrySchema.safeParse({ - relativePath: value.relativePath, - basename: value.relativePath.split('/').at(-1)?.slice(0, 255), - kind: value.kind === 'binary' ? 'binary' : 'text' - }) - return parsed.success ? [parsed.data] : [] - }) - const totalCount = - typeof result.totalCount === 'number' && - Number.isSafeInteger(result.totalCount) && - result.totalCount >= 0 - ? result.totalCount - : result.files.length - return MobileWebFileListResultSchema.parse({ - workspaceId: pageWorkspaceId, - files, - totalCount, - truncated: - result.truncated === true || result.files.length > files.length || totalCount > files.length - }) -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) -}