@@ -52,11 +52,25 @@ const mocks = vi.hoisted(() => {
5252} ) ;
5353const FakeProxyTranscodeError = mocks . ProxyTranscodeError ;
5454
55+ const mediaMocks = vi . hoisted ( ( ) => ( {
56+ probeAssetCodec : vi . fn ( async ( ) => ( {
57+ codecName : "hevc" ,
58+ browserHostile : true ,
59+ representativeMime : "video/mp4" ,
60+ hasAlpha : false ,
61+ } ) ) ,
62+ decideMediaProxyEligibility : vi . fn (
63+ ( facts : { hasAlpha : boolean ; browserHostile : boolean } | null ) =>
64+ facts ?. hasAlpha ? { eligible : false , reason : "alpha_source" } : { eligible : true } ,
65+ ) ,
66+ } ) ) ;
67+
5568vi . mock ( "@hyperframes/studio-server/proxy-transcoder" , ( ) => ( {
5669 resolveProxy : mocks . resolveProxy ,
5770 ProxyTranscodeError : mocks . ProxyTranscodeError ,
5871 ProxyCapacityError : mocks . ProxyCapacityError ,
5972} ) ) ;
73+ vi . mock ( "@hyperframes/studio-server/media-codec-map" , ( ) => mediaMocks ) ;
6074
6175// The shared injection helper ships as a self-contained dist bundle (its copy
6276// of scanProjectMediaCodecMap is inlined), so it must be mocked wholesale —
@@ -88,11 +102,35 @@ afterEach(() => {
88102 mocks . resolveProxy . mockReset ( ) ;
89103 mocks . resolveProxy . mockResolvedValue ( "/unused-prewarm-proxy-path" ) ;
90104 mocks . scanProjectMediaCodecMap . mockReset ( ) ;
105+ mediaMocks . probeAssetCodec . mockClear ( ) ;
106+ mediaMocks . decideMediaProxyEligibility . mockClear ( ) ;
91107 mocks . scanProjectMediaCodecMap . mockResolvedValue ( { } ) ;
92108 if ( dir ) rmSync ( dir , { recursive : true , force : true } ) ;
93109 dir = undefined ;
94110} ) ;
95111
112+ it ( "rejects direct proxy requests for alpha sources" , async ( ) => {
113+ const project = tmpProject ( ) ;
114+ writeFileSync ( join ( project . dir , "clip.mov" ) , "alpha-prores" ) ;
115+ mediaMocks . probeAssetCodec . mockResolvedValueOnce ( {
116+ codecName : "prores" ,
117+ browserHostile : true ,
118+ representativeMime : "video/quicktime" ,
119+ hasAlpha : true ,
120+ } ) ;
121+ mediaMocks . decideMediaProxyEligibility . mockReturnValueOnce ( {
122+ eligible : false ,
123+ reason : "alpha_source" ,
124+ } ) ;
125+ const app = await buildApp ( project , true ) ;
126+
127+ const res = await app . request ( "/composition/clip.mov?hf-proxy=h264" ) ;
128+
129+ expect ( res . status ) . toBe ( 422 ) ;
130+ expect ( await res . text ( ) ) . toContain ( "alpha_source" ) ;
131+ expect ( mocks . resolveProxy ) . not . toHaveBeenCalled ( ) ;
132+ } ) ;
133+
96134async function buildApp ( project : ProjectDir , autoProxy : boolean ) : Promise < Hono > {
97135 const app = new Hono ( ) ;
98136 await registerCompositionRoute ( app , project , autoProxy ) ;
0 commit comments