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:
_Kerman
2026-09-07 16:35:15 +08:00
parent f81c5072bd
commit 4b009e7f94
8 changed files with 223 additions and 270 deletions
@@ -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')
},
}