export interface StoreEntry { path: string; size: number; uploadedAt?: Date; } /** * Minimal object-store abstraction. Paths are bucket-relative, use `/` separators, * and never start with `/`. */ export interface BucketStore { /** Resolves null when the object does not exist. */ get(path: string): Promise; /** Creates or overwrites. */ put(path: string, bytes: Uint8Array, contentType?: string): Promise; /** * Appends bytes to an existing object, creating it if missing. Not atomic on every * backend: a crash mid-append may leave a truncated final line, so readers must * tolerate a trailing partial line. */ append(path: string, bytes: Uint8Array): Promise; /** * Lists files whose path starts with `prefix`, sorted by path. The prefix must be `''` * (the whole store) or end with `/`, so it always matches within a directory. */ list(prefix: string): Promise; /** Deletes objects; missing ones are ignored. */ delete(paths: string[]): Promise; /** Server-side copy. Rejects if `src` does not exist. */ copy(src: string, dst: string): Promise; } const SEGMENT_RE = /^[A-Za-z0-9._-]+$/; export function assertValidPath(path: string): void { if (path.length === 0 || path.length > 512) throw new Error(`Invalid path: ${path}`); if (path.startsWith('/') || path.endsWith('/')) throw new Error(`Invalid path: ${path}`); if (path.endsWith('.tmp')) throw new Error(`Invalid path: ${path}`); for (const seg of path.split('/')) { if (seg === '' || seg === '.' || seg === '..' || !SEGMENT_RE.test(seg)) throw new Error(`Invalid path: ${path}`); } } /** Validates a `list` prefix: either `''` or a valid path followed by `/`. */ export function assertValidPrefix(prefix: string): void { if (prefix === '') return; if (!prefix.endsWith('/')) throw new Error(`Invalid prefix: ${prefix}`); assertValidPath(prefix.slice(0, -1)); } export const encodeText = (s: string): Uint8Array => new TextEncoder().encode(s); export const decodeText = (b: Uint8Array): string => new TextDecoder().decode(b);