( auth: ZoomInfoAuth, requestId: string )
| 115 | } |
| 116 | |
| 117 | export async function fetchZoomInfoAccessToken( |
| 118 | auth: ZoomInfoAuth, |
| 119 | requestId: string |
| 120 | ): Promise<string> { |
| 121 | const cacheKey = tokenCacheKey(auth) |
| 122 | const cached = TOKEN_CACHE.get(cacheKey) |
| 123 | if (cached && cached.expiresAt - TOKEN_SAFETY_WINDOW_MS > Date.now()) { |
| 124 | return cached.accessToken |
| 125 | } |
| 126 | |
| 127 | const tokenUrl = assertSafeZoomInfoUrl(ZOOMINFO_TOKEN_URL, 'tokenUrl').toString() |
| 128 | const basic = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64') |
| 129 | |
| 130 | const params = new URLSearchParams() |
| 131 | params.set('grant_type', 'client_credentials') |
| 132 | |
| 133 | const response = await secureFetchWithValidation( |
| 134 | tokenUrl, |
| 135 | { |
| 136 | method: 'POST', |
| 137 | headers: { |
| 138 | Authorization: `Basic ${basic}`, |
| 139 | 'Content-Type': 'application/x-www-form-urlencoded', |
| 140 | Accept: 'application/json', |
| 141 | }, |
| 142 | body: params.toString(), |
| 143 | timeout: ZOOMINFO_OUTBOUND_FETCH_TIMEOUT_MS, |
| 144 | }, |
| 145 | 'tokenUrl' |
| 146 | ) |
| 147 | |
| 148 | if (!response.ok) { |
| 149 | const text = await response.text().catch(() => '') |
| 150 | logger.warn(`[${requestId}] ZoomInfo token fetch failed (${response.status}): ${text}`) |
| 151 | throw new Error(`ZoomInfo token request failed: HTTP ${response.status}`) |
| 152 | } |
| 153 | |
| 154 | const data = (await response.json()) as { |
| 155 | access_token?: string |
| 156 | expires_in?: number |
| 157 | } |
| 158 | |
| 159 | if (!data.access_token) { |
| 160 | throw new Error('ZoomInfo token response missing access_token') |
| 161 | } |
| 162 | |
| 163 | const expiresInMs = (data.expires_in ?? 3300) * 1000 |
| 164 | rememberToken(cacheKey, { |
| 165 | accessToken: data.access_token, |
| 166 | expiresAt: Date.now() + expiresInMs, |
| 167 | }) |
| 168 | return data.access_token |
| 169 | } |
| 170 | |
| 171 | export function extractZoomInfoError(body: unknown, status: number): string { |
| 172 | if (body && typeof body === 'object') { |
no test coverage detected