import crypto from 'crypto'; import fetch from 'node-fetch'; const DEDUPE_TTL_MS = 24 * 60 * 60 * 1000; export function verifyWebhookSignature(rawBody, signatureHeader, secret) { if (!secret) { return false; } if (!signatureHeader || !rawBody) { return false; } const expected = crypto.createHmac('sha1', secret).update(rawBody).digest('hex'); try { const expectedBuffer = Buffer.from(expected, 'utf8'); const actualBuffer = Buffer.from(signatureHeader, 'utf8'); if (expectedBuffer.length !== actualBuffer.length) { return false; } return crypto.timingSafeEqual(expectedBuffer, actualBuffer); } catch { return false; } } export function createMessageDeduper() { const seen = new Map(); function cleanup() { const now = Date.now(); for (const [messageId, expiresAt] of seen.entries()) { if (expiresAt <= now) { seen.delete(messageId); } } } function has(messageId) { cleanup(); return seen.has(messageId); } function add(messageId) { cleanup(); seen.set(messageId, Date.now() + DEDUPE_TTL_MS); } return { has, add }; } export async function fetchWebexMessage(token, messageId) { const response = await fetch(`https://webexapis.com/v1/messages/${encodeURIComponent(messageId)}`, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json', }, }); const responseText = await response.text(); let data; try { data = responseText ? JSON.parse(responseText) : {}; } catch { throw new Error(`Invalid JSON from Webex messages API: ${responseText}`); } if (!response.ok) { const error = new Error(data.message || responseText || `Webex API error (${response.status})`); error.status = response.status; throw error; } return data; } export function extractMessageFromWebhook(body) { const resource = body?.resource; const event = body?.event; const data = body?.data || {}; if (resource !== 'messages' || event !== 'created') { return { supported: false, reason: 'unsupported_event' }; } return { supported: true, messageId: data.id, roomId: data.roomId, personId: data.personId, personEmail: data.personEmail, text: data.text, parentId: data.parentId, }; }