(req: ProxyRequest, requestId: string)
| 60 | } |
| 61 | |
| 62 | async function fetchAccessToken(req: ProxyRequest, requestId: string): Promise<string> { |
| 63 | const cacheKey = tokenCacheKey(req) |
| 64 | const cached = TOKEN_CACHE.get(cacheKey) |
| 65 | if (cached && cached.expiresAt - TOKEN_SAFETY_WINDOW_MS > Date.now()) { |
| 66 | return cached.accessToken |
| 67 | } |
| 68 | |
| 69 | const tokenUrl = assertSafeSapExternalUrl(resolveTokenUrl(req), 'tokenUrl').toString() |
| 70 | const basic = Buffer.from(`${req.clientId}:${req.clientSecret}`).toString('base64') |
| 71 | |
| 72 | const response = await secureFetchWithValidation( |
| 73 | tokenUrl, |
| 74 | { |
| 75 | method: 'POST', |
| 76 | headers: { |
| 77 | Authorization: `Basic ${basic}`, |
| 78 | 'Content-Type': 'application/x-www-form-urlencoded', |
| 79 | Accept: 'application/json', |
| 80 | }, |
| 81 | body: 'grant_type=client_credentials', |
| 82 | timeout: OUTBOUND_FETCH_TIMEOUT_MS, |
| 83 | }, |
| 84 | 'tokenUrl' |
| 85 | ) |
| 86 | |
| 87 | if (!response.ok) { |
| 88 | const text = await response.text().catch(() => '') |
| 89 | logger.warn(`[${requestId}] Token fetch failed (${response.status}): ${text}`) |
| 90 | throw new Error(`SAP token request failed: HTTP ${response.status}`) |
| 91 | } |
| 92 | |
| 93 | const data = (await response.json()) as { |
| 94 | access_token?: string |
| 95 | expires_in?: number |
| 96 | } |
| 97 | |
| 98 | if (!data.access_token) { |
| 99 | throw new Error('SAP token response missing access_token') |
| 100 | } |
| 101 | |
| 102 | const expiresInMs = (data.expires_in ?? 3600) * 1000 |
| 103 | rememberToken(cacheKey, { |
| 104 | accessToken: data.access_token, |
| 105 | expiresAt: Date.now() + expiresInMs, |
| 106 | }) |
| 107 | return data.access_token |
| 108 | } |
| 109 | |
| 110 | interface CsrfBundle { |
| 111 | token: string |
no test coverage detected