1- import { db } from '@sim/db'
21import type { WorkspaceFileRow } from '@sim/db/schema'
32import { workspaceFiles } from '@sim/db/schema'
4- import { createLogger } from '@sim/logger'
5- import { describeError } from '@sim/utils/errors'
63import { and , asc , inArray , isNull } from 'drizzle-orm'
4+ import { requireAdmittedContent } from '@/lib/core/application/content-delivery'
75import { isDocSandboxEnabled } from '@/lib/core/config/env-flags'
86import { OrchestrationError } from '@/lib/core/orchestration/types'
97import { assertKnownSizeWithinLimit } from '@/lib/core/utils/stream-limits'
108import type { DbTransaction } from '@/lib/db/types'
11- import type { ProjectFileTarget } from '@/lib/projects/files/application/authorization'
9+ import type {
10+ ProjectFileAuthorizationContext ,
11+ ProjectFileTarget ,
12+ } from '@/lib/projects/files/application/authorization'
1213import { defineAuthorizedProjectFileUseCase } from '@/lib/projects/files/application/authorized-use-case'
1314import { projectFileOperations } from '@/lib/projects/files/application/operations'
1415import {
@@ -23,11 +24,7 @@ import {
2324 mergeWorkspaceFileSecretProvenance ,
2425 type WorkspaceFileSecretProvenance ,
2526} from '@/lib/uploads/contexts/workspace/file-secret-provenance'
26- import {
27- enqueueWorkspaceFileStorageCleanups ,
28- processWorkspaceFileStorageCleanupsNow ,
29- } from '@/lib/uploads/contexts/workspace/file-storage-cleanup-outbox'
30- import { deleteFile , downloadFile } from '@/lib/uploads/core/storage-service'
27+ import { downloadFile } from '@/lib/uploads/core/storage-service'
3128import {
3229 collectReferencedFileIds ,
3330 compileFileDocument ,
@@ -50,8 +47,6 @@ import {
5047import { createFileReadReceipt , type FileReadReceipt } from '@/lib/workspace-files/read-receipt'
5148import { markFileSearchArtifactReadyInTx } from '@/lib/workspace-files/search/artifact-ready'
5249
53- const logger = createLogger ( 'ProjectFileArtifacts' )
54-
5550type ProjectOwner = { entityType : 'project' ; entityId : string }
5651interface ArtifactInput extends ProjectFileTarget {
5752 fileId : string
@@ -68,10 +63,10 @@ interface ArtifactManifest {
6863 dependencies : WorkspaceFileRow [ ]
6964 pageHtml ?: string
7065 generatedDocument : boolean
66+ secretProvenance : WorkspaceFileSecretProvenance
7167}
7268interface PreparedArtifact {
7369 artifactKey ?: string
74- writtenArtifactKeys : readonly string [ ]
7570 manifest : ArtifactManifest
7671 buffer : Buffer
7772 contentType : string
@@ -86,45 +81,6 @@ interface ArtifactResult {
8681 receipt : FileReadReceipt
8782}
8883
89- async function discardArtifactWrites ( owner : ProjectOwner , keys : readonly string [ ] ) {
90- if ( keys . length === 0 ) return
91- const derivativePrefix = `project/${ owner . entityId } /image-derivative/`
92- if (
93- keys . some (
94- ( key ) =>
95- ! key . startsWith ( `project/${ owner . entityId } /compiled/` ) &&
96- ! (
97- key . startsWith ( derivativePrefix ) &&
98- / ^ [ a - f 0 - 9 ] { 64 } \. j p g $ / . test ( key . slice ( derivativePrefix . length ) )
99- )
100- )
101- )
102- throw new Error ( 'Artifact cleanup does not belong to this Project' )
103- let events : string [ ]
104- try {
105- events = await enqueueWorkspaceFileStorageCleanups ( db , keys , 'project' )
106- } catch ( error ) {
107- logger . warn ( 'Artifact cleanup could not be persisted; attempting direct deletion' , {
108- owner,
109- keys,
110- error : describeError ( error ) ,
111- } )
112- for ( const key of keys ) {
113- try {
114- await deleteFile ( { key, context : 'project' } )
115- } catch ( cleanupError ) {
116- logger . error ( 'Uncommitted artifact could not be deleted or queued for cleanup' , {
117- owner,
118- key,
119- error : describeError ( cleanupError ) ,
120- } )
121- }
122- }
123- return
124- }
125- await processWorkspaceFileStorageCleanupsNow ( events , { owner, reason : 'artifact read failed' } )
126- }
127-
12884function requireSameRevision ( current : WorkspaceFileRow | undefined , expected : WorkspaceFileRow ) {
12985 if (
13086 ! current ||
@@ -154,6 +110,34 @@ async function loadDependencies(tx: DbTransaction, owner: ProjectOwner, ids: str
154110 return rows
155111}
156112
113+ const snapshotSource = defineAuthorizedProjectFileUseCase <
114+ typeof projectFileOperations . readArtifact ,
115+ ArtifactInput ,
116+ { file : WorkspaceFileRow ; secretProvenance : WorkspaceFileSecretProvenance | undefined }
117+ > ( {
118+ operation : projectFileOperations . readArtifact ,
119+ async execute ( {
120+ context,
121+ tx,
122+ } : {
123+ context : ProjectFileAuthorizationContext
124+ tx : DbTransaction
125+ input : ArtifactInput
126+ } ) {
127+ const file = context . file
128+ if ( ! file ) throw new OrchestrationError ( 'not_found' , 'File not found' )
129+ const evidence = await getBoundWorkspaceFileSecretProvenanceByMetadata ( tx , [ file ] )
130+ return { file, secretProvenance : evidence . get ( file . id ) }
131+ } ,
132+ afterSuccess : ( { principal, result } ) =>
133+ requireAdmittedContent ( principal , {
134+ kind : 'workspace-file' ,
135+ id : result . file . id ,
136+ provenance : result . secretProvenance ?? { status : 'unknown' } ,
137+ view : 'opaque' ,
138+ } ) ,
139+ } )
140+
157141const snapshotArtifact = defineAuthorizedProjectFileUseCase <
158142 typeof projectFileOperations . readArtifact ,
159143 ArtifactInput & { source : SourceSnapshot } ,
@@ -203,8 +187,25 @@ const snapshotArtifact = defineAuthorizedProjectFileUseCase<
203187 'Document exceeds the referenced input limit'
204188 )
205189 const dependencies = await loadDependencies ( tx , context . owner , ids )
206- return { source : input . source . file , dependencies, pageHtml, generatedDocument }
190+ const evidence = await getBoundWorkspaceFileSecretProvenanceByMetadata ( tx , [
191+ input . source . file ,
192+ ...dependencies ,
193+ ] )
194+ return {
195+ source : input . source . file ,
196+ dependencies,
197+ pageHtml,
198+ generatedDocument,
199+ secretProvenance : mergeWorkspaceFileSecretProvenance ( ...evidence . values ( ) ) ,
200+ }
207201 } ,
202+ afterSuccess : ( { principal, result } ) =>
203+ requireAdmittedContent ( principal , {
204+ kind : 'workspace-file' ,
205+ id : result . source . id ,
206+ provenance : result . secretProvenance ,
207+ view : 'opaque' ,
208+ } ) ,
208209} )
209210
210211/** Renders only source-owned dependencies, then rechecks the source, every input, and current authority. */
@@ -223,97 +224,84 @@ export const readProjectFileArtifact = defineAuthorizedProjectFileUseCase<
223224 ) {
224225 throw new OrchestrationError ( 'validation' , 'Invalid document byte limit' )
225226 }
226- const file = context . file
227- if ( ! file ) throw new OrchestrationError ( 'not_found' , 'File not found' )
228- const writtenArtifactKeys = new Set < string > ( )
229- try {
230- const artifact = await resolveDocumentRender (
231- file . originalName ,
232- { maxBytes : input . maxBytes } ,
233- async ( ) => {
234- const content = await downloadFile ( {
235- key : file . key ,
227+ const { file } = await snapshotSource . execute ( { principal, input } )
228+ const artifact = await resolveDocumentRender (
229+ file . originalName ,
230+ { maxBytes : input . maxBytes } ,
231+ async ( ) => {
232+ const content = await downloadFile ( {
233+ key : file . key ,
234+ context : 'project' ,
235+ maxBytes : input . maxBytes ,
236+ } )
237+ const manifest = await snapshotArtifact . execute ( {
238+ principal,
239+ input : { ...input , source : { file, content } } ,
240+ } )
241+ let remaining = manifest . pageHtml ? 32 * 1024 * 1024 : 50 * 1024 * 1024
242+ const inputs = [ ]
243+ for ( const dependency of manifest . dependencies ) {
244+ const bytes = await downloadFile ( {
245+ key : dependency . key ,
236246 context : 'project' ,
237- maxBytes : input . maxBytes ,
247+ maxBytes : Math . min ( remaining , manifest . pageHtml ? 8 * 1024 * 1024 : 25 * 1024 * 1024 ) ,
238248 } )
239- const manifest = await snapshotArtifact . execute ( {
240- principal,
241- input : { ...input , source : { file, content } } ,
249+ remaining -= bytes . length
250+ inputs . push ( {
251+ fileId : dependency . id ,
252+ contentType : dependency . contentType ,
253+ content : bytes ,
242254 } )
243- let remaining = manifest . pageHtml ? 32 * 1024 * 1024 : 50 * 1024 * 1024
244- const inputs = [ ]
245- for ( const dependency of manifest . dependencies ) {
246- const bytes = await downloadFile ( {
247- key : dependency . key ,
248- context : 'project' ,
249- maxBytes : Math . min ( remaining , manifest . pageHtml ? 8 * 1024 * 1024 : 25 * 1024 * 1024 ) ,
250- } )
251- remaining -= bytes . length
252- inputs . push ( {
253- fileId : dependency . id ,
254- contentType : dependency . contentType ,
255- content : bytes ,
256- } )
257- }
258- if ( manifest . pageHtml ) {
259- const byId = new Map ( inputs . map ( ( input ) => [ input . fileId , input ] ) )
260- const page = await inlineSimPageImages (
261- manifest . pageHtml ,
262- async ( { fileId } ) => {
263- const asset = byId . get ( fileId )
264- if ( ! asset ) throw new OrchestrationError ( 'not_found' , 'Document input not found' )
265- return { bytes : asset . content , contentType : asset . contentType , identity : fileId }
266- } ,
267- { strict : true }
268- )
269- const buffer = Buffer . from ( page . html , 'utf-8' )
270- assertKnownSizeWithinLimit ( buffer . length , input . maxBytes , 'rendered page' )
271- return {
272- manifest,
273- buffer,
274- contentType : 'text/html' ,
275- dependsOnReferencedFiles : inputs . length > 0 ,
276- }
277- }
278- if ( manifest . generatedDocument ) {
279- const inputIdentity = fileDocumentInputIdentity ( context . owner , manifest . dependencies )
280- const compiled = await compileFileDocument ( {
281- owner : context . owner ,
282- source : content . toString ( 'utf-8' ) ,
283- fileName : file . originalName ,
284- inputs,
285- onArtifactWrite : ( key ) => {
286- writtenArtifactKeys . add ( key )
287- } ,
288- inputIdentity,
289- maxBytes : input . maxBytes ,
290- } )
291- return { manifest, ...compiled }
292- }
255+ }
256+ if ( manifest . pageHtml ) {
257+ const byId = new Map ( inputs . map ( ( input ) => [ input . fileId , input ] ) )
258+ const page = await inlineSimPageImages (
259+ manifest . pageHtml ,
260+ async ( { fileId } ) => {
261+ const asset = byId . get ( fileId )
262+ if ( ! asset ) throw new OrchestrationError ( 'not_found' , 'Document input not found' )
263+ return { bytes : asset . content , contentType : asset . contentType , identity : fileId }
264+ } ,
265+ { strict : true }
266+ )
267+ const buffer = Buffer . from ( page . html , 'utf-8' )
268+ assertKnownSizeWithinLimit ( buffer . length , input . maxBytes , 'rendered page' )
293269 return {
294270 manifest,
295- buffer : content ,
296- contentType : file . contentType ,
297- dependsOnReferencedFiles : false ,
271+ buffer,
272+ contentType : 'text/html' ,
273+ dependsOnReferencedFiles : inputs . length > 0 ,
298274 }
299275 }
300- )
301- const derivative = input . preview
302- ? await resolveServableImageBytes ( artifact . buffer , artifact . manifest . source . key , {
276+ if ( manifest . generatedDocument ) {
277+ const inputIdentity = fileDocumentInputIdentity ( context . owner , manifest . dependencies )
278+ const compiled = await compileFileDocument ( {
303279 owner : context . owner ,
304- onArtifactWrite : ( key ) => writtenArtifactKeys . add ( key ) ,
280+ source : content . toString ( 'utf-8' ) ,
281+ fileName : file . originalName ,
282+ inputs,
283+ inputIdentity,
284+ maxBytes : input . maxBytes ,
305285 } )
306- : null
307- if ( derivative )
308- assertKnownSizeWithinLimit ( derivative . buffer . length , input . maxBytes , 'image preview' )
309- return { ...artifact , ...derivative , writtenArtifactKeys : [ ...writtenArtifactKeys ] }
310- } catch ( error ) {
311- await discardArtifactWrites ( context . owner , [ ...writtenArtifactKeys ] )
312- throw error
313- }
286+ return { manifest, ...compiled }
287+ }
288+ return {
289+ manifest,
290+ buffer : content ,
291+ contentType : file . contentType ,
292+ dependsOnReferencedFiles : false ,
293+ }
294+ }
295+ )
296+ const derivative = input . preview
297+ ? await resolveServableImageBytes ( artifact . buffer , artifact . manifest . source . key , {
298+ owner : context . owner ,
299+ } )
300+ : null
301+ if ( derivative )
302+ assertKnownSizeWithinLimit ( derivative . buffer . length , input . maxBytes , 'image preview' )
303+ return { ...artifact , ...derivative }
314304 } ,
315- onCommitFailure : ( { context, prepared } ) =>
316- discardArtifactWrites ( context . owner , prepared . writtenArtifactKeys ) ,
317305 async execute ( { input, context, tx, prepared } ) {
318306 if ( ! prepared ) throw new Error ( 'Prepared document is unavailable' )
319307 requireSameRevision ( context . file , prepared . manifest . source )
@@ -361,6 +349,13 @@ export const readProjectFileArtifact = defineAuthorizedProjectFileUseCase<
361349 receipt : createFileReadReceipt ( context . owner , [ source , ...dependencies ] ) ,
362350 }
363351 } ,
364- afterSuccess : ( { result } ) =>
365- reportWorkspaceFileDelivery ( result . secretProvenance , result . file . id ) ,
352+ async afterSuccess ( { principal, result } ) {
353+ await requireAdmittedContent ( principal , {
354+ kind : 'workspace-file' ,
355+ id : result . file . id ,
356+ provenance : result . secretProvenance ,
357+ view : 'opaque' ,
358+ } )
359+ reportWorkspaceFileDelivery ( result . secretProvenance , result . file . id )
360+ } ,
366361} )
0 commit comments