1
/**2
* Authenticated GET/HEAD /api/file reads bounded file responses through3
* the composed filesystem provider. Paths and MIME types do not restrict access;4
* the connection service authenticates requests before this handler.5
* @module @deepseek-ai/dsh-api-session-controller/media-references6
*/8
import { isAbsolute } from 'node:path'9
import type { Context } from '@deepseek-ai/cordis'10
import type {} from '@deepseek-ai/dsh-client-connection'11
import type {} from '@deepseek-ai/dsh-attachment'12
import { FsError, type FileSystem } from '@deepseek-ai/dsh-fs'13
import mime from 'mime-types'15
const BASE_HEADERS = {16
'Cache-Control': 'private, no-store',17
'X-Content-Type-Options': 'nosniff',18
// HTML and SVG files may be opened directly on the authenticated API origin.19
'Content-Security-Policy': "sandbox; default-src 'none'",20
}22
async function serveFile(request: Request, fs: FileSystem, maxBytes: number): Promise<Response> {23
const fail = (status: number, text: string): Response =>24
new Response(request.method === 'HEAD' ? null : text, { status, headers: BASE_HEADERS })25
const path = new URL(request.url).searchParams.get('path')26
if (path === null || path.length === 0) return fail(400, 'missing path')27
if (path.includes('\0') || !isAbsolute(path)) return fail(400, 'absolute path required')28
try {29
const target = await fs.resolve(path, { signal: request.signal })30
const mediaType = mime.lookup(target.displayPath) || 'application/octet-stream'31
const headers: Record<string, string> = {32
...BASE_HEADERS,33
'Content-Type': mediaType,34
}35
if (request.method === 'HEAD') {36
const info = await fs.stat(target, request.signal)37
if (info === undefined) return fail(404, 'not found')38
if (info.type !== 'file') return fail(403, 'not a regular file')39
if (info.size !== undefined) {40
if (info.size > maxBytes) return fail(413, 'file exceeds byte limit')41
headers['Content-Length'] = String(info.size)42
}43
return new Response(null, { headers })44
}45
const bytes = await fs.readBytes(target, request.signal, maxBytes)46
headers['Content-Length'] = String(bytes.byteLength)47
return new Response(bytes.slice(), { headers })48
} catch (error: unknown) {49
if (!(error instanceof FsError)) throw error50
const statuses: Partial<Record<FsError['code'], number>> = {51
FS_NOT_FOUND: 404,52
FS_NOT_REGULAR_FILE: 403,53
FS_PERMISSION_DENIED: 403,54
FS_SANDBOX_DENIED: 403,55
FS_TOO_LARGE: 413,56
FS_ABORTED: 499,57
}58
return fail(statuses[error.code] ?? 500, error.code)59
}60
}62
/**63
* File-display contribution. The connection service supplies authentication;64
* `ctx.fs` supplies the execution world's paths, reads, and access policy.65
*/66
export const SessionMediaReferences = {67
inject: ['connection', 'fs', 'attachments'],68
apply(ctx: Context): void {69
const maxBytes = ctx.attachments.imageLimits.maxImageBytes70
ctx.effect(() => ctx.connection.fetch.register({71
path: '/api/file',72
methods: ['GET', 'HEAD'],73
requestBody: 'buffered',74
fetch: request => serveFile(request, ctx.fs, maxBytes),75
}), 'session-controller: /api/file')76
},77
}