mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
refactor(api): shrink media-references surface and reuse maintained libs
- /api/file module now exports only the SessionMediaReferences plugin contribution; MIME and range policy helpers are module-private and exercised entirely through the registered route. - Replace the hand-rolled media extension table with mime-types (served categories image/video/audio, excluding image/svg+xml) and the hand-rolled Range parser with range-parser; keep the fail-closed workspace containment policy. - Spec rewritten as route-level behavior tests (12 cases) covering the same branches; Agent Note facts updated in the same change.
This commit is contained in:
@@ -10,121 +10,67 @@
|
||||
* - its canonical location must lie inside a registered workspace root
|
||||
* (`ctx.workspaceRegistry`); no other directory is readable;
|
||||
* - the file must exist and be a regular file;
|
||||
* - its extension must name an allowlisted media type (media bytes are never
|
||||
* sniffed here: the allowlist keeps non-media content out, and browsers
|
||||
* already reject corrupt image payloads);
|
||||
* - responses are private, uncached, sniff-proof, and support HTTP range
|
||||
* requests so `<video>`/`<audio>` can seek without buffering the file.
|
||||
* - its MIME type (resolved by `mime-types`) must belong to the served
|
||||
* categories image/video/audio, excluding `image/svg+xml`; media bytes are
|
||||
* never sniffed here, and a corrupt payload fails in the browser;
|
||||
* - responses are private, uncached, sniff-proof, and stream with HTTP range
|
||||
* support (parsed by `range-parser`) so `<video>`/`<audio>` can seek.
|
||||
*
|
||||
* The route is deliberately presentational: it never writes and follows no
|
||||
* redirects, returning 400/403/404/415/416 instead of falling back to any
|
||||
* other file-serving behavior.
|
||||
*
|
||||
* The package entry imports only `SessionMediaReferences`; the remaining
|
||||
* module exports exist for same-package unit tests and are not part of the
|
||||
* package's public API.
|
||||
* Only the plugin contribution below is exported: package-internal policy
|
||||
* helpers stay module-private and are exercised through the registered route.
|
||||
* @module @deepseek-ai/dsh-api-session-controller/media-references
|
||||
*/
|
||||
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { realpath, stat } from 'node:fs/promises'
|
||||
import { extname, isAbsolute, sep } from 'node:path'
|
||||
import { isAbsolute, sep } from 'node:path'
|
||||
import { Readable } from 'node:stream'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
// Cordis `ctx.connection` typing and the fetch-route contract.
|
||||
import type {} from '@deepseek-ai/dsh-client-connection'
|
||||
// Cordis `ctx.workspaceRegistry` typing.
|
||||
import type {} from '@deepseek-ai/dsh-workspace'
|
||||
import mime from 'mime-types'
|
||||
import rangeParser from 'range-parser'
|
||||
|
||||
/** Registered-workspace view the route reads; see the `workspaceRegistry` service. */
|
||||
export interface MediaReferenceRegistry {
|
||||
/** List registered workspaces with their canonical root directories. */
|
||||
list(): readonly { path: string }[]
|
||||
}
|
||||
|
||||
/** Media extensions the route serves, mapped to their content types. */
|
||||
const MEDIA_TYPES: Readonly<Record<string, string>> = {
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
'.avif': 'image/avif',
|
||||
'.mp4': 'video/mp4',
|
||||
'.webm': 'video/webm',
|
||||
'.mov': 'video/quicktime',
|
||||
'.ogv': 'video/ogg',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.wav': 'audio/wav',
|
||||
'.ogg': 'audio/ogg',
|
||||
'.oga': 'audio/ogg',
|
||||
'.m4a': 'audio/mp4',
|
||||
'.aac': 'audio/aac',
|
||||
'.flac': 'audio/flac',
|
||||
}
|
||||
/** Media categories this route serves. */
|
||||
const MEDIA_CATEGORIES: ReadonlySet<string> = new Set(['image', 'video', 'audio'])
|
||||
/** Category matches that still must not be served. */
|
||||
const DENIED_MEDIA_TYPES: ReadonlySet<string> = new Set(['image/svg+xml'])
|
||||
|
||||
/**
|
||||
* The content type a file path may be served as, or undefined when the
|
||||
* extension is not an allowlisted media type.
|
||||
* The content type a file path may be served as, or undefined when it is not
|
||||
* an allowlisted media type.
|
||||
* @param path - Canonical file path (extension only is read).
|
||||
* @returns the content type, or undefined to refuse the file.
|
||||
*/
|
||||
export function mediaTypeForPath(path: string): string | undefined {
|
||||
return MEDIA_TYPES[extname(path).toLowerCase()]
|
||||
}
|
||||
|
||||
/** One resolved byte-range within a file; `full` streams the whole file. */
|
||||
type ByteRange =
|
||||
| { readonly kind: 'full' }
|
||||
| { readonly kind: 'partial'; readonly start: number; readonly end: number }
|
||||
|
||||
/**
|
||||
* Parse one single-range `Range` header value against the file size.
|
||||
* @param header - Raw `Range` request header, or null when absent.
|
||||
* @param size - Total file size in bytes.
|
||||
* @returns the byte range to serve, or undefined when the header names no
|
||||
* satisfiable single range (the caller answers 416 with a `bytes *\/size`
|
||||
* Content-Range header).
|
||||
*/
|
||||
export function parseByteRange(header: string | null, size: number): ByteRange | undefined {
|
||||
if (header === null) return { kind: 'full' }
|
||||
const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim())
|
||||
if (match === null) return undefined
|
||||
const startText = match[1]
|
||||
const endText = match[2]
|
||||
if (startText === '' && endText === '') return undefined
|
||||
let start: number
|
||||
let end: number
|
||||
if (startText === '') {
|
||||
// Suffix range: the last N bytes.
|
||||
const length = Number(endText)
|
||||
if (length === 0) return undefined
|
||||
start = Math.max(size - length, 0)
|
||||
end = size - 1
|
||||
} else {
|
||||
start = Number(startText)
|
||||
end = endText === '' ? size - 1 : Number(endText)
|
||||
}
|
||||
if (start > end || start >= size) return undefined
|
||||
return { kind: 'partial', start, end: Math.min(end, size - 1) }
|
||||
function mediaTypeForPath(path: string): string | undefined {
|
||||
const looked = mime.lookup(path)
|
||||
if (looked === false) return undefined
|
||||
if (DENIED_MEDIA_TYPES.has(looked)) return undefined
|
||||
const category = looked.slice(0, looked.indexOf('/'))
|
||||
return MEDIA_CATEGORIES.has(category) ? looked : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve one workspace-contained media file over the shared API channel.
|
||||
* @param request - Authenticated fetch-route request (GET or HEAD).
|
||||
* @param registry - Workspace registry; absent (or empty) denies everything.
|
||||
* @param roots - Registered workspace root directories.
|
||||
* @returns A streaming media response or a fail-closed status.
|
||||
*/
|
||||
export async function serveMediaReference(
|
||||
async function serveMediaReference(
|
||||
request: Request,
|
||||
registry: MediaReferenceRegistry | undefined,
|
||||
roots: readonly { path: string }[],
|
||||
): Promise<Response> {
|
||||
const path = new URL(request.url).searchParams.get('path')
|
||||
if (path === null || path.length === 0) return new Response('missing path', { status: 400 })
|
||||
if (path.includes('\0') || !isAbsolute(path)) {
|
||||
return new Response('absolute path required', { status: 400 })
|
||||
}
|
||||
if (registry === undefined) return new Response('file serving is unavailable', { status: 403 })
|
||||
let canonical: string
|
||||
let info
|
||||
try {
|
||||
@@ -133,7 +79,7 @@ export async function serveMediaReference(
|
||||
} catch {
|
||||
return new Response('not found', { status: 404 })
|
||||
}
|
||||
const insideWorkspace = registry.list().some(root =>
|
||||
const insideWorkspace = roots.some(root =>
|
||||
canonical === root.path || canonical.startsWith(root.path + sep))
|
||||
if (!insideWorkspace) return new Response('outside workspace roots', { status: 403 })
|
||||
if (!info.isFile()) return new Response('not a regular file', { status: 403 })
|
||||
@@ -141,29 +87,41 @@ export async function serveMediaReference(
|
||||
if (mediaType === undefined) {
|
||||
return new Response('not an allowlisted media type', { status: 415 })
|
||||
}
|
||||
const range = parseByteRange(request.headers.get('range'), info.size)
|
||||
if (range === undefined) {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Range': 'bytes */' + String(info.size),
|
||||
'Cache-Control': 'private, no-store',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
const total = info.size
|
||||
const rangeHeader = request.headers.get('range')
|
||||
let start = 0
|
||||
let end = total - 1
|
||||
let partial = false
|
||||
if (rangeHeader !== null) {
|
||||
const parsed = rangeParser(total, rangeHeader)
|
||||
// -1 (unsatisfiable) and -2 (malformed) both answer the same terminal
|
||||
// way; array results always carry at least one range.
|
||||
const first = Array.isArray(parsed) ? parsed[0] : undefined
|
||||
if (first === undefined) {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Range': 'bytes */' + String(total),
|
||||
'Cache-Control': 'private, no-store',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
}
|
||||
return new Response(null, { status: 416, headers })
|
||||
}
|
||||
return new Response(null, { status: 416, headers })
|
||||
start = first.start
|
||||
end = first.end
|
||||
partial = true
|
||||
}
|
||||
const partial = range.kind === 'partial'
|
||||
const start = partial ? range.start : 0
|
||||
const end = partial ? range.end : info.size - 1
|
||||
const body: ReadableStream<Uint8Array> = Readable.toWeb(
|
||||
createReadStream(canonical, { start, end }),
|
||||
createReadStream(canonical, partial ? { start, end } : undefined),
|
||||
) as ReadableStream<Uint8Array>
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': mediaType,
|
||||
'Content-Length': String(partial ? end - start + 1 : info.size),
|
||||
'Content-Length': String(partial ? end - start + 1 : total),
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Cache-Control': 'private, no-store',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
}
|
||||
if (partial) headers['Content-Range'] = 'bytes ' + String(start) + '-' + String(end) + '/' + String(info.size)
|
||||
if (partial) {
|
||||
headers['Content-Range'] = 'bytes ' + String(start) + '-' + String(end) + '/' + String(total)
|
||||
}
|
||||
const head = request.method === 'HEAD'
|
||||
return new Response(head ? null : body, { status: partial ? 206 : 200, headers })
|
||||
}
|
||||
@@ -186,7 +144,7 @@ export const SessionMediaReferences = {
|
||||
path: '/api/file',
|
||||
methods: ['GET', 'HEAD'],
|
||||
requestBody: 'buffered',
|
||||
fetch: request => serveMediaReference(request, ctx.workspaceRegistry),
|
||||
fetch: request => serveMediaReference(request, ctx.workspaceRegistry.list()),
|
||||
}), 'session-controller: /api/file')
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user