mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 16:02:37 +00:00
fix: address review findings (#2703)
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { sanitizeRepoIcon } from './repo-icon'
|
||||
|
||||
describe('sanitizeRepoIcon', () => {
|
||||
it('accepts lucide, emoji, and supported image icons', () => {
|
||||
expect(sanitizeRepoIcon({ type: 'lucide', name: 'Folder' })).toEqual({
|
||||
type: 'lucide',
|
||||
name: 'Folder'
|
||||
})
|
||||
expect(sanitizeRepoIcon({ type: 'emoji', emoji: '🚀' })).toEqual({
|
||||
type: 'emoji',
|
||||
emoji: '🚀'
|
||||
})
|
||||
expect(
|
||||
sanitizeRepoIcon({
|
||||
type: 'image',
|
||||
src: 'https://github.com/stablyai.png?size=64',
|
||||
source: 'github',
|
||||
label: 'stablyai/orca'
|
||||
})
|
||||
).toEqual({
|
||||
type: 'image',
|
||||
src: 'https://github.com/stablyai.png?size=64',
|
||||
source: 'github',
|
||||
label: 'stablyai/orca'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps null as an explicit reset', () => {
|
||||
expect(sanitizeRepoIcon(null)).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects unsupported image urls and oversized payloads', () => {
|
||||
expect(
|
||||
sanitizeRepoIcon({
|
||||
type: 'image',
|
||||
src: 'javascript:alert(1)',
|
||||
source: 'favicon'
|
||||
})
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
sanitizeRepoIcon({
|
||||
type: 'image',
|
||||
src: `data:image/png;base64,${'a'.repeat(401 * 1024)}`,
|
||||
source: 'upload'
|
||||
})
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
export type RepoIconImageSource = 'upload' | 'favicon' | 'github'
|
||||
|
||||
export type RepoIcon =
|
||||
| { type: 'lucide'; name: string }
|
||||
| { type: 'emoji'; emoji: string }
|
||||
| { type: 'image'; src: string; source: RepoIconImageSource; label?: string }
|
||||
|
||||
export const MAX_REPO_ICON_UPLOAD_BYTES = 256 * 1024
|
||||
export const MAX_REPO_ICON_DATA_URL_LENGTH = 400 * 1024
|
||||
|
||||
const LUCIDE_ICON_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9]*$/
|
||||
const IMAGE_SOURCE_IDS = new Set(['upload', 'favicon', 'github'])
|
||||
|
||||
function isSupportedImageSrc(src: string): boolean {
|
||||
return (
|
||||
/^https:\/\/[^\s]+$/i.test(src) ||
|
||||
/^data:image\/(?:png|svg\+xml);base64,[A-Za-z0-9+/=\s]+$/i.test(src)
|
||||
)
|
||||
}
|
||||
|
||||
export function sanitizeRepoIcon(value: unknown): RepoIcon | null | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (value === null) {
|
||||
return null
|
||||
}
|
||||
if (!value || typeof value !== 'object') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const candidate = value as Record<string, unknown>
|
||||
if (candidate.type === 'lucide') {
|
||||
const name = typeof candidate.name === 'string' ? candidate.name.trim() : ''
|
||||
if (!LUCIDE_ICON_NAME_PATTERN.test(name) || name.length > 40) {
|
||||
return undefined
|
||||
}
|
||||
return { type: 'lucide', name }
|
||||
}
|
||||
|
||||
if (candidate.type === 'emoji') {
|
||||
const emoji = typeof candidate.emoji === 'string' ? candidate.emoji.trim() : ''
|
||||
if (!emoji || emoji.length > 16) {
|
||||
return undefined
|
||||
}
|
||||
return { type: 'emoji', emoji }
|
||||
}
|
||||
|
||||
if (candidate.type === 'image') {
|
||||
const src = typeof candidate.src === 'string' ? candidate.src.trim() : ''
|
||||
const source = typeof candidate.source === 'string' ? candidate.source : ''
|
||||
if (!IMAGE_SOURCE_IDS.has(source) || src.length > MAX_REPO_ICON_DATA_URL_LENGTH) {
|
||||
return undefined
|
||||
}
|
||||
if (!isSupportedImageSrc(src)) {
|
||||
return undefined
|
||||
}
|
||||
const label = typeof candidate.label === 'string' ? candidate.label.trim().slice(0, 80) : ''
|
||||
return {
|
||||
type: 'image',
|
||||
src,
|
||||
source: source as RepoIconImageSource,
|
||||
...(label ? { label } : {})
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import type { TaskProvider } from './task-providers'
|
||||
import type { FeatureTipId } from './feature-tips'
|
||||
import type { GitBranchChangeStatus } from './git-status-types'
|
||||
import type { KeybindingOverrides, TerminalShortcutPolicy } from './keybindings'
|
||||
import type { RepoIcon } from './repo-icon'
|
||||
|
||||
// Re-exported for backward compat with renderer call sites that import
|
||||
// `WorkspaceCreateTelemetrySource` from '../../../shared/types'.
|
||||
@@ -75,6 +76,7 @@ export type Repo = {
|
||||
path: string
|
||||
displayName: string
|
||||
badgeColor: string
|
||||
repoIcon?: RepoIcon | null
|
||||
addedAt: number
|
||||
kind?: RepoKind
|
||||
gitUsername?: string
|
||||
|
||||
Reference in New Issue
Block a user