UNPKG

@sayna-ai/node-sdk

Version:

Node.js SDK for Sayna.ai server-side WebSocket connections

203 lines 6.36 kB
import type { WebhookSIPOutput } from "./types"; /** * Receives and verifies cryptographically signed webhooks from Sayna SIP service. * * This class handles the secure verification of webhook signatures using HMAC-SHA256, * validates timestamp freshness to prevent replay attacks, and parses the webhook * payload into a strongly-typed WebhookSIPOutput object. * * ## Security Features * * - **HMAC-SHA256 Signature Verification**: Ensures webhook authenticity * - **Constant-Time Comparison**: Prevents timing attack vulnerabilities * - **Replay Protection**: 5-minute timestamp window prevents replay attacks * - **Strict Validation**: Comprehensive checks on all required fields * * ## Usage * * ### Basic Example * * ```typescript * import { WebhookReceiver } from "@sayna-ai/node-sdk"; * * // Initialize with secret (or uses SAYNA_WEBHOOK_SECRET env variable) * const receiver = new WebhookReceiver("your-secret-key-min-16-chars"); * * // In your Express route handler * app.post('/webhook', express.json({ verify: (req, res, buf) => { * req.rawBody = buf.toString('utf8'); * }}), (req, res) => { * try { * const webhook = receiver.receive(req.headers, req.rawBody); * * console.log('Valid webhook received:'); * console.log(' From:', webhook.from_phone_number); * console.log(' To:', webhook.to_phone_number); * console.log(' Room:', webhook.room.name); * console.log(' SIP Host:', webhook.sip_host); * console.log(' Participant:', webhook.participant.identity); * * res.status(200).json({ received: true }); * } catch (error) { * console.error('Webhook verification failed:', error.message); * res.status(401).json({ error: 'Invalid signature' }); * } * }); * ``` * * ### With Environment Variable * * ```typescript * // Set environment variable * process.env.SAYNA_WEBHOOK_SECRET = "your-secret-key"; * * // Receiver automatically uses env variable * const receiver = new WebhookReceiver(); * ``` * * ### Fastify Example * * ```typescript * import Fastify from 'fastify'; * import { WebhookReceiver } from "@sayna-ai/node-sdk"; * * const fastify = Fastify(); * const receiver = new WebhookReceiver(); * * fastify.post('/webhook', { * config: { * rawBody: true * } * }, async (request, reply) => { * try { * const webhook = receiver.receive( * request.headers, * request.rawBody * ); * * // Process webhook... * * return { received: true }; * } catch (error) { * reply.code(401); * return { error: error.message }; * } * }); * ``` * * ## Important Notes * * - **Raw Body Required**: You MUST pass the raw request body string, not the parsed JSON object. * The signature is computed over the exact bytes received, so any formatting changes will * cause verification to fail. * * - **Case-Insensitive Headers**: Header names are case-insensitive in HTTP. This class handles * both `X-Sayna-Signature` and `x-sayna-signature` correctly. * * - **Secret Security**: Never commit secrets to version control. Use environment variables * or a secret management system. * * @see WebhookSIPOutput */ export declare class WebhookReceiver { private readonly secret; /** * Creates a new webhook receiver with the specified signing secret. * * @param secret - HMAC signing secret (min 16 chars, 32+ recommended). * If not provided, uses SAYNA_WEBHOOK_SECRET environment variable. * * @throws {SaynaValidationError} If secret is missing or too short * * @example * ```typescript * // Explicit secret * const receiver = new WebhookReceiver("my-secret-key-at-least-16-chars"); * * // From environment variable * const receiver = new WebhookReceiver(); * ``` */ constructor(secret?: string); /** * Verifies and parses an incoming SIP webhook from Sayna. * * This method performs the following security checks: * 1. Validates presence of required headers * 2. Verifies timestamp is within acceptable window (prevents replay attacks) * 3. Computes HMAC-SHA256 signature over canonical string * 4. Performs constant-time comparison to prevent timing attacks * 5. Parses and validates the webhook payload structure * * @param headers - HTTP request headers (case-insensitive) * @param body - Raw request body as string (not parsed JSON) * * @returns Parsed and validated webhook payload * * @throws {SaynaValidationError} If signature verification fails or payload is invalid * * @example * ```typescript * const receiver = new WebhookReceiver("your-secret"); * * // Express example * app.post('/webhook', express.json({ verify: (req, res, buf) => { * req.rawBody = buf.toString(); * }}), (req, res) => { * const webhook = receiver.receive(req.headers, req.rawBody); * // webhook is now a validated WebhookSIPOutput object * }); * ``` */ receive(headers: Record<string, string | string[] | undefined>, body: string): WebhookSIPOutput; /** * Normalizes HTTP headers to lowercase for case-insensitive access. * Handles both single string values and arrays of strings. * * @internal */ private normalizeHeaders; /** * Retrieves a required header value or throws a validation error. * * @internal */ private getRequiredHeader; /** * Validates the timestamp is within the acceptable window. * * @internal */ private validateTimestamp; /** * Performs constant-time string comparison to prevent timing attacks. * * @internal */ private constantTimeEqual; /** * Parses and validates the webhook payload structure. * * @internal */ private parseAndValidatePayload; /** * Validates the participant object structure. * * @internal */ private validateParticipant; /** * Validates the room object structure. * * @internal */ private validateRoom; /** * Validates a required string field. * * @internal */ private validateStringField; } //# sourceMappingURL=webhook-receiver.d.ts.map