@@ -2,7 +2,7 @@ import { mkdtempSync } from 'node:fs'
22import { mkdir , rm , writeFile } from 'node:fs/promises'
33import { tmpdir } from 'node:os'
44import { dirname , join } from 'node:path'
5- import { db } from '@sim/db'
5+ import { db , runOutsideTransactionContext } from '@sim/db'
66import {
77 folder ,
88 idempotencyKey ,
@@ -17,6 +17,7 @@ import {
1717 subscription ,
1818 user ,
1919 workspace ,
20+ workspaceFileSecretProvenance ,
2021 workspaceFiles ,
2122} from '@sim/db/schema'
2223import { deleteWorkspaceFixture , insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures'
@@ -38,6 +39,7 @@ import {
3839 createProjectFile ,
3940 updateProjectFileContent ,
4041} from '@/lib/projects/files/application/content'
42+ import * as shareManager from '@/lib/public-shares/manager'
4143import * as storage from '@/lib/uploads/core/storage-service'
4244
4345vi . mock ( '@/lib/core/config/feature-flags' , ( ) => featureFlagsMock )
@@ -139,6 +141,84 @@ function createInput(projectId: string, content = 'Project architecture') {
139141}
140142
141143describe ( 'Public file shares against PostgreSQL and private storage' , ( ) => {
144+ check (
145+ 'public delivery rejects unknown and secret-bearing provenance before storage' ,
146+ async ( ) => {
147+ const { updateProjectFileShare } = await import ( '@/lib/projects/files/application/shares' )
148+ const { authorizePublicFileShare, readPublicFileShareContent } = await import (
149+ '@/lib/public-shares/application'
150+ )
151+ const f = await fixture ( )
152+ for ( const secretProvenance of [
153+ { status : 'unknown' as const } ,
154+ {
155+ status : 'exact' as const ,
156+ entries : [ { encryptedValue : 'private-fixture' , sourceUserId : f . editorId } ] ,
157+ } ,
158+ ] ) {
159+ const { file } = await createProjectFile . execute ( {
160+ principal : f . principal ,
161+ input : {
162+ ...createInput ( f . projectId ) ,
163+ name : `${ generateId ( ) } .md` ,
164+ secretProvenance,
165+ } ,
166+ } )
167+ const { share } = await updateProjectFileShare . execute ( {
168+ principal : f . principal ,
169+ input : {
170+ projectId : f . projectId ,
171+ fileId : file . id ,
172+ isActive : true ,
173+ } ,
174+ } )
175+ const access = await authorizePublicFileShare ( {
176+ token : share . token ,
177+ credential : { method : 'GET' } ,
178+ } )
179+ if ( ! access . authorized ) throw new Error ( 'Public fixture rejected' )
180+ const download = vi . spyOn ( storage , 'downloadFile' )
181+ await expect ( readPublicFileShareContent ( { grant : access . grant } ) ) . rejects . toMatchObject ( {
182+ code : 'forbidden' ,
183+ } )
184+ expect ( download ) . not . toHaveBeenCalled ( )
185+ download . mockRestore ( )
186+ }
187+ }
188+ )
189+
190+ check ( 'non-SSO shares do not consult an unavailable session service' , async ( ) => {
191+ const { updateProjectFileShare } = await import ( '@/lib/projects/files/application/shares' )
192+ const { authorizePublicFileShare } = await import ( '@/lib/public-shares/application' )
193+ const f = await fixture ( )
194+ const { file } = await createProjectFile . execute ( {
195+ principal : f . principal ,
196+ input : createInput ( f . projectId ) ,
197+ } )
198+ const resolveSessionPrincipal = vi
199+ . fn ( )
200+ . mockRejectedValue ( new Error ( 'Session service unavailable' ) )
201+ for ( const authType of [ 'public' , 'password' , 'email' ] as const ) {
202+ const { share } = await updateProjectFileShare . execute ( {
203+ principal : f . principal ,
204+ input : {
205+ projectId : f . projectId ,
206+ fileId : file . id ,
207+ isActive : true ,
208+ authType,
209+ password : 'fixture-password' ,
210+ allowedEmails : [ 'reader@shared.invalid' ] ,
211+ } ,
212+ } )
213+ const result = await authorizePublicFileShare ( {
214+ token : share . token ,
215+ credential : { method : 'GET' , resolveSessionPrincipal } ,
216+ } )
217+ expect ( result . authorized ) . toBe ( authType === 'public' )
218+ }
219+ expect ( resolveSessionPrincipal ) . not . toHaveBeenCalled ( )
220+ } )
221+
142222 check (
143223 'public share configuration requires Project write and the canonical organization sharing policy' ,
144224 async ( ) => {
@@ -274,6 +354,21 @@ describe('Public file shares against PostgreSQL and private storage', () => {
274354 input : { ...input , authType : 'password' , password : 'a-valid-share-password' } ,
275355 } )
276356 expect ( allowed . share . authType ) . toBe ( 'password' )
357+ const upsert = shareManager . upsertOwnedFileShare
358+ const interleave = vi
359+ . spyOn ( shareManager , 'upsertOwnedFileShare' )
360+ . mockImplementationOnce ( async ( ...args ) => {
361+ await runOutsideTransactionContext ( ( ) =>
362+ db
363+ . update ( publicShare )
364+ . set ( { authType : 'public' , isActive : false } )
365+ . where ( eq ( publicShare . id , allowed . share . id ) )
366+ )
367+ return upsert ( ...args )
368+ } )
369+ const reenabled = await updateProjectFileShare . execute ( { principal : f . principal , input } )
370+ expect ( reenabled . share ) . toMatchObject ( { authType : 'password' , isActive : true } )
371+ interleave . mockRestore ( )
277372 } finally {
278373 await deleteWorkspaceFixture ( db , eq ( workspace . id , secondId ) )
279374 }
@@ -467,6 +562,17 @@ describe('Public file shares against PostgreSQL and private storage', () => {
467562 await requestPublicFileShareOtp ( { token : share . token , email : 'outsider@shared.invalid' } )
468563 ) . deliver ( )
469564 expect ( await otpStore . getOTP ( 'file' , share . id , 'outsider@shared.invalid' ) ) . toBeNull ( )
565+ const failure = { authorized : false , status : 400 , error : 'Invalid verification code' }
566+ expect (
567+ await verifyPublicFileShareOtp ( {
568+ token : share . token ,
569+ email : 'outsider@shared.invalid' ,
570+ otp : '000000' ,
571+ } )
572+ ) . toEqual ( failure )
573+ expect ( await verifyPublicFileShareOtp ( { token : share . token , email, otp : '000000' } ) ) . toEqual (
574+ failure
575+ )
470576 await ( await requestPublicFileShareOtp ( { token : share . token , email } ) ) . deliver ( )
471577 const stored = await otpStore . getOTP ( 'file' , share . id , email )
472578 if ( ! stored ) throw new Error ( 'Allowed email received no code' )
@@ -613,7 +719,7 @@ describe('Public file shares against PostgreSQL and private storage', () => {
613719 '@/lib/public-shares/application'
614720 )
615721 const { encryptSecret } = await import ( '@/lib/core/security/encryption' )
616- for ( const change of [ 'disabled' , 'password' , 'archived' ] as const ) {
722+ for ( const change of [ 'disabled' , 'password' , 'archived' , 'provenance' ] as const ) {
617723 const f = await fixture ( )
618724 const { file } = await createProjectFile . execute ( {
619725 principal : f . principal ,
@@ -648,6 +754,11 @@ describe('Public file shares against PostgreSQL and private storage', () => {
648754 . update ( publicShare )
649755 . set ( { password : ( await encryptSecret ( 'replacement-password' ) ) . encrypted } )
650756 . where ( eq ( publicShare . id , share . id ) )
757+ if ( change === 'provenance' )
758+ await db
759+ . update ( workspaceFileSecretProvenance )
760+ . set ( { status : 'unknown' } )
761+ . where ( eq ( workspaceFileSecretProvenance . fileId , file . id ) )
651762 if ( change === 'archived' )
652763 await db
653764 . update ( workspaceFiles )
@@ -657,7 +768,7 @@ describe('Public file shares against PostgreSQL and private storage', () => {
657768 return result
658769 } )
659770 await expect ( readPublicFileShareContent ( { grant : access . grant } ) ) . rejects . toMatchObject ( {
660- code : 'not_found' ,
771+ code : change === 'provenance' ? 'forbidden' : 'not_found' ,
661772 } )
662773 spy . mockRestore ( )
663774 }
0 commit comments