( secretToken: string, signature: string, timestamp: string, body: string )
| 24 | */ |
| 25 | /** Exported for tests — Zoom signs `v0:{timestamp}:{rawBody}`. */ |
| 26 | export function validateZoomSignature( |
| 27 | secretToken: string, |
| 28 | signature: string, |
| 29 | timestamp: string, |
| 30 | body: string |
| 31 | ): boolean { |
| 32 | try { |
| 33 | if (!secretToken || !signature || !timestamp || !body) { |
| 34 | return false |
| 35 | } |
| 36 | |
| 37 | const nowSeconds = Math.floor(Date.now() / 1000) |
| 38 | const requestSeconds = Number.parseInt(timestamp, 10) |
| 39 | if (Number.isNaN(requestSeconds) || Math.abs(nowSeconds - requestSeconds) > 300) { |
| 40 | return false |
| 41 | } |
| 42 | |
| 43 | const message = `v0:${timestamp}:${body}` |
| 44 | const computedHash = hmacSha256Hex(message, secretToken) |
| 45 | const expectedSignature = `v0=${computedHash}` |
| 46 | |
| 47 | return safeCompare(expectedSignature, signature) |
| 48 | } catch (err) { |
| 49 | logger.error('Zoom signature validation error', err) |
| 50 | return false |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | async function resolveZoomChallengeSecrets( |
| 55 | path: string, |
no test coverage detected