11import { readFile } from 'fs/promises'
2+ import { type Principal , requirePrincipalSubjectUserId } from '@sim/auth/principal'
23import { createLogger } from '@sim/logger'
34import type { NextRequest } from 'next/server'
45import { NextResponse } from 'next/server'
56import { fileServeParamsSchema , fileServeQuerySchema } from '@/lib/api/contracts/storage-transfer'
7+ import {
8+ concealCrossTenantResourceError ,
9+ InternalUnauthenticatedError ,
10+ } from '@/lib/api/server/routes'
611import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
712import { resolveServableDocBytes } from '@/lib/copilot/tools/server/files/doc-compile'
813import { DocCompileUserError } from '@/lib/copilot/tools/server/files/doc-compile-error'
14+ import { asOrchestrationError } from '@/lib/core/orchestration/types'
915import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1016import { CopilotFiles , isUsingCloudStorage } from '@/lib/uploads'
1117import type { StorageContext } from '@/lib/uploads/config'
1218import { parseWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
1319import { downloadFile } from '@/lib/uploads/core/storage-service'
1420import { resolveServableImageBytes } from '@/lib/uploads/server/image-derivative'
1521import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
22+ import { internalWorkspaceFileServeAuth } from '@/lib/workspace-files/api'
23+ import { readWorkspaceFileContentByKey } from '@/lib/workspace-files/application/read-workspace-file-content-by-key'
1624import { verifyFileAccess } from '@/app/api/files/authorization'
1725import {
1826 createErrorResponse ,
@@ -66,9 +74,11 @@ async function resolveServableBytes(params: {
6674 workspaceId : string | undefined
6775 options : ServeOptions
6876 ownerKey : string | undefined
77+ filePrincipal ?: Principal
6978 signal : AbortSignal | undefined
7079} ) : Promise < { buffer : Buffer ; contentType : string } > {
71- const { buffer, filename, storageKey, workspaceId, options, ownerKey, signal } = params
80+ const { buffer, filename, storageKey, workspaceId, options, ownerKey, filePrincipal, signal } =
81+ params
7282 if ( options . raw ) return { buffer, contentType : getContentType ( filename ) }
7383
7484 if ( options . preview ) {
@@ -82,6 +92,7 @@ async function resolveServableBytes(params: {
8292 rawBuffer : buffer ,
8393 fileName : filename ,
8494 workspaceId,
95+ filePrincipal,
8596 ownerKey,
8697 signal,
8798 } )
@@ -154,6 +165,23 @@ export const GET = withRouteHandler(
154165 return await handleLocalFilePublic ( fullPath )
155166 }
156167
168+ const storageContext = inferContextFromKey ( cloudKey )
169+ const workspacePrincipal =
170+ storageContext === 'workspace'
171+ ? await internalWorkspaceFileServeAuth . authenticate ( request , { path } )
172+ : undefined
173+ const legacyAuthResult = workspacePrincipal
174+ ? undefined
175+ : await checkSessionOrInternalAuth ( request , { requireWorkflowId : false } )
176+
177+ if ( legacyAuthResult && ( ! legacyAuthResult . success || ! legacyAuthResult . userId ) ) {
178+ logger . warn ( 'Unauthorized file access attempt' , {
179+ path,
180+ error : legacyAuthResult . error || 'Missing userId' ,
181+ } )
182+ return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } )
183+ }
184+
157185 const query = fileServeQuerySchema . parse ( {
158186 raw : request . nextUrl . searchParams . get ( 'raw' ) ,
159187 preview : request . nextUrl . searchParams . get ( 'preview' ) ,
@@ -165,24 +193,24 @@ export const GET = withRouteHandler(
165193 versioned : query . v != null ,
166194 }
167195
168- const authResult = await checkSessionOrInternalAuth ( request , { requireWorkflowId : false } )
169-
170- if ( ! authResult . success || ! authResult . userId ) {
171- logger . warn ( 'Unauthorized file access attempt' , {
172- path,
173- error : authResult . error || 'Missing userId' ,
174- } )
175- return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } )
196+ if ( workspacePrincipal ) {
197+ return await handleWorkspaceFile ( cloudKey , workspacePrincipal , options , request )
176198 }
177199
178- const userId = authResult . userId
200+ const userId = legacyAuthResult ?. userId
201+ if ( ! userId ) throw new Error ( 'Authenticated file serve request is missing a user ID' )
179202
180203 if ( isUsingCloudStorage ( ) ) {
181204 return await handleCloudProxy ( cloudKey , userId , options , request . signal )
182205 }
183206
184207 return await handleLocalFile ( cloudKey , userId , options , request . signal )
185208 } catch ( error ) {
209+ if ( error instanceof InternalUnauthenticatedError ) {
210+ logger . warn ( 'Unauthorized file access attempt' , { error : error . message } )
211+ return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } )
212+ }
213+
186214 // An in-progress/incomplete doc source fails to compile — this is expected
187215 // mid-generation, not a server fault. Return 409 (not 500) so it isn't an
188216 // alarming error; the client re-fetches once the doc finishes (the serve
@@ -194,6 +222,15 @@ export const GET = withRouteHandler(
194222 return NextResponse . json ( { error : 'Document is still being generated' } , { status : 409 } )
195223 }
196224
225+ const orchestrationError = asOrchestrationError (
226+ concealCrossTenantResourceError ( error , 'File not found' )
227+ )
228+ if ( orchestrationError ?. code === 'not_found' ) {
229+ const notFound = new FileNotFoundError ( 'File not found' )
230+ logServeFailure ( 'Error serving file:' , notFound )
231+ return createErrorResponse ( notFound )
232+ }
233+
197234 logServeFailure ( 'Error serving file:' , error )
198235
199236 if ( error instanceof FileNotFoundError ) {
@@ -205,6 +242,45 @@ export const GET = withRouteHandler(
205242 }
206243)
207244
245+ async function handleWorkspaceFile (
246+ key : string ,
247+ principal : Principal ,
248+ options : ServeOptions ,
249+ request : NextRequest
250+ ) : Promise < NextResponse > {
251+ const workspaceId = getWorkspaceIdForCompile ( key )
252+ if ( ! workspaceId ) throw new FileNotFoundError ( `File not found: ${ key } ` )
253+
254+ const { file, content } = await readWorkspaceFileContentByKey . execute ( {
255+ principal,
256+ input : { key, assertedWorkspaceId : workspaceId } ,
257+ request,
258+ } )
259+ const ownerKey = `user:${ requirePrincipalSubjectUserId ( principal ) } `
260+ const resolved = await resolveServableBytes ( {
261+ buffer : content ,
262+ filename : file . name ,
263+ storageKey : key ,
264+ workspaceId,
265+ options,
266+ ownerKey,
267+ filePrincipal : principal ,
268+ signal : request . signal ,
269+ } )
270+
271+ logger . info ( 'Workspace file served' , {
272+ fileId : file . id ,
273+ workspaceId,
274+ size : resolved . buffer . length ,
275+ } )
276+ return createFileResponse ( {
277+ buffer : resolved . buffer ,
278+ contentType : resolved . contentType ,
279+ filename : file . name ,
280+ cacheControl : resolveServeCacheControl ( options . versioned , 'workspace' ) ,
281+ } )
282+ }
283+
208284async function handleLocalFile (
209285 filename : string ,
210286 userId : string ,
0 commit comments