@@ -11,6 +11,7 @@ import {
1111 isRecord ,
1212 isUrlAllowed ,
1313 normalizeContentType ,
14+ resolveAuthAllowedHosts ,
1415 resolveAllowedHosts ,
1516} from "./shared.js" ;
1617
@@ -85,6 +86,8 @@ async function fetchWithAuthFallback(params: {
8586 url : string ;
8687 tokenProvider ?: MSTeamsAccessTokenProvider ;
8788 fetchFn ?: typeof fetch ;
89+ allowHosts : string [ ] ;
90+ authAllowHosts : string [ ] ;
8891} ) : Promise < Response > {
8992 const fetchFn = params . fetchFn ?? fetch ;
9093 const firstAttempt = await fetchFn ( params . url ) ;
@@ -97,17 +100,40 @@ async function fetchWithAuthFallback(params: {
97100 if ( firstAttempt . status !== 401 && firstAttempt . status !== 403 ) {
98101 return firstAttempt ;
99102 }
103+ if ( ! isUrlAllowed ( params . url , params . authAllowHosts ) ) {
104+ return firstAttempt ;
105+ }
100106
101107 const scopes = scopeCandidatesForUrl ( params . url ) ;
102108 for ( const scope of scopes ) {
103109 try {
104110 const token = await params . tokenProvider . getAccessToken ( scope ) ;
105111 const res = await fetchFn ( params . url , {
106112 headers : { Authorization : `Bearer ${ token } ` } ,
113+ redirect : "manual" ,
107114 } ) ;
108115 if ( res . ok ) {
109116 return res ;
110117 }
118+ const redirectUrl = readRedirectUrl ( params . url , res ) ;
119+ if ( redirectUrl && isUrlAllowed ( redirectUrl , params . allowHosts ) ) {
120+ const redirectRes = await fetchFn ( redirectUrl ) ;
121+ if ( redirectRes . ok ) {
122+ return redirectRes ;
123+ }
124+ if (
125+ ( redirectRes . status === 401 || redirectRes . status === 403 ) &&
126+ isUrlAllowed ( redirectUrl , params . authAllowHosts )
127+ ) {
128+ const redirectAuthRes = await fetchFn ( redirectUrl , {
129+ headers : { Authorization : `Bearer ${ token } ` } ,
130+ redirect : "manual" ,
131+ } ) ;
132+ if ( redirectAuthRes . ok ) {
133+ return redirectAuthRes ;
134+ }
135+ }
136+ }
111137 } catch {
112138 // Try the next scope.
113139 }
@@ -116,6 +142,21 @@ async function fetchWithAuthFallback(params: {
116142 return firstAttempt ;
117143}
118144
145+ function readRedirectUrl ( baseUrl : string , res : Response ) : string | null {
146+ if ( ! [ 301 , 302 , 303 , 307 , 308 ] . includes ( res . status ) ) {
147+ return null ;
148+ }
149+ const location = res . headers . get ( "location" ) ;
150+ if ( ! location ) {
151+ return null ;
152+ }
153+ try {
154+ return new URL ( location , baseUrl ) . toString ( ) ;
155+ } catch {
156+ return null ;
157+ }
158+ }
159+
119160/**
120161 * Download all file attachments from a Teams message (images, documents, etc.).
121162 * Renamed from downloadMSTeamsImageAttachments to support all file types.
@@ -125,6 +166,7 @@ export async function downloadMSTeamsAttachments(params: {
125166 maxBytes : number ;
126167 tokenProvider ?: MSTeamsAccessTokenProvider ;
127168 allowHosts ?: string [ ] ;
169+ authAllowHosts ?: string [ ] ;
128170 fetchFn ?: typeof fetch ;
129171 /** When true, embeds original filename in stored path for later extraction. */
130172 preserveFilenames ?: boolean ;
@@ -134,6 +176,7 @@ export async function downloadMSTeamsAttachments(params: {
134176 return [ ] ;
135177 }
136178 const allowHosts = resolveAllowedHosts ( params . allowHosts ) ;
179+ const authAllowHosts = resolveAuthAllowedHosts ( params . authAllowHosts ) ;
137180
138181 // Download ANY downloadable attachment (not just images)
139182 const downloadable = list . filter ( isDownloadableAttachment ) ;
@@ -199,6 +242,8 @@ export async function downloadMSTeamsAttachments(params: {
199242 url : candidate . url ,
200243 tokenProvider : params . tokenProvider ,
201244 fetchFn : params . fetchFn ,
245+ allowHosts,
246+ authAllowHosts,
202247 } ) ;
203248 if ( ! res . ok ) {
204249 continue ;
0 commit comments