import type { UserFile } from '@/executor/types' export type UserFileLike = Pick & Partial> /** * Fields exposed for UserFile objects in UI (tag dropdown) and logs. * Internal fields like 'key' and 'context' are not exposed. */ export const USER_FILE_DISPLAY_FIELDS = ['id', 'name', 'url', 'size', 'type', 'base64'] as const export type UserFileDisplayField = (typeof USER_FILE_DISPLAY_FIELDS)[number] /** * Checks if a value matches the minimal UserFile shape. */ export function isUserFile(value: unknown): value is UserFileLike { if (!value || typeof value !== 'object') { return false } const candidate = value as Record return ( typeof candidate.id === 'string' && typeof candidate.key === 'string' && typeof candidate.url === 'string' && typeof candidate.name === 'string' ) } /** * Checks if a value matches the full UserFile metadata shape. */ export function isUserFileWithMetadata(value: unknown): value is UserFile { if (!isUserFile(value)) { return false } const candidate = value as Record return typeof candidate.size === 'number' && typeof candidate.type === 'string' } /** * Finds storage keys for UserFile objects embedded in a value. */ export function collectUserFileKeys(value: unknown): string[] { const keys = new Set() collectUserFileKeysInto(value, keys, new WeakSet()) return Array.from(keys) } function collectUserFileKeysInto(value: unknown, keys: Set, seen: WeakSet): void { if (!value || typeof value !== 'object') { return } if (seen.has(value)) { return } seen.add(value) if (isUserFileWithMetadata(value)) { keys.add(value.key) return } if (Array.isArray(value)) { for (const item of value) { collectUserFileKeysInto(item, keys, seen) } return } for (const item of Object.values(value)) { collectUserFileKeysInto(item, keys, seen) } } /** * Checks if a value matches the display-safe UserFile metadata shape after internal fields are stripped. */ export function isUserFileDisplayMetadata(value: unknown): value is Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { return false } const candidate = value as Record const url = typeof candidate.url === 'string' ? candidate.url : '' return ( typeof candidate.id === 'string' && typeof candidate.name === 'string' && url.length > 0 && typeof candidate.size === 'number' && typeof candidate.type === 'string' && (candidate.id.startsWith('file_') || url.includes('/api/files/serve/')) ) } /** * Filters a UserFile object to only include display fields. * Used for both UI display and log sanitization. */ export function filterUserFileForDisplay(data: Record): Record { const filtered: Record = {} for (const field of USER_FILE_DISPLAY_FIELDS) { if (field in data) { filtered[field] = data[field] } } return filtered } /** * Extracts base64 content from either a raw base64 string or a UserFile object. * Useful for tools that accept file input in either format. * @returns The base64 string, or undefined if not found */ export function extractBase64FromFileInput( input: string | UserFileLike | null | undefined ): string | undefined { if (typeof input === 'string') { return input } if (input?.base64) { return input.base64 } return undefined }