@sayna-ai/node-sdk
Version:
Node.js SDK for Sayna.ai server-side WebSocket connections
203 lines • 6.36 kB
TypeScript
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