* Handle Zoom endpoint URL validation challenges. * Zoom sends an `endpoint.url_validation` event with a `plainToken` that must * be hashed with the app's secret token and returned alongside the original token.
(
body: unknown,
request: NextRequest,
requestId: string,
path: string,
rawBody?: string
)
| 163 | * be hashed with the app's secret token and returned alongside the original token. |
| 164 | */ |
| 165 | async handleChallenge( |
| 166 | body: unknown, |
| 167 | request: NextRequest, |
| 168 | requestId: string, |
| 169 | path: string, |
| 170 | rawBody?: string |
| 171 | ) { |
| 172 | const obj = body as Record<string, unknown> | null |
| 173 | if (obj?.event !== 'endpoint.url_validation') { |
| 174 | return null |
| 175 | } |
| 176 | |
| 177 | const payload = obj.payload as Record<string, unknown> | undefined |
| 178 | const plainToken = payload?.plainToken as string | undefined |
| 179 | if (!plainToken) { |
| 180 | return null |
| 181 | } |
| 182 | |
| 183 | logger.info(`[${requestId}] Zoom URL validation request received for path: ${path}`) |
| 184 | |
| 185 | const signature = request.headers.get('x-zm-signature') |
| 186 | const timestamp = request.headers.get('x-zm-request-timestamp') |
| 187 | if (!signature || !timestamp) { |
| 188 | logger.warn(`[${requestId}] Zoom challenge request missing signature headers — rejecting`) |
| 189 | return null |
| 190 | } |
| 191 | |
| 192 | const bodyForSignature = |
| 193 | rawBody !== undefined && rawBody !== null ? rawBody : JSON.stringify(body) |
| 194 | |
| 195 | let rows: Array<{ secretToken: string }> = [] |
| 196 | try { |
| 197 | rows = await resolveZoomChallengeSecrets(path, requestId) |
| 198 | } catch (err) { |
| 199 | logger.warn(`[${requestId}] Failed to look up webhook secret for Zoom validation`, err) |
| 200 | return null |
| 201 | } |
| 202 | |
| 203 | for (const row of rows) { |
| 204 | const secretToken = row.secretToken |
| 205 | if ( |
| 206 | secretToken && |
| 207 | validateZoomSignature(secretToken, signature, timestamp, bodyForSignature) |
| 208 | ) { |
| 209 | const hashForValidate = hmacSha256Hex(plainToken, secretToken) |
| 210 | |
| 211 | return NextResponse.json({ |
| 212 | plainToken, |
| 213 | encryptedToken: hashForValidate, |
| 214 | }) |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | logger.warn( |
| 219 | `[${requestId}] Zoom challenge: no matching secret for path ${path} (${rows.length} webhook row(s))` |
| 220 | ) |
| 221 | return null |
| 222 | }, |
nothing calls this directly
no test coverage detected