( req: ProxyRequest, accessToken: string | null, csrf: CsrfBundle | null )
| 206 | } |
| 207 | |
| 208 | async function callOdata( |
| 209 | req: ProxyRequest, |
| 210 | accessToken: string | null, |
| 211 | csrf: CsrfBundle | null |
| 212 | ): Promise<OdataInvocation> { |
| 213 | const url = buildOdataUrl(req) |
| 214 | const headers: Record<string, string> = { |
| 215 | Authorization: buildAuthHeader(req, accessToken), |
| 216 | Accept: 'application/json', |
| 217 | } |
| 218 | |
| 219 | const isWrite = WRITE_METHODS.has(req.method) |
| 220 | const hasBody = req.body !== undefined && req.body !== null |
| 221 | if (hasBody) headers['Content-Type'] = 'application/json' |
| 222 | if (req.ifMatch) headers['If-Match'] = req.ifMatch |
| 223 | |
| 224 | if (isWrite && csrf) { |
| 225 | headers['X-CSRF-Token'] = csrf.token |
| 226 | if (csrf.cookie) headers.Cookie = csrf.cookie |
| 227 | } |
| 228 | |
| 229 | const response = await secureFetchWithValidation( |
| 230 | url, |
| 231 | { |
| 232 | method: req.method, |
| 233 | headers, |
| 234 | body: hasBody ? JSON.stringify(req.body) : undefined, |
| 235 | timeout: OUTBOUND_FETCH_TIMEOUT_MS, |
| 236 | }, |
| 237 | 'baseUrl' |
| 238 | ) |
| 239 | |
| 240 | const raw = await response.text() |
| 241 | let parsed: unknown = null |
| 242 | if (raw.length > 0) { |
| 243 | try { |
| 244 | parsed = JSON.parse(raw) |
| 245 | } catch { |
| 246 | parsed = raw |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | const csrfHeader = response.headers.get('x-csrf-token')?.toLowerCase() ?? '' |
| 251 | return { status: response.status, body: parsed, raw, csrfHeader } |
| 252 | } |
| 253 | |
| 254 | function isCsrfRequired(invocation: OdataInvocation): boolean { |
| 255 | if (invocation.status !== 403) return false |
no test coverage detected