返回源码地图

packages/api/session-controller/src/media-references.ts

main snapshot · da00f7f5358f · 正文引用章节 15;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * Authenticated GET/HEAD /api/file reads bounded file responses through
3 * 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-references
6 */
7
8import { isAbsolute } from 'node:path'
9import type { Context } from '@deepseek-ai/cordis'
10import type {} from '@deepseek-ai/dsh-client-connection'
11import type {} from '@deepseek-ai/dsh-attachment'
12import { FsError, type FileSystem } from '@deepseek-ai/dsh-fs'
13import mime from 'mime-types'
14
15const 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}
21
22async 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 error
50 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}
61
62/**
63 * File-display contribution. The connection service supplies authentication;
64 * `ctx.fs` supplies the execution world's paths, reads, and access policy.
65 */
66export const SessionMediaReferences = {
67 inject: ['connection', 'fs', 'attachments'],
68 apply(ctx: Context): void {
69 const maxBytes = ctx.attachments.imageLimits.maxImageBytes
70 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}