@sayna-ai/node-sdk 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,6 +52,7 @@ Performs a health check on the Sayna server.
52
52
  **Returns**: `Promise<{ status: string }>` - Status object with "OK" when healthy.
53
53
 
54
54
  **Example**:
55
+
55
56
  ```typescript
56
57
  const health = await client.health();
57
58
  console.log(health.status); // "OK"
@@ -64,10 +65,14 @@ Retrieves the catalogue of text-to-speech voices grouped by provider.
64
65
  **Returns**: `Promise<Record<string, Voice[]>>` - Object where keys are provider names and values are arrays of voice descriptors.
65
66
 
66
67
  **Example**:
68
+
67
69
  ```typescript
68
70
  const voices = await client.getVoices();
69
71
  for (const [provider, voiceList] of Object.entries(voices)) {
70
- console.log(`${provider}:`, voiceList.map(v => v.name));
72
+ console.log(
73
+ `${provider}:`,
74
+ voiceList.map((v) => v.name)
75
+ );
71
76
  }
72
77
  ```
73
78
 
@@ -75,14 +80,15 @@ for (const [provider, voiceList] of Object.entries(voices)) {
75
80
 
76
81
  Synthesizes text into audio using the REST API. This is a standalone method that doesn't require an active WebSocket connection.
77
82
 
78
- | parameter | type | purpose |
79
- | --- | --- | --- |
80
- | `text` | `string` | Text to synthesize (must be non-empty). |
81
- | `ttsConfig` | `TTSConfig` | Text-to-speech provider configuration. |
83
+ | parameter | type | purpose |
84
+ | ----------- | ----------- | --------------------------------------- |
85
+ | `text` | `string` | Text to synthesize (must be non-empty). |
86
+ | `ttsConfig` | `TTSConfig` | Text-to-speech provider configuration. |
82
87
 
83
88
  **Returns**: `Promise<ArrayBuffer>` - Raw audio data.
84
89
 
85
90
  **Example**:
91
+
86
92
  ```typescript
87
93
  const audioBuffer = await client.speakRest("Hello, world!", {
88
94
  provider: "elevenlabs",
@@ -93,7 +99,7 @@ const audioBuffer = await client.speakRest("Hello, world!", {
93
99
  sample_rate: 24000,
94
100
  connection_timeout: 30,
95
101
  request_timeout: 60,
96
- pronunciations: []
102
+ pronunciations: [],
97
103
  });
98
104
  ```
99
105
 
@@ -101,15 +107,16 @@ const audioBuffer = await client.speakRest("Hello, world!", {
101
107
 
102
108
  Issues a LiveKit access token for a participant.
103
109
 
104
- | parameter | type | purpose |
105
- | --- | --- | --- |
106
- | `roomName` | `string` | LiveKit room to join or create. |
107
- | `participantName` | `string` | Display name for the participant. |
110
+ | parameter | type | purpose |
111
+ | --------------------- | -------- | -------------------------------------- |
112
+ | `roomName` | `string` | LiveKit room to join or create. |
113
+ | `participantName` | `string` | Display name for the participant. |
108
114
  | `participantIdentity` | `string` | Unique identifier for the participant. |
109
115
 
110
116
  **Returns**: `Promise<LiveKitTokenResponse>` - Object containing token, room name, participant identity, and LiveKit URL.
111
117
 
112
118
  **Example**:
119
+
113
120
  ```typescript
114
121
  const tokenInfo = await client.getLiveKitToken(
115
122
  "my-room",
@@ -128,13 +135,13 @@ These methods require an active WebSocket connection:
128
135
 
129
136
  ### `new SaynaClient(url, sttConfig, ttsConfig, livekitConfig?, withoutAudio?)`
130
137
 
131
- | parameter | type | purpose |
132
- | --- | --- | --- |
133
- | `url` | `string` | Sayna server URL (http://, https://, ws://, or wss://). |
134
- | `sttConfig` | `STTConfig` | Speech-to-text provider configuration. |
135
- | `ttsConfig` | `TTSConfig` | Text-to-speech provider configuration. |
136
- | `livekitConfig` | `LiveKitConfig` | Optional LiveKit room configuration. |
137
- | `withoutAudio` | `boolean` | Disable audio streaming (defaults to `false`). |
138
+ | parameter | type | purpose |
139
+ | --------------- | --------------- | ------------------------------------------------------- |
140
+ | `url` | `string` | Sayna server URL (http://, https://, ws://, or wss://). |
141
+ | `sttConfig` | `STTConfig` | Speech-to-text provider configuration. |
142
+ | `ttsConfig` | `TTSConfig` | Text-to-speech provider configuration. |
143
+ | `livekitConfig` | `LiveKitConfig` | Optional LiveKit room configuration. |
144
+ | `withoutAudio` | `boolean` | Disable audio streaming (defaults to `false`). |
138
145
 
139
146
  ### `await client.connect()`
140
147
 
@@ -168,11 +175,11 @@ Registers a callback for TTS playback completion events.
168
175
 
169
176
  Sends text to be synthesized as speech.
170
177
 
171
- | parameter | type | default | purpose |
172
- | --- | --- | --- | --- |
173
- | `text` | `string` | - | Text to synthesize. |
174
- | `flush` | `boolean` | `true` | Clear TTS queue before speaking. |
175
- | `allowInterruption` | `boolean` | `true` | Allow speech to be interrupted. |
178
+ | parameter | type | default | purpose |
179
+ | ------------------- | --------- | ------- | -------------------------------- |
180
+ | `text` | `string` | - | Text to synthesize. |
181
+ | `flush` | `boolean` | `true` | Clear TTS queue before speaking. |
182
+ | `allowInterruption` | `boolean` | `true` | Allow speech to be interrupted. |
176
183
 
177
184
  ### `await client.onAudioInput(audioData)`
178
185
 
package/dist/index.js CHANGED
@@ -56,6 +56,7 @@ class SaynaClient {
56
56
  ttsConfig;
57
57
  livekitConfig;
58
58
  withoutAudio;
59
+ apiKey;
59
60
  websocket;
60
61
  isConnected = false;
61
62
  isReady = false;
@@ -71,18 +72,24 @@ class SaynaClient {
71
72
  ttsPlaybackCompleteCallback;
72
73
  readyPromiseResolve;
73
74
  readyPromiseReject;
74
- constructor(url, sttConfig, ttsConfig, livekitConfig, withoutAudio = false) {
75
+ constructor(url, sttConfig, ttsConfig, livekitConfig, withoutAudio = false, apiKey) {
75
76
  if (!url || typeof url !== "string") {
76
77
  throw new SaynaValidationError("URL must be a non-empty string");
77
78
  }
78
79
  if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("ws://") && !url.startsWith("wss://")) {
79
80
  throw new SaynaValidationError("URL must start with http://, https://, ws://, or wss://");
80
81
  }
82
+ if (!withoutAudio) {
83
+ if (!sttConfig || !ttsConfig) {
84
+ throw new SaynaValidationError("sttConfig and ttsConfig are required when withoutAudio=false (audio streaming enabled). " + "Either provide both configs or set withoutAudio=true for non-audio use cases.");
85
+ }
86
+ }
81
87
  this.url = url;
82
88
  this.sttConfig = sttConfig;
83
89
  this.ttsConfig = ttsConfig;
84
90
  this.livekitConfig = livekitConfig;
85
91
  this.withoutAudio = withoutAudio;
92
+ this.apiKey = apiKey ?? process.env.SAYNA_API_KEY;
86
93
  }
87
94
  async connect() {
88
95
  if (this.isConnected) {
@@ -93,7 +100,9 @@ class SaynaClient {
93
100
  this.readyPromiseResolve = resolve;
94
101
  this.readyPromiseReject = reject;
95
102
  try {
96
- this.websocket = new WebSocket(wsUrl);
103
+ const headers = this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : undefined;
104
+ const WebSocketConstructor = WebSocket;
105
+ this.websocket = headers ? new WebSocketConstructor(wsUrl, undefined, { headers }) : new WebSocket(wsUrl);
97
106
  this.websocket.onopen = () => {
98
107
  this.isConnected = true;
99
108
  const configMessage = {
@@ -123,14 +132,18 @@ class SaynaClient {
123
132
  await this.ttsCallback(buffer);
124
133
  }
125
134
  } else {
135
+ if (typeof event.data !== "string") {
136
+ throw new Error("Expected string data for JSON messages");
137
+ }
126
138
  const data = JSON.parse(event.data);
127
139
  await this.handleJsonMessage(data);
128
140
  }
129
141
  } catch (error) {
130
142
  if (this.errorCallback) {
143
+ const errorMessage = error instanceof Error ? error.message : String(error);
131
144
  await this.errorCallback({
132
145
  type: "error",
133
- message: `Failed to process message: ${error instanceof Error ? error.message : String(error)}`
146
+ message: `Failed to process message: ${errorMessage}`
134
147
  });
135
148
  }
136
149
  }
@@ -234,6 +247,10 @@ class SaynaClient {
234
247
  const headers = {
235
248
  ...options.headers
236
249
  };
250
+ const hasAuthHeader = Object.keys(headers).some((key) => key.toLowerCase() === "authorization");
251
+ if (this.apiKey && !hasAuthHeader) {
252
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
253
+ }
237
254
  if (options.method === "POST" && options.body && !headers["Content-Type"]) {
238
255
  headers["Content-Type"] = "application/json";
239
256
  }
@@ -243,10 +260,14 @@ class SaynaClient {
243
260
  headers
244
261
  });
245
262
  if (!response.ok) {
246
- const errorData = await response.json().catch(() => ({
247
- error: response.statusText
248
- }));
249
- throw new SaynaServerError(errorData?.error ?? `Request failed: ${response.status} ${response.statusText}`);
263
+ let errorMessage;
264
+ try {
265
+ const errorData = await response.json();
266
+ errorMessage = errorData && typeof errorData === "object" && "error" in errorData && typeof errorData.error === "string" ? errorData.error : `Request failed: ${response.status} ${response.statusText}`;
267
+ } catch {
268
+ errorMessage = `Request failed: ${response.status} ${response.statusText}`;
269
+ }
270
+ throw new SaynaServerError(errorMessage);
250
271
  }
251
272
  if (responseType === "arrayBuffer") {
252
273
  return await response.arrayBuffer();
@@ -260,7 +281,7 @@ class SaynaClient {
260
281
  throw new SaynaConnectionError(`Failed to fetch from ${endpoint}`, error);
261
282
  }
262
283
  }
263
- async disconnect() {
284
+ disconnect() {
264
285
  if (this.websocket) {
265
286
  this.websocket.onopen = null;
266
287
  this.websocket.onmessage = null;
@@ -273,7 +294,7 @@ class SaynaClient {
273
294
  }
274
295
  this.cleanup();
275
296
  }
276
- async onAudioInput(audioData) {
297
+ onAudioInput(audioData) {
277
298
  if (!this.isConnected || !this.websocket) {
278
299
  throw new SaynaNotConnectedError;
279
300
  }
@@ -310,7 +331,7 @@ class SaynaClient {
310
331
  registerOnTtsPlaybackComplete(callback) {
311
332
  this.ttsPlaybackCompleteCallback = callback;
312
333
  }
313
- async speak(text, flush = true, allowInterruption = true) {
334
+ speak(text, flush = true, allowInterruption = true) {
314
335
  if (!this.isConnected || !this.websocket) {
315
336
  throw new SaynaNotConnectedError;
316
337
  }
@@ -332,7 +353,7 @@ class SaynaClient {
332
353
  throw new SaynaConnectionError("Failed to send speak command", error);
333
354
  }
334
355
  }
335
- async clear() {
356
+ clear() {
336
357
  if (!this.isConnected || !this.websocket) {
337
358
  throw new SaynaNotConnectedError;
338
359
  }
@@ -348,10 +369,10 @@ class SaynaClient {
348
369
  throw new SaynaConnectionError("Failed to send clear command", error);
349
370
  }
350
371
  }
351
- async ttsFlush(allowInterruption = true) {
352
- await this.speak("", true, allowInterruption);
372
+ ttsFlush(allowInterruption = true) {
373
+ this.speak("", true, allowInterruption);
353
374
  }
354
- async sendMessage(message, role, topic, debug) {
375
+ sendMessage(message, role, topic, debug) {
355
376
  if (!this.isConnected || !this.websocket) {
356
377
  throw new SaynaNotConnectedError;
357
378
  }
@@ -378,16 +399,16 @@ class SaynaClient {
378
399
  }
379
400
  }
380
401
  async health() {
381
- return await this.fetchFromSayna("");
402
+ return this.fetchFromSayna("");
382
403
  }
383
404
  async getVoices() {
384
- return await this.fetchFromSayna("voices");
405
+ return this.fetchFromSayna("voices");
385
406
  }
386
407
  async speakRest(text, ttsConfig) {
387
408
  if (!text || text.trim().length === 0) {
388
409
  throw new SaynaValidationError("Text cannot be empty");
389
410
  }
390
- return await this.fetchFromSayna("speak", {
411
+ return this.fetchFromSayna("speak", {
391
412
  method: "POST",
392
413
  body: JSON.stringify({
393
414
  text,
@@ -405,7 +426,7 @@ class SaynaClient {
405
426
  if (!participantIdentity || participantIdentity.trim().length === 0) {
406
427
  throw new SaynaValidationError("participant_identity cannot be empty");
407
428
  }
408
- return await this.fetchFromSayna("livekit/token", {
429
+ return this.fetchFromSayna("livekit/token", {
409
430
  method: "POST",
410
431
  body: JSON.stringify({
411
432
  room_name: roomName,
@@ -433,15 +454,139 @@ class SaynaClient {
433
454
  return this._saynaParticipantName;
434
455
  }
435
456
  }
457
+ // src/webhook-receiver.ts
458
+ import { createHmac, timingSafeEqual } from "crypto";
459
+ var MIN_SECRET_LENGTH = 16;
460
+ var TIMESTAMP_TOLERANCE_SECONDS = 300;
461
+
462
+ class WebhookReceiver {
463
+ secret;
464
+ constructor(secret) {
465
+ const effectiveSecret = secret ?? process.env.SAYNA_WEBHOOK_SECRET;
466
+ if (!effectiveSecret) {
467
+ throw new SaynaValidationError("Webhook secret is required. Provide it as a constructor parameter or set SAYNA_WEBHOOK_SECRET environment variable.");
468
+ }
469
+ const trimmedSecret = effectiveSecret.trim();
470
+ if (trimmedSecret.length < MIN_SECRET_LENGTH) {
471
+ throw new SaynaValidationError(`Webhook secret must be at least ${MIN_SECRET_LENGTH} characters long. ` + `Received ${trimmedSecret.length} characters. ` + `Generate a secure secret with: openssl rand -hex 32`);
472
+ }
473
+ this.secret = trimmedSecret;
474
+ }
475
+ receive(headers, body) {
476
+ const normalizedHeaders = this.normalizeHeaders(headers);
477
+ const signature = this.getRequiredHeader(normalizedHeaders, "x-sayna-signature");
478
+ const timestamp = this.getRequiredHeader(normalizedHeaders, "x-sayna-timestamp");
479
+ const eventId = this.getRequiredHeader(normalizedHeaders, "x-sayna-event-id");
480
+ if (!signature.startsWith("v1=")) {
481
+ throw new SaynaValidationError("Invalid signature format. Expected 'v1=<hex>' but got: " + signature.substring(0, 10) + "...");
482
+ }
483
+ const signatureHex = signature.substring(3);
484
+ if (!/^[0-9a-f]{64}$/i.test(signatureHex)) {
485
+ throw new SaynaValidationError("Invalid signature: must be 64 hex characters (HMAC-SHA256)");
486
+ }
487
+ this.validateTimestamp(timestamp);
488
+ const canonical = `v1:${timestamp}:${eventId}:${body}`;
489
+ const hmac = createHmac("sha256", this.secret);
490
+ hmac.update(canonical, "utf8");
491
+ const expectedSignature = hmac.digest("hex");
492
+ if (!this.constantTimeEqual(signatureHex, expectedSignature)) {
493
+ throw new SaynaValidationError("Signature verification failed. The webhook may have been tampered with or the secret is incorrect.");
494
+ }
495
+ return this.parseAndValidatePayload(body);
496
+ }
497
+ normalizeHeaders(headers) {
498
+ const normalized = {};
499
+ for (const [key, value] of Object.entries(headers)) {
500
+ if (value !== undefined) {
501
+ const stringValue = Array.isArray(value) ? value[0] : value;
502
+ if (stringValue) {
503
+ normalized[key.toLowerCase()] = stringValue;
504
+ }
505
+ }
506
+ }
507
+ return normalized;
508
+ }
509
+ getRequiredHeader(headers, name) {
510
+ const value = headers[name.toLowerCase()];
511
+ if (!value) {
512
+ throw new SaynaValidationError(`Missing required header: ${name}`);
513
+ }
514
+ return value;
515
+ }
516
+ validateTimestamp(timestampStr) {
517
+ const timestamp = Number(timestampStr);
518
+ if (isNaN(timestamp)) {
519
+ throw new SaynaValidationError(`Invalid timestamp format: expected Unix seconds but got '${timestampStr}'`);
520
+ }
521
+ const now = Math.floor(Date.now() / 1000);
522
+ const diff = Math.abs(now - timestamp);
523
+ if (diff > TIMESTAMP_TOLERANCE_SECONDS) {
524
+ throw new SaynaValidationError(`Timestamp outside replay protection window. ` + `Difference: ${diff} seconds (max allowed: ${TIMESTAMP_TOLERANCE_SECONDS}). ` + `This webhook may be a replay attack or there may be significant clock skew.`);
525
+ }
526
+ }
527
+ constantTimeEqual(a, b) {
528
+ if (a.length !== b.length) {
529
+ return false;
530
+ }
531
+ const bufA = Buffer.from(a, "utf8");
532
+ const bufB = Buffer.from(b, "utf8");
533
+ return timingSafeEqual(bufA, bufB);
534
+ }
535
+ parseAndValidatePayload(body) {
536
+ let payload;
537
+ try {
538
+ payload = JSON.parse(body);
539
+ } catch (error) {
540
+ throw new SaynaValidationError(`Invalid JSON payload: ${error instanceof Error ? error.message : String(error)}`);
541
+ }
542
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
543
+ throw new SaynaValidationError("Webhook payload must be a JSON object");
544
+ }
545
+ const data = payload;
546
+ this.validateParticipant(data.participant);
547
+ this.validateRoom(data.room);
548
+ this.validateStringField(data, "from_phone_number", "from_phone_number");
549
+ this.validateStringField(data, "to_phone_number", "to_phone_number");
550
+ this.validateStringField(data, "room_prefix", "room_prefix");
551
+ this.validateStringField(data, "sip_host", "sip_host");
552
+ return data;
553
+ }
554
+ validateParticipant(participant) {
555
+ if (!participant || typeof participant !== "object" || Array.isArray(participant)) {
556
+ throw new SaynaValidationError("Webhook payload missing required field 'participant' (must be an object)");
557
+ }
558
+ const p = participant;
559
+ this.validateStringField(p, "identity", "participant.identity");
560
+ this.validateStringField(p, "sid", "participant.sid");
561
+ if (p.name !== undefined && typeof p.name !== "string") {
562
+ throw new SaynaValidationError("Field 'participant.name' must be a string if present");
563
+ }
564
+ }
565
+ validateRoom(room) {
566
+ if (!room || typeof room !== "object" || Array.isArray(room)) {
567
+ throw new SaynaValidationError("Webhook payload missing required field 'room' (must be an object)");
568
+ }
569
+ const r = room;
570
+ this.validateStringField(r, "name", "room.name");
571
+ this.validateStringField(r, "sid", "room.sid");
572
+ }
573
+ validateStringField(obj, field, displayName) {
574
+ const value = obj[field];
575
+ if (typeof value !== "string" || value.length === 0) {
576
+ throw new SaynaValidationError(`Webhook payload missing required field '${displayName}' (must be a non-empty string)`);
577
+ }
578
+ }
579
+ }
436
580
 
437
581
  // src/index.ts
438
- async function saynaConnect(url, sttConfig, ttsConfig, livekitConfig, withoutAudio = false) {
439
- const client = new SaynaClient(url, sttConfig, ttsConfig, livekitConfig, withoutAudio);
582
+ async function saynaConnect(url, sttConfig, ttsConfig, livekitConfig, withoutAudio = false, apiKey) {
583
+ const client = new SaynaClient(url, sttConfig, ttsConfig, livekitConfig, withoutAudio, apiKey);
440
584
  await client.connect();
441
585
  return client;
442
586
  }
443
587
  export {
444
588
  saynaConnect,
589
+ WebhookReceiver,
445
590
  SaynaValidationError,
446
591
  SaynaServerError,
447
592
  SaynaNotReadyError,
@@ -451,4 +596,4 @@ export {
451
596
  SaynaClient
452
597
  };
453
598
 
454
- //# debugId=D827CC25BF1F390D64756E2164756E21
599
+ //# debugId=61BF4B0E0558D09664756E2164756E21
package/dist/index.js.map CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/errors.ts", "../src/sayna-client.ts", "../src/index.ts"],
3
+ "sources": ["../src/errors.ts", "../src/sayna-client.ts", "../src/webhook-receiver.ts", "../src/index.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\n * Base error class for all Sayna SDK errors.\n */\nexport class SaynaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SaynaError\";\n Object.setPrototypeOf(this, SaynaError.prototype);\n }\n}\n\n/**\n * Error thrown when attempting to use the client before it's connected.\n */\nexport class SaynaNotConnectedError extends SaynaError {\n constructor(message: string = \"Not connected to Sayna WebSocket\") {\n super(message);\n this.name = \"SaynaNotConnectedError\";\n Object.setPrototypeOf(this, SaynaNotConnectedError.prototype);\n }\n}\n\n/**\n * Error thrown when attempting operations before the client is ready.\n */\nexport class SaynaNotReadyError extends SaynaError {\n constructor(\n message: string = \"Sayna voice providers are not ready. Wait for the connection to be established.\"\n ) {\n super(message);\n this.name = \"SaynaNotReadyError\";\n Object.setPrototypeOf(this, SaynaNotReadyError.prototype);\n }\n}\n\n/**\n * Error thrown when WebSocket connection fails.\n */\nexport class SaynaConnectionError extends SaynaError {\n public override readonly cause?: unknown;\n\n constructor(message: string, cause?: unknown) {\n super(message);\n this.name = \"SaynaConnectionError\";\n this.cause = cause;\n Object.setPrototypeOf(this, SaynaConnectionError.prototype);\n }\n}\n\n/**\n * Error thrown when invalid parameters are provided.\n */\nexport class SaynaValidationError extends SaynaError {\n constructor(message: string) {\n super(message);\n this.name = \"SaynaValidationError\";\n Object.setPrototypeOf(this, SaynaValidationError.prototype);\n }\n}\n\n/**\n * Error thrown when the server returns an error.\n */\nexport class SaynaServerError extends SaynaError {\n constructor(message: string) {\n super(message);\n this.name = \"SaynaServerError\";\n Object.setPrototypeOf(this, SaynaServerError.prototype);\n }\n}\n",
6
- "import type {\n STTConfig,\n TTSConfig,\n LiveKitConfig,\n ConfigMessage,\n SpeakMessage,\n ClearMessage,\n SendMessageMessage,\n ReadyMessage,\n STTResultMessage,\n ErrorMessage,\n MessageMessage,\n ParticipantDisconnectedMessage,\n OutgoingMessage,\n SaynaMessage,\n Participant,\n TTSPlaybackCompleteMessage,\n VoicesResponse,\n HealthResponse,\n LiveKitTokenResponse,\n} from \"./types\";\nimport {\n SaynaNotConnectedError,\n SaynaNotReadyError,\n SaynaConnectionError,\n SaynaValidationError,\n SaynaServerError,\n} from \"./errors\";\n\n// Node.js 18+ has native fetch support\ndeclare const fetch: typeof globalThis.fetch;\n\n/**\n * Event handler for speech-to-text results.\n */\nexport type STTResultHandler = (\n result: STTResultMessage\n) => void | Promise<void>;\n\n/**\n * Event handler for text-to-speech audio data.\n */\nexport type TTSAudioHandler = (audio: ArrayBuffer) => void | Promise<void>;\n\n/**\n * Event handler for error messages.\n */\nexport type ErrorHandler = (error: ErrorMessage) => void | Promise<void>;\n\n/**\n * Event handler for participant messages.\n */\nexport type MessageHandler = (message: SaynaMessage) => void | Promise<void>;\n\n/**\n * Event handler for participant disconnections.\n */\nexport type ParticipantDisconnectedHandler = (\n participant: Participant\n) => void | Promise<void>;\n\n/**\n * Event handler for TTS playback completion.\n */\nexport type TTSPlaybackCompleteHandler = (\n timestamp: number\n) => void | Promise<void>;\n\n/**\n * Client for connecting to Sayna WebSocket server for real-time voice interactions.\n *\n * @example\n * ```typescript\n * const client = await saynaConnect(\n * \"https://api.sayna.ai\",\n * sttConfig,\n * ttsConfig\n * );\n *\n * client.registerOnSttResult((result) => {\n * console.log(\"Transcription:\", result.transcript);\n * });\n *\n * await client.speak(\"Hello, world!\");\n * ```\n */\nexport class SaynaClient {\n private url: string;\n private sttConfig: STTConfig;\n private ttsConfig: TTSConfig;\n private livekitConfig?: LiveKitConfig;\n private withoutAudio: boolean;\n private websocket?: WebSocket;\n private isConnected: boolean = false;\n private isReady: boolean = false;\n private _livekitRoomName?: string;\n private _livekitUrl?: string;\n private _saynaParticipantIdentity?: string;\n private _saynaParticipantName?: string;\n private sttCallback?: STTResultHandler;\n private ttsCallback?: TTSAudioHandler;\n private errorCallback?: ErrorHandler;\n private messageCallback?: MessageHandler;\n private participantDisconnectedCallback?: ParticipantDisconnectedHandler;\n private ttsPlaybackCompleteCallback?: TTSPlaybackCompleteHandler;\n private readyPromiseResolve?: () => void;\n private readyPromiseReject?: (error: Error) => void;\n\n /**\n * Creates a new SaynaClient instance.\n *\n * @param url - The Sayna server URL (e.g., \"https://api.sayna.ai\")\n * @param sttConfig - Speech-to-text configuration\n * @param ttsConfig - Text-to-speech configuration\n * @param livekitConfig - Optional LiveKit room configuration\n * @param withoutAudio - If true, disables audio streaming (default: false)\n *\n * @throws {SaynaValidationError} If URL or configurations are invalid\n */\n constructor(\n url: string,\n sttConfig: STTConfig,\n ttsConfig: TTSConfig,\n livekitConfig?: LiveKitConfig,\n withoutAudio: boolean = false\n ) {\n // Validate URL\n if (!url || typeof url !== \"string\") {\n throw new SaynaValidationError(\"URL must be a non-empty string\");\n }\n if (\n !url.startsWith(\"http://\") &&\n !url.startsWith(\"https://\") &&\n !url.startsWith(\"ws://\") &&\n !url.startsWith(\"wss://\")\n ) {\n throw new SaynaValidationError(\n \"URL must start with http://, https://, ws://, or wss://\"\n );\n }\n\n this.url = url;\n this.sttConfig = sttConfig;\n this.ttsConfig = ttsConfig;\n this.livekitConfig = livekitConfig;\n this.withoutAudio = withoutAudio;\n }\n\n /**\n * Establishes connection to the Sayna WebSocket server.\n *\n * @throws {SaynaConnectionError} If connection fails\n * @returns Promise that resolves when the connection is ready\n */\n async connect(): Promise<void> {\n if (this.isConnected) {\n return;\n }\n\n // Convert HTTP(S) URL to WebSocket URL\n const wsUrl =\n this.url.replace(/^https:\\/\\//, \"wss://\").replace(/^http:\\/\\//, \"ws://\") +\n (this.url.endsWith(\"/\") ? \"ws\" : \"/ws\");\n\n return new Promise((resolve, reject) => {\n this.readyPromiseResolve = resolve;\n this.readyPromiseReject = reject;\n\n try {\n this.websocket = new WebSocket(wsUrl);\n\n this.websocket.onopen = () => {\n this.isConnected = true;\n\n // Send initial configuration\n const configMessage: ConfigMessage = {\n type: \"config\",\n stt_config: this.sttConfig,\n tts_config: this.ttsConfig,\n livekit: this.livekitConfig,\n audio: !this.withoutAudio,\n };\n\n try {\n if (this.websocket) {\n this.websocket.send(JSON.stringify(configMessage));\n }\n } catch (error) {\n this.cleanup();\n const err = new SaynaConnectionError(\n \"Failed to send configuration\",\n error\n );\n if (this.readyPromiseReject) {\n this.readyPromiseReject(err);\n }\n }\n };\n\n this.websocket.onmessage = async (event) => {\n try {\n if (\n event.data instanceof Blob ||\n event.data instanceof ArrayBuffer\n ) {\n // Binary TTS audio data\n const buffer =\n event.data instanceof Blob\n ? await event.data.arrayBuffer()\n : event.data;\n if (this.ttsCallback) {\n await this.ttsCallback(buffer);\n }\n } else {\n // JSON control messages\n const data = JSON.parse(event.data) as OutgoingMessage;\n await this.handleJsonMessage(data);\n }\n } catch (error) {\n // Log parse errors but don't break the connection\n if (this.errorCallback) {\n await this.errorCallback({\n type: \"error\",\n message: `Failed to process message: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n }\n };\n\n this.websocket.onerror = () => {\n const error = new SaynaConnectionError(\"WebSocket connection error\");\n if (this.readyPromiseReject && !this.isReady) {\n this.readyPromiseReject(error);\n }\n };\n\n this.websocket.onclose = (event) => {\n const wasReady = this.isReady;\n this.cleanup();\n\n // If connection closed before ready, reject the promise\n if (!wasReady && this.readyPromiseReject) {\n this.readyPromiseReject(\n new SaynaConnectionError(\n `WebSocket closed before ready (code: ${event.code}, reason: ${event.reason || \"none\"})`\n )\n );\n }\n };\n } catch (error) {\n reject(new SaynaConnectionError(\"Failed to create WebSocket\", error));\n }\n });\n }\n\n /**\n * Handles incoming JSON messages from the WebSocket.\n * @internal\n */\n private async handleJsonMessage(data: OutgoingMessage): Promise<void> {\n const messageType = data.type;\n\n try {\n switch (messageType) {\n case \"ready\": {\n const readyMsg = data as ReadyMessage;\n this.isReady = true;\n this._livekitRoomName = readyMsg.livekit_room_name;\n this._livekitUrl = readyMsg.livekit_url;\n this._saynaParticipantIdentity = readyMsg.sayna_participant_identity;\n this._saynaParticipantName = readyMsg.sayna_participant_name;\n if (this.readyPromiseResolve) {\n this.readyPromiseResolve();\n }\n break;\n }\n\n case \"stt_result\": {\n const sttResult = data as STTResultMessage;\n if (this.sttCallback) {\n await this.sttCallback(sttResult);\n }\n break;\n }\n\n case \"error\": {\n const errorMsg = data as ErrorMessage;\n if (this.errorCallback) {\n await this.errorCallback(errorMsg);\n }\n if (this.readyPromiseReject && !this.isReady) {\n this.readyPromiseReject(new SaynaServerError(errorMsg.message));\n }\n break;\n }\n\n case \"message\": {\n const messageData = data as MessageMessage;\n if (this.messageCallback) {\n await this.messageCallback(messageData.message);\n }\n break;\n }\n\n case \"participant_disconnected\": {\n const participantMsg = data as ParticipantDisconnectedMessage;\n if (this.participantDisconnectedCallback) {\n await this.participantDisconnectedCallback(\n participantMsg.participant\n );\n }\n break;\n }\n\n case \"tts_playback_complete\": {\n const ttsPlaybackCompleteMsg = data as TTSPlaybackCompleteMessage;\n if (this.ttsPlaybackCompleteCallback) {\n await this.ttsPlaybackCompleteCallback(ttsPlaybackCompleteMsg.timestamp);\n }\n break;\n }\n }\n } catch (error) {\n // Notify error callback if handler fails\n if (this.errorCallback) {\n await this.errorCallback({\n type: \"error\",\n message: `Handler error: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n }\n }\n\n /**\n * Cleans up internal state.\n * @internal\n */\n private cleanup(): void {\n this.isConnected = false;\n this.isReady = false;\n this._livekitRoomName = undefined;\n this._livekitUrl = undefined;\n this._saynaParticipantIdentity = undefined;\n this._saynaParticipantName = undefined;\n }\n\n /**\n * Converts WebSocket URL to HTTP URL for REST API calls.\n * @internal\n */\n private getHttpUrl(): string {\n return this.url\n .replace(/^ws:\\/\\//, \"http://\")\n .replace(/^wss:\\/\\//, \"https://\");\n }\n\n /**\n * Generic fetch helper for making REST API calls to Sayna server.\n * Handles URL construction, headers, error responses, and type conversion.\n * @internal\n *\n * @param endpoint - API endpoint path (e.g., \"/voices\", \"/speak\")\n * @param options - Fetch options including method, body, headers, etc.\n * @param responseType - Expected response type: \"json\" or \"arrayBuffer\"\n * @returns Promise resolving to the parsed response\n * @throws {SaynaConnectionError} If the network request fails\n * @throws {SaynaServerError} If the server returns an error response\n */\n private async fetchFromSayna<T>(\n endpoint: string,\n options: RequestInit = {},\n responseType: \"json\" | \"arrayBuffer\" = \"json\"\n ): Promise<T> {\n const httpUrl = this.getHttpUrl();\n const url = `${httpUrl}${httpUrl.endsWith(\"/\") ? \"\" : \"/\"}${endpoint.startsWith(\"/\") ? endpoint.slice(1) : endpoint}`;\n\n // Merge default headers with user-provided headers\n const headers: Record<string, string> = {\n ...(options.headers as Record<string, string>),\n };\n\n // Add Content-Type for JSON requests if not already set\n if (options.method === \"POST\" && options.body && !headers[\"Content-Type\"]) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n });\n\n if (!response.ok) {\n // Try to parse error message from JSON response\n const errorData = await response.json().catch(() => ({\n error: response.statusText,\n }));\n throw new SaynaServerError(\n errorData?.error ?? `Request failed: ${response.status} ${response.statusText}`\n );\n }\n\n // Parse response based on expected type\n if (responseType === \"arrayBuffer\") {\n return (await response.arrayBuffer()) as T;\n } else {\n return (await response.json()) as T;\n }\n } catch (error) {\n // Re-throw SaynaServerError as-is\n if (error instanceof SaynaServerError) {\n throw error;\n }\n // Wrap other errors in SaynaConnectionError\n throw new SaynaConnectionError(\n `Failed to fetch from ${endpoint}`,\n error\n );\n }\n }\n\n /**\n * Disconnects from the Sayna WebSocket server and cleans up resources.\n */\n async disconnect(): Promise<void> {\n if (this.websocket) {\n // Remove event handlers to prevent memory leaks\n this.websocket.onopen = null;\n this.websocket.onmessage = null;\n this.websocket.onerror = null;\n this.websocket.onclose = null;\n\n if (this.websocket.readyState === WebSocket.OPEN) {\n this.websocket.close(1000, \"Client disconnect\");\n }\n\n this.websocket = undefined;\n }\n\n this.cleanup();\n }\n\n /**\n * Sends audio data to the server for speech recognition.\n *\n * @param audioData - Raw audio data as ArrayBuffer\n * @throws {SaynaNotConnectedError} If not connected\n * @throws {SaynaNotReadyError} If connection is not ready\n */\n async onAudioInput(audioData: ArrayBuffer): Promise<void> {\n if (!this.isConnected || !this.websocket) {\n throw new SaynaNotConnectedError();\n }\n\n if (!this.isReady) {\n throw new SaynaNotReadyError();\n }\n\n if (!(audioData instanceof ArrayBuffer)) {\n throw new SaynaValidationError(\"audioData must be an ArrayBuffer\");\n }\n\n if (audioData.byteLength === 0) {\n throw new SaynaValidationError(\"audioData cannot be empty\");\n }\n\n try {\n this.websocket.send(audioData);\n } catch (error) {\n throw new SaynaConnectionError(\"Failed to send audio data\", error);\n }\n }\n\n /**\n * Registers a callback for speech-to-text results.\n *\n * @param callback - Function to call when STT results are received\n */\n registerOnSttResult(callback: STTResultHandler): void {\n this.sttCallback = callback;\n }\n\n /**\n * Registers a callback for text-to-speech audio data.\n *\n * @param callback - Function to call when TTS audio is received\n */\n registerOnTtsAudio(callback: TTSAudioHandler): void {\n this.ttsCallback = callback;\n }\n\n /**\n * Registers a callback for error messages.\n *\n * @param callback - Function to call when errors occur\n */\n registerOnError(callback: ErrorHandler): void {\n this.errorCallback = callback;\n }\n\n /**\n * Registers a callback for participant messages.\n *\n * @param callback - Function to call when messages are received\n */\n registerOnMessage(callback: MessageHandler): void {\n this.messageCallback = callback;\n }\n\n /**\n * Registers a callback for participant disconnection events.\n *\n * @param callback - Function to call when a participant disconnects\n */\n registerOnParticipantDisconnected(\n callback: ParticipantDisconnectedHandler\n ): void {\n this.participantDisconnectedCallback = callback;\n }\n\n /**\n * Registers a callback for TTS playback completion.\n *\n * @param callback - Function to call when TTS playback is complete\n */\n registerOnTtsPlaybackComplete(callback: TTSPlaybackCompleteHandler): void {\n this.ttsPlaybackCompleteCallback = callback;\n }\n\n /**\n * Sends text to be synthesized as speech.\n *\n * @param text - Text to synthesize\n * @param flush - Whether to flush the TTS queue before speaking (default: true)\n * @param allowInterruption - Whether this speech can be interrupted (default: true)\n * @throws {SaynaNotConnectedError} If not connected\n * @throws {SaynaNotReadyError} If connection is not ready\n * @throws {SaynaValidationError} If text is not a string\n */\n async speak(\n text: string,\n flush: boolean = true,\n allowInterruption: boolean = true\n ): Promise<void> {\n if (!this.isConnected || !this.websocket) {\n throw new SaynaNotConnectedError();\n }\n\n if (!this.isReady) {\n throw new SaynaNotReadyError();\n }\n\n if (typeof text !== \"string\") {\n throw new SaynaValidationError(\"text must be a string\");\n }\n\n try {\n const speakMessage: SpeakMessage = {\n type: \"speak\",\n text,\n flush,\n allow_interruption: allowInterruption,\n };\n this.websocket.send(JSON.stringify(speakMessage));\n } catch (error) {\n throw new SaynaConnectionError(\"Failed to send speak command\", error);\n }\n }\n\n /**\n * Clears the text-to-speech queue.\n *\n * @throws {SaynaNotConnectedError} If not connected\n * @throws {SaynaNotReadyError} If connection is not ready\n */\n async clear(): Promise<void> {\n if (!this.isConnected || !this.websocket) {\n throw new SaynaNotConnectedError();\n }\n\n if (!this.isReady) {\n throw new SaynaNotReadyError();\n }\n\n try {\n const clearMessage: ClearMessage = {\n type: \"clear\",\n };\n this.websocket.send(JSON.stringify(clearMessage));\n } catch (error) {\n throw new SaynaConnectionError(\"Failed to send clear command\", error);\n }\n }\n\n /**\n * Flushes the TTS queue by sending an empty speak command.\n *\n * @param allowInterruption - Whether the flush can be interrupted (default: true)\n * @throws {SaynaNotConnectedError} If not connected\n * @throws {SaynaNotReadyError} If connection is not ready\n */\n async ttsFlush(allowInterruption: boolean = true): Promise<void> {\n await this.speak(\"\", true, allowInterruption);\n }\n\n /**\n * Sends a message to the Sayna session.\n *\n * @param message - Message content\n * @param role - Message role (e.g., \"user\", \"assistant\")\n * @param topic - Optional topic identifier\n * @param debug - Optional debug metadata\n * @throws {SaynaNotConnectedError} If not connected\n * @throws {SaynaNotReadyError} If connection is not ready\n * @throws {SaynaValidationError} If parameters are invalid\n */\n async sendMessage(\n message: string,\n role: string,\n topic?: string,\n debug?: Record<string, unknown>\n ): Promise<void> {\n if (!this.isConnected || !this.websocket) {\n throw new SaynaNotConnectedError();\n }\n\n if (!this.isReady) {\n throw new SaynaNotReadyError();\n }\n\n if (typeof message !== \"string\") {\n throw new SaynaValidationError(\"message must be a string\");\n }\n\n if (typeof role !== \"string\") {\n throw new SaynaValidationError(\"role must be a string\");\n }\n\n try {\n const sendMsg: SendMessageMessage = {\n type: \"send_message\",\n message,\n role,\n topic,\n debug,\n };\n this.websocket.send(JSON.stringify(sendMsg));\n } catch (error) {\n throw new SaynaConnectionError(\"Failed to send message\", error);\n }\n }\n\n /**\n * Performs a health check on the Sayna server.\n *\n * @returns Promise that resolves with the health status\n * @throws {SaynaConnectionError} If the request fails\n * @throws {SaynaServerError} If server returns an error\n *\n * @example\n * ```typescript\n * const health = await client.health();\n * console.log(health.status); // \"OK\"\n * ```\n */\n async health(): Promise<HealthResponse> {\n return await this.fetchFromSayna<HealthResponse>(\"\");\n }\n\n /**\n * Retrieves the catalogue of text-to-speech voices grouped by provider.\n *\n * @returns Promise that resolves with voices organized by provider\n * @throws {SaynaConnectionError} If the request fails\n * @throws {SaynaServerError} If server returns an error\n *\n * @example\n * ```typescript\n * const voices = await client.getVoices();\n * for (const [provider, voiceList] of Object.entries(voices)) {\n * console.log(`${provider}:`, voiceList.map(v => v.name));\n * }\n * ```\n */\n async getVoices(): Promise<VoicesResponse> {\n return await this.fetchFromSayna<VoicesResponse>(\"voices\");\n }\n\n /**\n * Synthesizes text into audio using the REST API endpoint.\n * This is a standalone synthesis method that doesn't require an active WebSocket connection.\n *\n * @param text - Text to synthesize\n * @param ttsConfig - Text-to-speech configuration\n * @returns Promise that resolves with the audio data as ArrayBuffer\n * @throws {SaynaValidationError} If text is empty\n * @throws {SaynaConnectionError} If the request fails\n * @throws {SaynaServerError} If server returns an error\n *\n * @example\n * ```typescript\n * const audioBuffer = await client.speakRest(\"Hello, world!\", {\n * provider: \"elevenlabs\",\n * voice_id: \"21m00Tcm4TlvDq8ikWAM\",\n * model: \"eleven_turbo_v2\",\n * speaking_rate: 1.0,\n * audio_format: \"mp3\",\n * sample_rate: 24000,\n * connection_timeout: 30,\n * request_timeout: 60,\n * pronunciations: []\n * });\n * ```\n */\n async speakRest(text: string, ttsConfig: TTSConfig): Promise<ArrayBuffer> {\n if (!text || text.trim().length === 0) {\n throw new SaynaValidationError(\"Text cannot be empty\");\n }\n\n return await this.fetchFromSayna<ArrayBuffer>(\n \"speak\",\n {\n method: \"POST\",\n body: JSON.stringify({\n text,\n tts_config: ttsConfig,\n }),\n },\n \"arrayBuffer\"\n );\n }\n\n /**\n * Issues a LiveKit access token for a participant.\n *\n * @param roomName - LiveKit room to join or create\n * @param participantName - Display name assigned to the participant\n * @param participantIdentity - Unique identifier for the participant\n * @returns Promise that resolves with the LiveKit token and connection details\n * @throws {SaynaValidationError} If any parameter is empty\n * @throws {SaynaConnectionError} If the request fails\n * @throws {SaynaServerError} If server returns an error\n *\n * @example\n * ```typescript\n * const tokenInfo = await client.getLiveKitToken(\n * \"my-room\",\n * \"John Doe\",\n * \"user-123\"\n * );\n * console.log(\"Token:\", tokenInfo.token);\n * console.log(\"LiveKit URL:\", tokenInfo.livekit_url);\n * ```\n */\n async getLiveKitToken(\n roomName: string,\n participantName: string,\n participantIdentity: string\n ): Promise<LiveKitTokenResponse> {\n if (!roomName || roomName.trim().length === 0) {\n throw new SaynaValidationError(\"room_name cannot be empty\");\n }\n\n if (!participantName || participantName.trim().length === 0) {\n throw new SaynaValidationError(\"participant_name cannot be empty\");\n }\n\n if (!participantIdentity || participantIdentity.trim().length === 0) {\n throw new SaynaValidationError(\"participant_identity cannot be empty\");\n }\n\n return await this.fetchFromSayna<LiveKitTokenResponse>(\n \"livekit/token\",\n {\n method: \"POST\",\n body: JSON.stringify({\n room_name: roomName,\n participant_name: participantName,\n participant_identity: participantIdentity,\n }),\n }\n );\n }\n\n /**\n * Whether the client is ready to send/receive data.\n */\n get ready(): boolean {\n return this.isReady;\n }\n\n /**\n * Whether the client is connected to the WebSocket.\n */\n get connected(): boolean {\n return this.isConnected;\n }\n\n /**\n * LiveKit room name acknowledged by the server, if available.\n */\n get livekitRoomName(): string | undefined {\n return this._livekitRoomName;\n }\n\n /**\n * LiveKit WebSocket URL configured on the server, if available.\n */\n get livekitUrl(): string | undefined {\n return this._livekitUrl;\n }\n\n /**\n * Identity assigned to the agent participant when LiveKit is enabled, if available.\n */\n get saynaParticipantIdentity(): string | undefined {\n return this._saynaParticipantIdentity;\n }\n\n /**\n * Display name assigned to the agent participant when LiveKit is enabled, if available.\n */\n get saynaParticipantName(): string | undefined {\n return this._saynaParticipantName;\n }\n}\n",
7
- "import { SaynaClient } from \"./sayna-client\";\nimport { STTConfig, TTSConfig, LiveKitConfig } from \"./types\";\n\nexport * from \"./sayna-client\";\nexport * from \"./types\";\nexport * from \"./errors\";\n\n/**\n * Creates and connects a new SaynaClient instance.\n *\n * This is the recommended way to create a Sayna client. It handles both\n * instantiation and connection, returning a ready-to-use client.\n *\n * @param url - The Sayna server URL (e.g., \"https://api.sayna.ai\")\n * @param sttConfig - Speech-to-text configuration\n * @param ttsConfig - Text-to-speech configuration\n * @param livekitConfig - Optional LiveKit room configuration\n * @param withoutAudio - If true, disables audio streaming (default: false)\n *\n * @returns Promise that resolves to a connected SaynaClient\n *\n * @throws {SaynaValidationError} If parameters are invalid\n * @throws {SaynaConnectionError} If connection fails\n * @throws {SaynaServerError} If server returns an error during setup\n *\n * @example\n * ```typescript\n * import { saynaConnect } from \"@sayna/node-sdk\";\n *\n * const client = await saynaConnect(\n * \"https://api.sayna.ai\",\n * {\n * provider: \"deepgram\",\n * language: \"en-US\",\n * sample_rate: 16000,\n * channels: 1,\n * punctuation: true,\n * encoding: \"linear16\",\n * model: \"nova-2\"\n * },\n * {\n * provider: \"elevenlabs\",\n * voice_id: \"21m00Tcm4TlvDq8ikWAM\",\n * speaking_rate: 1.0,\n * audio_format: \"pcm\",\n * sample_rate: 16000,\n * connection_timeout: 5000,\n * request_timeout: 10000,\n * model: \"eleven_turbo_v2\",\n * pronunciations: []\n * }\n * );\n *\n * // Register event handlers\n * client.registerOnSttResult((result) => {\n * console.log(\"Transcription:\", result.transcript);\n * });\n *\n * // Send text to be spoken\n * await client.speak(\"Hello, world!\");\n *\n * // Clean up\n * await client.disconnect();\n * ```\n */\nexport async function saynaConnect(\n url: string,\n sttConfig: STTConfig,\n ttsConfig: TTSConfig,\n livekitConfig?: LiveKitConfig,\n withoutAudio: boolean = false\n): Promise<SaynaClient> {\n const client = new SaynaClient(\n url,\n sttConfig,\n ttsConfig,\n livekitConfig,\n withoutAudio\n );\n await client.connect();\n return client;\n}\n"
6
+ "import type {\n STTConfig,\n TTSConfig,\n LiveKitConfig,\n ConfigMessage,\n SpeakMessage,\n ClearMessage,\n SendMessageMessage,\n STTResultMessage,\n ErrorMessage,\n OutgoingMessage,\n SaynaMessage,\n Participant,\n VoicesResponse,\n HealthResponse,\n LiveKitTokenResponse,\n} from \"./types\";\nimport {\n SaynaNotConnectedError,\n SaynaNotReadyError,\n SaynaConnectionError,\n SaynaValidationError,\n SaynaServerError,\n} from \"./errors\";\n\n// Node.js 18+ has native fetch support\ndeclare const fetch: typeof globalThis.fetch;\n\n/**\n * Event handler for speech-to-text results.\n */\nexport type STTResultHandler = (\n result: STTResultMessage\n) => void | Promise<void>;\n\n/**\n * Event handler for text-to-speech audio data.\n */\nexport type TTSAudioHandler = (audio: ArrayBuffer) => void | Promise<void>;\n\n/**\n * Event handler for error messages.\n */\nexport type ErrorHandler = (error: ErrorMessage) => void | Promise<void>;\n\n/**\n * Event handler for participant messages.\n */\nexport type MessageHandler = (message: SaynaMessage) => void | Promise<void>;\n\n/**\n * Event handler for participant disconnections.\n */\nexport type ParticipantDisconnectedHandler = (\n participant: Participant\n) => void | Promise<void>;\n\n/**\n * Event handler for TTS playback completion.\n */\nexport type TTSPlaybackCompleteHandler = (\n timestamp: number\n) => void | Promise<void>;\n\n/**\n * Client for connecting to Sayna WebSocket server for real-time voice interactions.\n *\n * @example\n * ```typescript\n * const client = await saynaConnect(\n * \"https://api.sayna.ai\",\n * sttConfig,\n * ttsConfig\n * );\n *\n * client.registerOnSttResult((result) => {\n * console.log(\"Transcription:\", result.transcript);\n * });\n *\n * await client.speak(\"Hello, world!\");\n * ```\n */\nexport class SaynaClient {\n private url: string;\n private sttConfig?: STTConfig;\n private ttsConfig?: TTSConfig;\n private livekitConfig?: LiveKitConfig;\n private withoutAudio: boolean;\n private apiKey?: string;\n private websocket?: WebSocket;\n private isConnected: boolean = false;\n private isReady: boolean = false;\n private _livekitRoomName?: string;\n private _livekitUrl?: string;\n private _saynaParticipantIdentity?: string;\n private _saynaParticipantName?: string;\n private sttCallback?: STTResultHandler;\n private ttsCallback?: TTSAudioHandler;\n private errorCallback?: ErrorHandler;\n private messageCallback?: MessageHandler;\n private participantDisconnectedCallback?: ParticipantDisconnectedHandler;\n private ttsPlaybackCompleteCallback?: TTSPlaybackCompleteHandler;\n private readyPromiseResolve?: () => void;\n private readyPromiseReject?: (error: Error) => void;\n\n /**\n * Creates a new SaynaClient instance.\n *\n * @param url - The Sayna server URL (e.g., \"https://api.sayna.ai\")\n * @param sttConfig - Speech-to-text configuration (required when withoutAudio=false)\n * @param ttsConfig - Text-to-speech configuration (required when withoutAudio=false)\n * @param livekitConfig - Optional LiveKit room configuration\n * @param withoutAudio - If true, disables audio streaming (default: false)\n * @param apiKey - Optional API key used to authorize HTTP and WebSocket calls (defaults to SAYNA_API_KEY env)\n *\n * @throws {SaynaValidationError} If URL is invalid or if audio configs are missing when audio is enabled\n */\n constructor(\n url: string,\n sttConfig?: STTConfig,\n ttsConfig?: TTSConfig,\n livekitConfig?: LiveKitConfig,\n withoutAudio: boolean = false,\n apiKey?: string\n ) {\n // Validate URL\n if (!url || typeof url !== \"string\") {\n throw new SaynaValidationError(\"URL must be a non-empty string\");\n }\n if (\n !url.startsWith(\"http://\") &&\n !url.startsWith(\"https://\") &&\n !url.startsWith(\"ws://\") &&\n !url.startsWith(\"wss://\")\n ) {\n throw new SaynaValidationError(\n \"URL must start with http://, https://, ws://, or wss://\"\n );\n }\n\n // Validate audio config requirements\n if (!withoutAudio) {\n if (!sttConfig || !ttsConfig) {\n throw new SaynaValidationError(\n \"sttConfig and ttsConfig are required when withoutAudio=false (audio streaming enabled). \" +\n \"Either provide both configs or set withoutAudio=true for non-audio use cases.\"\n );\n }\n }\n\n this.url = url;\n this.sttConfig = sttConfig;\n this.ttsConfig = ttsConfig;\n this.livekitConfig = livekitConfig;\n this.withoutAudio = withoutAudio;\n this.apiKey = apiKey ?? process.env.SAYNA_API_KEY;\n }\n\n /**\n * Establishes connection to the Sayna WebSocket server.\n *\n * @throws {SaynaConnectionError} If connection fails\n * @returns Promise that resolves when the connection is ready\n */\n async connect(): Promise<void> {\n if (this.isConnected) {\n return;\n }\n\n // Convert HTTP(S) URL to WebSocket URL\n const wsUrl =\n this.url.replace(/^https:\\/\\//, \"wss://\").replace(/^http:\\/\\//, \"ws://\") +\n (this.url.endsWith(\"/\") ? \"ws\" : \"/ws\");\n\n return new Promise((resolve, reject) => {\n this.readyPromiseResolve = resolve;\n this.readyPromiseReject = reject;\n\n try {\n const headers = this.apiKey\n ? { Authorization: `Bearer ${this.apiKey}` }\n : undefined;\n\n const WebSocketConstructor = WebSocket as unknown as {\n new (\n url: string,\n protocols?: string | string[],\n options?: { headers?: Record<string, string> }\n ): WebSocket;\n };\n\n this.websocket = headers\n ? new WebSocketConstructor(wsUrl, undefined, { headers })\n : new WebSocket(wsUrl);\n\n this.websocket.onopen = () => {\n this.isConnected = true;\n\n // Send initial configuration\n const configMessage: ConfigMessage = {\n type: \"config\",\n stt_config: this.sttConfig,\n tts_config: this.ttsConfig,\n livekit: this.livekitConfig,\n audio: !this.withoutAudio,\n };\n\n try {\n if (this.websocket) {\n this.websocket.send(JSON.stringify(configMessage));\n }\n } catch (error) {\n this.cleanup();\n const err = new SaynaConnectionError(\n \"Failed to send configuration\",\n error\n );\n if (this.readyPromiseReject) {\n this.readyPromiseReject(err);\n }\n }\n };\n\n this.websocket.onmessage = async (event) => {\n try {\n if (\n event.data instanceof Blob ||\n event.data instanceof ArrayBuffer\n ) {\n // Binary TTS audio data\n const buffer =\n event.data instanceof Blob\n ? await event.data.arrayBuffer()\n : event.data;\n if (this.ttsCallback) {\n await this.ttsCallback(buffer);\n }\n } else {\n // JSON control messages\n if (typeof event.data !== \"string\") {\n throw new Error(\"Expected string data for JSON messages\");\n }\n const data = JSON.parse(event.data) as OutgoingMessage;\n await this.handleJsonMessage(data);\n }\n } catch (error) {\n // Log parse errors but don't break the connection\n if (this.errorCallback) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n await this.errorCallback({\n type: \"error\",\n message: `Failed to process message: ${errorMessage}`,\n });\n }\n }\n };\n\n this.websocket.onerror = () => {\n const error = new SaynaConnectionError(\"WebSocket connection error\");\n if (this.readyPromiseReject && !this.isReady) {\n this.readyPromiseReject(error);\n }\n };\n\n this.websocket.onclose = (event) => {\n const wasReady = this.isReady;\n this.cleanup();\n\n // If connection closed before ready, reject the promise\n if (!wasReady && this.readyPromiseReject) {\n this.readyPromiseReject(\n new SaynaConnectionError(\n `WebSocket closed before ready (code: ${event.code}, reason: ${event.reason || \"none\"})`\n )\n );\n }\n };\n } catch (error) {\n reject(new SaynaConnectionError(\"Failed to create WebSocket\", error));\n }\n });\n }\n\n /**\n * Handles incoming JSON messages from the WebSocket.\n * @internal\n */\n private async handleJsonMessage(data: OutgoingMessage): Promise<void> {\n const messageType = data.type;\n\n try {\n switch (messageType) {\n case \"ready\": {\n const readyMsg = data;\n this.isReady = true;\n this._livekitRoomName = readyMsg.livekit_room_name;\n this._livekitUrl = readyMsg.livekit_url;\n this._saynaParticipantIdentity = readyMsg.sayna_participant_identity;\n this._saynaParticipantName = readyMsg.sayna_participant_name;\n if (this.readyPromiseResolve) {\n this.readyPromiseResolve();\n }\n break;\n }\n\n case \"stt_result\": {\n const sttResult = data;\n if (this.sttCallback) {\n await this.sttCallback(sttResult);\n }\n break;\n }\n\n case \"error\": {\n const errorMsg = data;\n if (this.errorCallback) {\n await this.errorCallback(errorMsg);\n }\n if (this.readyPromiseReject && !this.isReady) {\n this.readyPromiseReject(new SaynaServerError(errorMsg.message));\n }\n break;\n }\n\n case \"message\": {\n const messageData = data;\n if (this.messageCallback) {\n await this.messageCallback(messageData.message);\n }\n break;\n }\n\n case \"participant_disconnected\": {\n const participantMsg = data;\n if (this.participantDisconnectedCallback) {\n await this.participantDisconnectedCallback(\n participantMsg.participant\n );\n }\n break;\n }\n\n case \"tts_playback_complete\": {\n const ttsPlaybackCompleteMsg = data;\n if (this.ttsPlaybackCompleteCallback) {\n await this.ttsPlaybackCompleteCallback(\n ttsPlaybackCompleteMsg.timestamp\n );\n }\n break;\n }\n }\n } catch (error) {\n // Notify error callback if handler fails\n if (this.errorCallback) {\n await this.errorCallback({\n type: \"error\",\n message: `Handler error: ${error instanceof Error ? error.message : String(error)}`,\n });\n }\n }\n }\n\n /**\n * Cleans up internal state.\n * @internal\n */\n private cleanup(): void {\n this.isConnected = false;\n this.isReady = false;\n this._livekitRoomName = undefined;\n this._livekitUrl = undefined;\n this._saynaParticipantIdentity = undefined;\n this._saynaParticipantName = undefined;\n }\n\n /**\n * Converts WebSocket URL to HTTP URL for REST API calls.\n * @internal\n */\n private getHttpUrl(): string {\n return this.url\n .replace(/^ws:\\/\\//, \"http://\")\n .replace(/^wss:\\/\\//, \"https://\");\n }\n\n /**\n * Generic fetch helper for making REST API calls to Sayna server.\n * Handles URL construction, headers, error responses, and type conversion.\n * @internal\n *\n * @param endpoint - API endpoint path (e.g., \"/voices\", \"/speak\")\n * @param options - Fetch options including method, body, headers, etc.\n * @param responseType - Expected response type: \"json\" or \"arrayBuffer\"\n * @returns Promise resolving to the parsed response\n * @throws {SaynaConnectionError} If the network request fails\n * @throws {SaynaServerError} If the server returns an error response\n */\n private async fetchFromSayna<T>(\n endpoint: string,\n options: RequestInit = {},\n responseType: \"json\" | \"arrayBuffer\" = \"json\"\n ): Promise<T> {\n const httpUrl = this.getHttpUrl();\n const url = `${httpUrl}${httpUrl.endsWith(\"/\") ? \"\" : \"/\"}${endpoint.startsWith(\"/\") ? endpoint.slice(1) : endpoint}`;\n\n // Merge default headers with user-provided headers\n const headers: Record<string, string> = {\n ...(options.headers as Record<string, string>),\n };\n\n // Add Authorization header when an API key is provided, unless user supplied one\n const hasAuthHeader = Object.keys(headers).some(\n (key) => key.toLowerCase() === \"authorization\"\n );\n if (this.apiKey && !hasAuthHeader) {\n headers[\"Authorization\"] = `Bearer ${this.apiKey}`;\n }\n\n // Add Content-Type for JSON requests if not already set\n if (options.method === \"POST\" && options.body && !headers[\"Content-Type\"]) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n try {\n const response = await fetch(url, {\n ...options,\n headers,\n });\n\n if (!response.ok) {\n // Try to parse error message from JSON response\n let errorMessage: string;\n try {\n const errorData: unknown = await response.json();\n errorMessage =\n errorData &&\n typeof errorData === \"object\" &&\n \"error\" in errorData &&\n typeof errorData.error === \"string\"\n ? errorData.error\n : `Request failed: ${response.status} ${response.statusText}`;\n } catch {\n errorMessage = `Request failed: ${response.status} ${response.statusText}`;\n }\n throw new SaynaServerError(errorMessage);\n }\n\n // Parse response based on expected type\n if (responseType === \"arrayBuffer\") {\n return (await response.arrayBuffer()) as T;\n } else {\n return (await response.json()) as T;\n }\n } catch (error) {\n // Re-throw SaynaServerError as-is\n if (error instanceof SaynaServerError) {\n throw error;\n }\n // Wrap other errors in SaynaConnectionError\n throw new SaynaConnectionError(`Failed to fetch from ${endpoint}`, error);\n }\n }\n\n /**\n * Disconnects from the Sayna WebSocket server and cleans up resources.\n */\n disconnect(): void {\n if (this.websocket) {\n // Remove event handlers to prevent memory leaks\n this.websocket.onopen = null;\n this.websocket.onmessage = null;\n this.websocket.onerror = null;\n this.websocket.onclose = null;\n\n if (this.websocket.readyState === WebSocket.OPEN) {\n this.websocket.close(1000, \"Client disconnect\");\n }\n\n this.websocket = undefined;\n }\n\n this.cleanup();\n }\n\n /**\n * Sends audio data to the server for speech recognition.\n *\n * @param audioData - Raw audio data as ArrayBuffer\n * @throws {SaynaNotConnectedError} If not connected\n * @throws {SaynaNotReadyError} If connection is not ready\n */\n onAudioInput(audioData: ArrayBuffer): void {\n if (!this.isConnected || !this.websocket) {\n throw new SaynaNotConnectedError();\n }\n\n if (!this.isReady) {\n throw new SaynaNotReadyError();\n }\n\n if (!(audioData instanceof ArrayBuffer)) {\n throw new SaynaValidationError(\"audioData must be an ArrayBuffer\");\n }\n\n if (audioData.byteLength === 0) {\n throw new SaynaValidationError(\"audioData cannot be empty\");\n }\n\n try {\n this.websocket.send(audioData);\n } catch (error) {\n throw new SaynaConnectionError(\"Failed to send audio data\", error);\n }\n }\n\n /**\n * Registers a callback for speech-to-text results.\n *\n * @param callback - Function to call when STT results are received\n */\n registerOnSttResult(callback: STTResultHandler): void {\n this.sttCallback = callback;\n }\n\n /**\n * Registers a callback for text-to-speech audio data.\n *\n * @param callback - Function to call when TTS audio is received\n */\n registerOnTtsAudio(callback: TTSAudioHandler): void {\n this.ttsCallback = callback;\n }\n\n /**\n * Registers a callback for error messages.\n *\n * @param callback - Function to call when errors occur\n */\n registerOnError(callback: ErrorHandler): void {\n this.errorCallback = callback;\n }\n\n /**\n * Registers a callback for participant messages.\n *\n * @param callback - Function to call when messages are received\n */\n registerOnMessage(callback: MessageHandler): void {\n this.messageCallback = callback;\n }\n\n /**\n * Registers a callback for participant disconnection events.\n *\n * @param callback - Function to call when a participant disconnects\n */\n registerOnParticipantDisconnected(\n callback: ParticipantDisconnectedHandler\n ): void {\n this.participantDisconnectedCallback = callback;\n }\n\n /**\n * Registers a callback for TTS playback completion.\n *\n * @param callback - Function to call when TTS playback is complete\n */\n registerOnTtsPlaybackComplete(callback: TTSPlaybackCompleteHandler): void {\n this.ttsPlaybackCompleteCallback = callback;\n }\n\n /**\n * Sends text to be synthesized as speech.\n *\n * @param text - Text to synthesize\n * @param flush - Whether to flush the TTS queue before speaking (default: true)\n * @param allowInterruption - Whether this speech can be interrupted (default: true)\n * @throws {SaynaNotConnectedError} If not connected\n * @throws {SaynaNotReadyError} If connection is not ready\n * @throws {SaynaValidationError} If text is not a string\n */\n speak(\n text: string,\n flush: boolean = true,\n allowInterruption: boolean = true\n ): void {\n if (!this.isConnected || !this.websocket) {\n throw new SaynaNotConnectedError();\n }\n\n if (!this.isReady) {\n throw new SaynaNotReadyError();\n }\n\n if (typeof text !== \"string\") {\n throw new SaynaValidationError(\"text must be a string\");\n }\n\n try {\n const speakMessage: SpeakMessage = {\n type: \"speak\",\n text,\n flush,\n allow_interruption: allowInterruption,\n };\n this.websocket.send(JSON.stringify(speakMessage));\n } catch (error) {\n throw new SaynaConnectionError(\"Failed to send speak command\", error);\n }\n }\n\n /**\n * Clears the text-to-speech queue.\n *\n * @throws {SaynaNotConnectedError} If not connected\n * @throws {SaynaNotReadyError} If connection is not ready\n */\n clear(): void {\n if (!this.isConnected || !this.websocket) {\n throw new SaynaNotConnectedError();\n }\n\n if (!this.isReady) {\n throw new SaynaNotReadyError();\n }\n\n try {\n const clearMessage: ClearMessage = {\n type: \"clear\",\n };\n this.websocket.send(JSON.stringify(clearMessage));\n } catch (error) {\n throw new SaynaConnectionError(\"Failed to send clear command\", error);\n }\n }\n\n /**\n * Flushes the TTS queue by sending an empty speak command.\n *\n * @param allowInterruption - Whether the flush can be interrupted (default: true)\n * @throws {SaynaNotConnectedError} If not connected\n * @throws {SaynaNotReadyError} If connection is not ready\n */\n ttsFlush(allowInterruption: boolean = true): void {\n this.speak(\"\", true, allowInterruption);\n }\n\n /**\n * Sends a message to the Sayna session.\n *\n * @param message - Message content\n * @param role - Message role (e.g., \"user\", \"assistant\")\n * @param topic - Optional topic identifier\n * @param debug - Optional debug metadata\n * @throws {SaynaNotConnectedError} If not connected\n * @throws {SaynaNotReadyError} If connection is not ready\n * @throws {SaynaValidationError} If parameters are invalid\n */\n sendMessage(\n message: string,\n role: string,\n topic?: string,\n debug?: Record<string, unknown>\n ): void {\n if (!this.isConnected || !this.websocket) {\n throw new SaynaNotConnectedError();\n }\n\n if (!this.isReady) {\n throw new SaynaNotReadyError();\n }\n\n if (typeof message !== \"string\") {\n throw new SaynaValidationError(\"message must be a string\");\n }\n\n if (typeof role !== \"string\") {\n throw new SaynaValidationError(\"role must be a string\");\n }\n\n try {\n const sendMsg: SendMessageMessage = {\n type: \"send_message\",\n message,\n role,\n topic,\n debug,\n };\n this.websocket.send(JSON.stringify(sendMsg));\n } catch (error) {\n throw new SaynaConnectionError(\"Failed to send message\", error);\n }\n }\n\n /**\n * Performs a health check on the Sayna server.\n *\n * @returns Promise that resolves with the health status\n * @throws {SaynaConnectionError} If the request fails\n * @throws {SaynaServerError} If server returns an error\n *\n * @example\n * ```typescript\n * const health = await client.health();\n * console.log(health.status); // \"OK\"\n * ```\n */\n async health(): Promise<HealthResponse> {\n return this.fetchFromSayna<HealthResponse>(\"\");\n }\n\n /**\n * Retrieves the catalogue of text-to-speech voices grouped by provider.\n *\n * @returns Promise that resolves with voices organized by provider\n * @throws {SaynaConnectionError} If the request fails\n * @throws {SaynaServerError} If server returns an error\n *\n * @example\n * ```typescript\n * const voices = await client.getVoices();\n * for (const [provider, voiceList] of Object.entries(voices)) {\n * console.log(`${provider}:`, voiceList.map(v => v.name));\n * }\n * ```\n */\n async getVoices(): Promise<VoicesResponse> {\n return this.fetchFromSayna<VoicesResponse>(\"voices\");\n }\n\n /**\n * Synthesizes text into audio using the REST API endpoint.\n * This is a standalone synthesis method that doesn't require an active WebSocket connection.\n *\n * @param text - Text to synthesize\n * @param ttsConfig - Text-to-speech configuration\n * @returns Promise that resolves with the audio data as ArrayBuffer\n * @throws {SaynaValidationError} If text is empty\n * @throws {SaynaConnectionError} If the request fails\n * @throws {SaynaServerError} If server returns an error\n *\n * @example\n * ```typescript\n * const audioBuffer = await client.speakRest(\"Hello, world!\", {\n * provider: \"elevenlabs\",\n * voice_id: \"21m00Tcm4TlvDq8ikWAM\",\n * model: \"eleven_turbo_v2\",\n * speaking_rate: 1.0,\n * audio_format: \"mp3\",\n * sample_rate: 24000,\n * connection_timeout: 30,\n * request_timeout: 60,\n * pronunciations: []\n * });\n * ```\n */\n async speakRest(text: string, ttsConfig: TTSConfig): Promise<ArrayBuffer> {\n if (!text || text.trim().length === 0) {\n throw new SaynaValidationError(\"Text cannot be empty\");\n }\n\n return this.fetchFromSayna<ArrayBuffer>(\n \"speak\",\n {\n method: \"POST\",\n body: JSON.stringify({\n text,\n tts_config: ttsConfig,\n }),\n },\n \"arrayBuffer\"\n );\n }\n\n /**\n * Issues a LiveKit access token for a participant.\n *\n * @param roomName - LiveKit room to join or create\n * @param participantName - Display name assigned to the participant\n * @param participantIdentity - Unique identifier for the participant\n * @returns Promise that resolves with the LiveKit token and connection details\n * @throws {SaynaValidationError} If any parameter is empty\n * @throws {SaynaConnectionError} If the request fails\n * @throws {SaynaServerError} If server returns an error\n *\n * @example\n * ```typescript\n * const tokenInfo = await client.getLiveKitToken(\n * \"my-room\",\n * \"John Doe\",\n * \"user-123\"\n * );\n * console.log(\"Token:\", tokenInfo.token);\n * console.log(\"LiveKit URL:\", tokenInfo.livekit_url);\n * ```\n */\n async getLiveKitToken(\n roomName: string,\n participantName: string,\n participantIdentity: string\n ): Promise<LiveKitTokenResponse> {\n if (!roomName || roomName.trim().length === 0) {\n throw new SaynaValidationError(\"room_name cannot be empty\");\n }\n\n if (!participantName || participantName.trim().length === 0) {\n throw new SaynaValidationError(\"participant_name cannot be empty\");\n }\n\n if (!participantIdentity || participantIdentity.trim().length === 0) {\n throw new SaynaValidationError(\"participant_identity cannot be empty\");\n }\n\n return this.fetchFromSayna<LiveKitTokenResponse>(\"livekit/token\", {\n method: \"POST\",\n body: JSON.stringify({\n room_name: roomName,\n participant_name: participantName,\n participant_identity: participantIdentity,\n }),\n });\n }\n\n /**\n * Whether the client is ready to send/receive data.\n */\n get ready(): boolean {\n return this.isReady;\n }\n\n /**\n * Whether the client is connected to the WebSocket.\n */\n get connected(): boolean {\n return this.isConnected;\n }\n\n /**\n * LiveKit room name acknowledged by the server, if available.\n */\n get livekitRoomName(): string | undefined {\n return this._livekitRoomName;\n }\n\n /**\n * LiveKit WebSocket URL configured on the server, if available.\n */\n get livekitUrl(): string | undefined {\n return this._livekitUrl;\n }\n\n /**\n * Identity assigned to the agent participant when LiveKit is enabled, if available.\n */\n get saynaParticipantIdentity(): string | undefined {\n return this._saynaParticipantIdentity;\n }\n\n /**\n * Display name assigned to the agent participant when LiveKit is enabled, if available.\n */\n get saynaParticipantName(): string | undefined {\n return this._saynaParticipantName;\n }\n}\n",
7
+ "import { createHmac, timingSafeEqual } from \"crypto\";\nimport { SaynaValidationError } from \"./errors\";\nimport type { WebhookSIPOutput } from \"./types\";\n\n/**\n * Minimum required secret length in characters for security.\n * @internal\n */\nconst MIN_SECRET_LENGTH = 16;\n\n/**\n * Maximum allowed time difference in seconds for replay protection.\n * Webhooks with timestamps outside this window will be rejected.\n * @internal\n */\nconst TIMESTAMP_TOLERANCE_SECONDS = 300; // 5 minutes\n\n/**\n * Receives and verifies cryptographically signed webhooks from Sayna SIP service.\n *\n * This class handles the secure verification of webhook signatures using HMAC-SHA256,\n * validates timestamp freshness to prevent replay attacks, and parses the webhook\n * payload into a strongly-typed WebhookSIPOutput object.\n *\n * ## Security Features\n *\n * - **HMAC-SHA256 Signature Verification**: Ensures webhook authenticity\n * - **Constant-Time Comparison**: Prevents timing attack vulnerabilities\n * - **Replay Protection**: 5-minute timestamp window prevents replay attacks\n * - **Strict Validation**: Comprehensive checks on all required fields\n *\n * ## Usage\n *\n * ### Basic Example\n *\n * ```typescript\n * import { WebhookReceiver } from \"@sayna-ai/node-sdk\";\n *\n * // Initialize with secret (or uses SAYNA_WEBHOOK_SECRET env variable)\n * const receiver = new WebhookReceiver(\"your-secret-key-min-16-chars\");\n *\n * // In your Express route handler\n * app.post('/webhook', express.json({ verify: (req, res, buf) => {\n * req.rawBody = buf.toString('utf8');\n * }}), (req, res) => {\n * try {\n * const webhook = receiver.receive(req.headers, req.rawBody);\n *\n * console.log('Valid webhook received:');\n * console.log(' From:', webhook.from_phone_number);\n * console.log(' To:', webhook.to_phone_number);\n * console.log(' Room:', webhook.room.name);\n * console.log(' SIP Host:', webhook.sip_host);\n * console.log(' Participant:', webhook.participant.identity);\n *\n * res.status(200).json({ received: true });\n * } catch (error) {\n * console.error('Webhook verification failed:', error.message);\n * res.status(401).json({ error: 'Invalid signature' });\n * }\n * });\n * ```\n *\n * ### With Environment Variable\n *\n * ```typescript\n * // Set environment variable\n * process.env.SAYNA_WEBHOOK_SECRET = \"your-secret-key\";\n *\n * // Receiver automatically uses env variable\n * const receiver = new WebhookReceiver();\n * ```\n *\n * ### Fastify Example\n *\n * ```typescript\n * import Fastify from 'fastify';\n * import { WebhookReceiver } from \"@sayna-ai/node-sdk\";\n *\n * const fastify = Fastify();\n * const receiver = new WebhookReceiver();\n *\n * fastify.post('/webhook', {\n * config: {\n * rawBody: true\n * }\n * }, async (request, reply) => {\n * try {\n * const webhook = receiver.receive(\n * request.headers,\n * request.rawBody\n * );\n *\n * // Process webhook...\n *\n * return { received: true };\n * } catch (error) {\n * reply.code(401);\n * return { error: error.message };\n * }\n * });\n * ```\n *\n * ## Important Notes\n *\n * - **Raw Body Required**: You MUST pass the raw request body string, not the parsed JSON object.\n * The signature is computed over the exact bytes received, so any formatting changes will\n * cause verification to fail.\n *\n * - **Case-Insensitive Headers**: Header names are case-insensitive in HTTP. This class handles\n * both `X-Sayna-Signature` and `x-sayna-signature` correctly.\n *\n * - **Secret Security**: Never commit secrets to version control. Use environment variables\n * or a secret management system.\n *\n * @see WebhookSIPOutput\n */\nexport class WebhookReceiver {\n private readonly secret: string;\n\n /**\n * Creates a new webhook receiver with the specified signing secret.\n *\n * @param secret - HMAC signing secret (min 16 chars, 32+ recommended).\n * If not provided, uses SAYNA_WEBHOOK_SECRET environment variable.\n *\n * @throws {SaynaValidationError} If secret is missing or too short\n *\n * @example\n * ```typescript\n * // Explicit secret\n * const receiver = new WebhookReceiver(\"my-secret-key-at-least-16-chars\");\n *\n * // From environment variable\n * const receiver = new WebhookReceiver();\n * ```\n */\n constructor(secret?: string) {\n const effectiveSecret = secret ?? process.env.SAYNA_WEBHOOK_SECRET;\n\n if (!effectiveSecret) {\n throw new SaynaValidationError(\n \"Webhook secret is required. Provide it as a constructor parameter or set SAYNA_WEBHOOK_SECRET environment variable.\"\n );\n }\n\n const trimmedSecret = effectiveSecret.trim();\n\n if (trimmedSecret.length < MIN_SECRET_LENGTH) {\n throw new SaynaValidationError(\n `Webhook secret must be at least ${MIN_SECRET_LENGTH} characters long. ` +\n `Received ${trimmedSecret.length} characters. ` +\n `Generate a secure secret with: openssl rand -hex 32`\n );\n }\n\n this.secret = trimmedSecret;\n }\n\n /**\n * Verifies and parses an incoming SIP webhook from Sayna.\n *\n * This method performs the following security checks:\n * 1. Validates presence of required headers\n * 2. Verifies timestamp is within acceptable window (prevents replay attacks)\n * 3. Computes HMAC-SHA256 signature over canonical string\n * 4. Performs constant-time comparison to prevent timing attacks\n * 5. Parses and validates the webhook payload structure\n *\n * @param headers - HTTP request headers (case-insensitive)\n * @param body - Raw request body as string (not parsed JSON)\n *\n * @returns Parsed and validated webhook payload\n *\n * @throws {SaynaValidationError} If signature verification fails or payload is invalid\n *\n * @example\n * ```typescript\n * const receiver = new WebhookReceiver(\"your-secret\");\n *\n * // Express example\n * app.post('/webhook', express.json({ verify: (req, res, buf) => {\n * req.rawBody = buf.toString();\n * }}), (req, res) => {\n * const webhook = receiver.receive(req.headers, req.rawBody);\n * // webhook is now a validated WebhookSIPOutput object\n * });\n * ```\n */\n receive(\n headers: Record<string, string | string[] | undefined>,\n body: string\n ): WebhookSIPOutput {\n // Normalize headers to lowercase for case-insensitive lookup\n const normalizedHeaders = this.normalizeHeaders(headers);\n\n // Extract required headers\n const signature = this.getRequiredHeader(\n normalizedHeaders,\n \"x-sayna-signature\"\n );\n const timestamp = this.getRequiredHeader(\n normalizedHeaders,\n \"x-sayna-timestamp\"\n );\n const eventId = this.getRequiredHeader(\n normalizedHeaders,\n \"x-sayna-event-id\"\n );\n\n // Parse and validate signature format\n if (!signature.startsWith(\"v1=\")) {\n throw new SaynaValidationError(\n \"Invalid signature format. Expected 'v1=<hex>' but got: \" +\n signature.substring(0, 10) +\n \"...\"\n );\n }\n const signatureHex = signature.substring(3);\n\n // Validate signature is valid hex (64 chars for SHA256)\n if (!/^[0-9a-f]{64}$/i.test(signatureHex)) {\n throw new SaynaValidationError(\n \"Invalid signature: must be 64 hex characters (HMAC-SHA256)\"\n );\n }\n\n // Validate and check timestamp\n this.validateTimestamp(timestamp);\n\n // Build canonical string for signature verification\n const canonical = `v1:${timestamp}:${eventId}:${body}`;\n\n // Compute expected signature\n const hmac = createHmac(\"sha256\", this.secret);\n hmac.update(canonical, \"utf8\");\n const expectedSignature = hmac.digest(\"hex\");\n\n // Constant-time comparison to prevent timing attacks\n if (!this.constantTimeEqual(signatureHex, expectedSignature)) {\n throw new SaynaValidationError(\n \"Signature verification failed. The webhook may have been tampered with or the secret is incorrect.\"\n );\n }\n\n // Parse and validate the webhook payload\n return this.parseAndValidatePayload(body);\n }\n\n /**\n * Normalizes HTTP headers to lowercase for case-insensitive access.\n * Handles both single string values and arrays of strings.\n *\n * @internal\n */\n private normalizeHeaders(\n headers: Record<string, string | string[] | undefined>\n ): Record<string, string> {\n const normalized: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(headers)) {\n if (value !== undefined) {\n // Handle array values (take first element)\n const stringValue = Array.isArray(value) ? value[0] : value;\n if (stringValue) {\n normalized[key.toLowerCase()] = stringValue;\n }\n }\n }\n\n return normalized;\n }\n\n /**\n * Retrieves a required header value or throws a validation error.\n *\n * @internal\n */\n private getRequiredHeader(\n headers: Record<string, string>,\n name: string\n ): string {\n const value = headers[name.toLowerCase()];\n\n if (!value) {\n throw new SaynaValidationError(`Missing required header: ${name}`);\n }\n\n return value;\n }\n\n /**\n * Validates the timestamp is within the acceptable window.\n *\n * @internal\n */\n private validateTimestamp(timestampStr: string): void {\n // Parse timestamp\n const timestamp = Number(timestampStr);\n\n if (isNaN(timestamp)) {\n throw new SaynaValidationError(\n `Invalid timestamp format: expected Unix seconds but got '${timestampStr}'`\n );\n }\n\n // Check if timestamp is within acceptable range\n const now = Math.floor(Date.now() / 1000);\n const diff = Math.abs(now - timestamp);\n\n if (diff > TIMESTAMP_TOLERANCE_SECONDS) {\n throw new SaynaValidationError(\n `Timestamp outside replay protection window. ` +\n `Difference: ${diff} seconds (max allowed: ${TIMESTAMP_TOLERANCE_SECONDS}). ` +\n `This webhook may be a replay attack or there may be significant clock skew.`\n );\n }\n }\n\n /**\n * Performs constant-time string comparison to prevent timing attacks.\n *\n * @internal\n */\n private constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n const bufA = Buffer.from(a, \"utf8\");\n const bufB = Buffer.from(b, \"utf8\");\n\n return timingSafeEqual(bufA, bufB);\n }\n\n /**\n * Parses and validates the webhook payload structure.\n *\n * @internal\n */\n private parseAndValidatePayload(body: string): WebhookSIPOutput {\n let payload: unknown;\n\n // Parse JSON\n try {\n payload = JSON.parse(body);\n } catch (error) {\n throw new SaynaValidationError(\n `Invalid JSON payload: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n\n // Validate payload is an object\n if (!payload || typeof payload !== \"object\" || Array.isArray(payload)) {\n throw new SaynaValidationError(\"Webhook payload must be a JSON object\");\n }\n\n const data = payload as Record<string, unknown>;\n\n // Validate required fields\n this.validateParticipant(data.participant);\n this.validateRoom(data.room);\n this.validateStringField(data, \"from_phone_number\", \"from_phone_number\");\n this.validateStringField(data, \"to_phone_number\", \"to_phone_number\");\n this.validateStringField(data, \"room_prefix\", \"room_prefix\");\n this.validateStringField(data, \"sip_host\", \"sip_host\");\n\n // TypeScript type assertion is safe here because we've validated all fields\n // We use double assertion through unknown to satisfy strict type checking\n return data as unknown as WebhookSIPOutput;\n }\n\n /**\n * Validates the participant object structure.\n *\n * @internal\n */\n private validateParticipant(participant: unknown): void {\n if (\n !participant ||\n typeof participant !== \"object\" ||\n Array.isArray(participant)\n ) {\n throw new SaynaValidationError(\n \"Webhook payload missing required field 'participant' (must be an object)\"\n );\n }\n\n const p = participant as Record<string, unknown>;\n\n this.validateStringField(p, \"identity\", \"participant.identity\");\n this.validateStringField(p, \"sid\", \"participant.sid\");\n\n // name is optional, but if present must be a string\n if (p.name !== undefined && typeof p.name !== \"string\") {\n throw new SaynaValidationError(\n \"Field 'participant.name' must be a string if present\"\n );\n }\n }\n\n /**\n * Validates the room object structure.\n *\n * @internal\n */\n private validateRoom(room: unknown): void {\n if (!room || typeof room !== \"object\" || Array.isArray(room)) {\n throw new SaynaValidationError(\n \"Webhook payload missing required field 'room' (must be an object)\"\n );\n }\n\n const r = room as Record<string, unknown>;\n\n this.validateStringField(r, \"name\", \"room.name\");\n this.validateStringField(r, \"sid\", \"room.sid\");\n }\n\n /**\n * Validates a required string field.\n *\n * @internal\n */\n private validateStringField(\n obj: Record<string, unknown>,\n field: string,\n displayName: string\n ): void {\n const value = obj[field];\n\n if (typeof value !== \"string\" || value.length === 0) {\n throw new SaynaValidationError(\n `Webhook payload missing required field '${displayName}' (must be a non-empty string)`\n );\n }\n }\n}\n",
8
+ "import { SaynaClient } from \"./sayna-client\";\nimport type { STTConfig, TTSConfig, LiveKitConfig } from \"./types\";\n\nexport * from \"./sayna-client\";\nexport * from \"./types\";\nexport * from \"./errors\";\nexport * from \"./webhook-receiver\";\n\n/**\n * Creates and connects a new SaynaClient instance.\n *\n * This is the recommended way to create a Sayna client. It handles both\n * instantiation and connection, returning a ready-to-use client.\n *\n * @param url - The Sayna server URL (e.g., \"https://api.sayna.ai\")\n * @param sttConfig - Speech-to-text configuration\n * @param ttsConfig - Text-to-speech configuration\n * @param livekitConfig - Optional LiveKit room configuration\n * @param withoutAudio - If true, disables audio streaming (default: false)\n * @param apiKey - Optional API key used to authorize HTTP and WebSocket calls (defaults to SAYNA_API_KEY env)\n *\n * @returns Promise that resolves to a connected SaynaClient\n *\n * @throws {SaynaValidationError} If parameters are invalid\n * @throws {SaynaConnectionError} If connection fails\n * @throws {SaynaServerError} If server returns an error during setup\n *\n * @example\n * ```typescript\n * import { saynaConnect } from \"@sayna/node-sdk\";\n *\n * const client = await saynaConnect(\n * \"https://api.sayna.ai\",\n * {\n * provider: \"deepgram\",\n * language: \"en-US\",\n * sample_rate: 16000,\n * channels: 1,\n * punctuation: true,\n * encoding: \"linear16\",\n * model: \"nova-2\"\n * },\n * {\n * provider: \"elevenlabs\",\n * voice_id: \"21m00Tcm4TlvDq8ikWAM\",\n * speaking_rate: 1.0,\n * audio_format: \"pcm\",\n * sample_rate: 16000,\n * connection_timeout: 5000,\n * request_timeout: 10000,\n * model: \"eleven_turbo_v2\",\n * pronunciations: []\n * }\n * );\n *\n * // Register event handlers\n * client.registerOnSttResult((result) => {\n * console.log(\"Transcription:\", result.transcript);\n * });\n *\n * // Send text to be spoken\n * await client.speak(\"Hello, world!\");\n *\n * // Clean up\n * await client.disconnect();\n * ```\n */\nexport async function saynaConnect(\n url: string,\n sttConfig: STTConfig,\n ttsConfig: TTSConfig,\n livekitConfig?: LiveKitConfig,\n withoutAudio: boolean = false,\n apiKey?: string\n): Promise<SaynaClient> {\n const client = new SaynaClient(\n url,\n sttConfig,\n ttsConfig,\n livekitConfig,\n withoutAudio,\n apiKey\n );\n await client.connect();\n return client;\n}\n"
8
9
  ],
9
- "mappings": "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",
10
- "debugId": "D827CC25BF1F390D64756E2164756E21",
10
+ "mappings": "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",
11
+ "debugId": "61BF4B0E0558D09664756E2164756E21",
11
12
  "names": []
12
13
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAK5B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,UAAU;gBACxC,OAAO,GAAE,MAA2C;CAKjE;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,UAAU;gBAE9C,OAAO,GAAE,MAA0F;CAMtG;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,UAAU;IAClD,SAAyB,KAAK,CAAC,EAAE,OAAO,CAAC;gBAE7B,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,UAAU;gBACtC,OAAO,EAAE,MAAM;CAK5B;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,UAAU;gBAClC,OAAO,EAAE,MAAM;CAK5B"}
@@ -1,8 +1,9 @@
1
1
  import { SaynaClient } from "./sayna-client";
2
- import { STTConfig, TTSConfig, LiveKitConfig } from "./types";
2
+ import type { STTConfig, TTSConfig, LiveKitConfig } from "./types";
3
3
  export * from "./sayna-client";
4
4
  export * from "./types";
5
5
  export * from "./errors";
6
+ export * from "./webhook-receiver";
6
7
  /**
7
8
  * Creates and connects a new SaynaClient instance.
8
9
  *
@@ -14,6 +15,7 @@ export * from "./errors";
14
15
  * @param ttsConfig - Text-to-speech configuration
15
16
  * @param livekitConfig - Optional LiveKit room configuration
16
17
  * @param withoutAudio - If true, disables audio streaming (default: false)
18
+ * @param apiKey - Optional API key used to authorize HTTP and WebSocket calls (defaults to SAYNA_API_KEY env)
17
19
  *
18
20
  * @returns Promise that resolves to a connected SaynaClient
19
21
  *
@@ -61,5 +63,5 @@ export * from "./errors";
61
63
  * await client.disconnect();
62
64
  * ```
63
65
  */
64
- export declare function saynaConnect(url: string, sttConfig: STTConfig, ttsConfig: TTSConfig, livekitConfig?: LiveKitConfig, withoutAudio?: boolean): Promise<SaynaClient>;
66
+ export declare function saynaConnect(url: string, sttConfig: STTConfig, ttsConfig: TTSConfig, livekitConfig?: LiveKitConfig, withoutAudio?: boolean, apiKey?: string): Promise<SaynaClient>;
65
67
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEnE,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,oBAAoB,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,aAAa,CAAC,EAAE,aAAa,EAC7B,YAAY,GAAE,OAAe,EAC7B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC,CAWtB"}
@@ -43,10 +43,11 @@ export type TTSPlaybackCompleteHandler = (timestamp: number) => void | Promise<v
43
43
  */
44
44
  export declare class SaynaClient {
45
45
  private url;
46
- private sttConfig;
47
- private ttsConfig;
46
+ private sttConfig?;
47
+ private ttsConfig?;
48
48
  private livekitConfig?;
49
49
  private withoutAudio;
50
+ private apiKey?;
50
51
  private websocket?;
51
52
  private isConnected;
52
53
  private isReady;
@@ -66,14 +67,15 @@ export declare class SaynaClient {
66
67
  * Creates a new SaynaClient instance.
67
68
  *
68
69
  * @param url - The Sayna server URL (e.g., "https://api.sayna.ai")
69
- * @param sttConfig - Speech-to-text configuration
70
- * @param ttsConfig - Text-to-speech configuration
70
+ * @param sttConfig - Speech-to-text configuration (required when withoutAudio=false)
71
+ * @param ttsConfig - Text-to-speech configuration (required when withoutAudio=false)
71
72
  * @param livekitConfig - Optional LiveKit room configuration
72
73
  * @param withoutAudio - If true, disables audio streaming (default: false)
74
+ * @param apiKey - Optional API key used to authorize HTTP and WebSocket calls (defaults to SAYNA_API_KEY env)
73
75
  *
74
- * @throws {SaynaValidationError} If URL or configurations are invalid
76
+ * @throws {SaynaValidationError} If URL is invalid or if audio configs are missing when audio is enabled
75
77
  */
76
- constructor(url: string, sttConfig: STTConfig, ttsConfig: TTSConfig, livekitConfig?: LiveKitConfig, withoutAudio?: boolean);
78
+ constructor(url: string, sttConfig?: STTConfig, ttsConfig?: TTSConfig, livekitConfig?: LiveKitConfig, withoutAudio?: boolean, apiKey?: string);
77
79
  /**
78
80
  * Establishes connection to the Sayna WebSocket server.
79
81
  *
@@ -112,7 +114,7 @@ export declare class SaynaClient {
112
114
  /**
113
115
  * Disconnects from the Sayna WebSocket server and cleans up resources.
114
116
  */
115
- disconnect(): Promise<void>;
117
+ disconnect(): void;
116
118
  /**
117
119
  * Sends audio data to the server for speech recognition.
118
120
  *
@@ -120,7 +122,7 @@ export declare class SaynaClient {
120
122
  * @throws {SaynaNotConnectedError} If not connected
121
123
  * @throws {SaynaNotReadyError} If connection is not ready
122
124
  */
123
- onAudioInput(audioData: ArrayBuffer): Promise<void>;
125
+ onAudioInput(audioData: ArrayBuffer): void;
124
126
  /**
125
127
  * Registers a callback for speech-to-text results.
126
128
  *
@@ -167,14 +169,14 @@ export declare class SaynaClient {
167
169
  * @throws {SaynaNotReadyError} If connection is not ready
168
170
  * @throws {SaynaValidationError} If text is not a string
169
171
  */
170
- speak(text: string, flush?: boolean, allowInterruption?: boolean): Promise<void>;
172
+ speak(text: string, flush?: boolean, allowInterruption?: boolean): void;
171
173
  /**
172
174
  * Clears the text-to-speech queue.
173
175
  *
174
176
  * @throws {SaynaNotConnectedError} If not connected
175
177
  * @throws {SaynaNotReadyError} If connection is not ready
176
178
  */
177
- clear(): Promise<void>;
179
+ clear(): void;
178
180
  /**
179
181
  * Flushes the TTS queue by sending an empty speak command.
180
182
  *
@@ -182,7 +184,7 @@ export declare class SaynaClient {
182
184
  * @throws {SaynaNotConnectedError} If not connected
183
185
  * @throws {SaynaNotReadyError} If connection is not ready
184
186
  */
185
- ttsFlush(allowInterruption?: boolean): Promise<void>;
187
+ ttsFlush(allowInterruption?: boolean): void;
186
188
  /**
187
189
  * Sends a message to the Sayna session.
188
190
  *
@@ -194,7 +196,7 @@ export declare class SaynaClient {
194
196
  * @throws {SaynaNotReadyError} If connection is not ready
195
197
  * @throws {SaynaValidationError} If parameters are invalid
196
198
  */
197
- sendMessage(message: string, role: string, topic?: string, debug?: Record<string, unknown>): Promise<void>;
199
+ sendMessage(message: string, role: string, topic?: string, debug?: Record<string, unknown>): void;
198
200
  /**
199
201
  * Performs a health check on the Sayna server.
200
202
  *
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sayna-client.d.ts","sourceRoot":"","sources":["../../src/sayna-client.ts"],"names":[],"mappings":"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"}
@@ -74,10 +74,10 @@ export interface ConfigMessage {
74
74
  type: "config";
75
75
  /** Whether audio streaming is enabled */
76
76
  audio?: boolean;
77
- /** Speech-to-text configuration */
78
- stt_config: STTConfig;
79
- /** Text-to-speech configuration */
80
- tts_config: TTSConfig;
77
+ /** Speech-to-text configuration (required when audio=true) */
78
+ stt_config?: STTConfig;
79
+ /** Text-to-speech configuration (required when audio=true) */
80
+ tts_config?: TTSConfig;
81
81
  /** Optional LiveKit room configuration */
82
82
  livekit?: LiveKitConfig;
83
83
  }
@@ -252,4 +252,47 @@ export interface LiveKitTokenResponse {
252
252
  /** WebSocket endpoint for the LiveKit server */
253
253
  livekit_url: string;
254
254
  }
255
+ /**
256
+ * Participant information from a SIP webhook event.
257
+ */
258
+ export interface WebhookSIPParticipant {
259
+ /** Display name of the SIP participant (may be undefined if not provided) */
260
+ name?: string;
261
+ /** Unique identity assigned to the participant */
262
+ identity: string;
263
+ /** Participant session ID from LiveKit */
264
+ sid: string;
265
+ }
266
+ /**
267
+ * Room information from a SIP webhook event.
268
+ */
269
+ export interface WebhookSIPRoom {
270
+ /** Name of the LiveKit room */
271
+ name: string;
272
+ /** Room session ID from LiveKit */
273
+ sid: string;
274
+ }
275
+ /**
276
+ * SIP webhook payload sent from Sayna service.
277
+ *
278
+ * This represents a cryptographically signed webhook event forwarded by Sayna
279
+ * when a SIP participant joins a LiveKit room. Use the WebhookReceiver class
280
+ * to verify the signature and parse this payload securely.
281
+ *
282
+ * @see WebhookReceiver
283
+ */
284
+ export interface WebhookSIPOutput {
285
+ /** SIP participant information */
286
+ participant: WebhookSIPParticipant;
287
+ /** LiveKit room information */
288
+ room: WebhookSIPRoom;
289
+ /** Caller's phone number (E.164 format, e.g., "+15559876543") */
290
+ from_phone_number: string;
291
+ /** Called phone number (E.164 format, e.g., "+15551234567") */
292
+ to_phone_number: string;
293
+ /** Room name prefix configured in Sayna (e.g., "sip-") */
294
+ room_prefix: string;
295
+ /** SIP domain extracted from the To header (e.g., "example.com") */
296
+ sip_host: string;
297
+ }
255
298
  //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,203 @@
1
+ import type { WebhookSIPOutput } from "./types";
2
+ /**
3
+ * Receives and verifies cryptographically signed webhooks from Sayna SIP service.
4
+ *
5
+ * This class handles the secure verification of webhook signatures using HMAC-SHA256,
6
+ * validates timestamp freshness to prevent replay attacks, and parses the webhook
7
+ * payload into a strongly-typed WebhookSIPOutput object.
8
+ *
9
+ * ## Security Features
10
+ *
11
+ * - **HMAC-SHA256 Signature Verification**: Ensures webhook authenticity
12
+ * - **Constant-Time Comparison**: Prevents timing attack vulnerabilities
13
+ * - **Replay Protection**: 5-minute timestamp window prevents replay attacks
14
+ * - **Strict Validation**: Comprehensive checks on all required fields
15
+ *
16
+ * ## Usage
17
+ *
18
+ * ### Basic Example
19
+ *
20
+ * ```typescript
21
+ * import { WebhookReceiver } from "@sayna-ai/node-sdk";
22
+ *
23
+ * // Initialize with secret (or uses SAYNA_WEBHOOK_SECRET env variable)
24
+ * const receiver = new WebhookReceiver("your-secret-key-min-16-chars");
25
+ *
26
+ * // In your Express route handler
27
+ * app.post('/webhook', express.json({ verify: (req, res, buf) => {
28
+ * req.rawBody = buf.toString('utf8');
29
+ * }}), (req, res) => {
30
+ * try {
31
+ * const webhook = receiver.receive(req.headers, req.rawBody);
32
+ *
33
+ * console.log('Valid webhook received:');
34
+ * console.log(' From:', webhook.from_phone_number);
35
+ * console.log(' To:', webhook.to_phone_number);
36
+ * console.log(' Room:', webhook.room.name);
37
+ * console.log(' SIP Host:', webhook.sip_host);
38
+ * console.log(' Participant:', webhook.participant.identity);
39
+ *
40
+ * res.status(200).json({ received: true });
41
+ * } catch (error) {
42
+ * console.error('Webhook verification failed:', error.message);
43
+ * res.status(401).json({ error: 'Invalid signature' });
44
+ * }
45
+ * });
46
+ * ```
47
+ *
48
+ * ### With Environment Variable
49
+ *
50
+ * ```typescript
51
+ * // Set environment variable
52
+ * process.env.SAYNA_WEBHOOK_SECRET = "your-secret-key";
53
+ *
54
+ * // Receiver automatically uses env variable
55
+ * const receiver = new WebhookReceiver();
56
+ * ```
57
+ *
58
+ * ### Fastify Example
59
+ *
60
+ * ```typescript
61
+ * import Fastify from 'fastify';
62
+ * import { WebhookReceiver } from "@sayna-ai/node-sdk";
63
+ *
64
+ * const fastify = Fastify();
65
+ * const receiver = new WebhookReceiver();
66
+ *
67
+ * fastify.post('/webhook', {
68
+ * config: {
69
+ * rawBody: true
70
+ * }
71
+ * }, async (request, reply) => {
72
+ * try {
73
+ * const webhook = receiver.receive(
74
+ * request.headers,
75
+ * request.rawBody
76
+ * );
77
+ *
78
+ * // Process webhook...
79
+ *
80
+ * return { received: true };
81
+ * } catch (error) {
82
+ * reply.code(401);
83
+ * return { error: error.message };
84
+ * }
85
+ * });
86
+ * ```
87
+ *
88
+ * ## Important Notes
89
+ *
90
+ * - **Raw Body Required**: You MUST pass the raw request body string, not the parsed JSON object.
91
+ * The signature is computed over the exact bytes received, so any formatting changes will
92
+ * cause verification to fail.
93
+ *
94
+ * - **Case-Insensitive Headers**: Header names are case-insensitive in HTTP. This class handles
95
+ * both `X-Sayna-Signature` and `x-sayna-signature` correctly.
96
+ *
97
+ * - **Secret Security**: Never commit secrets to version control. Use environment variables
98
+ * or a secret management system.
99
+ *
100
+ * @see WebhookSIPOutput
101
+ */
102
+ export declare class WebhookReceiver {
103
+ private readonly secret;
104
+ /**
105
+ * Creates a new webhook receiver with the specified signing secret.
106
+ *
107
+ * @param secret - HMAC signing secret (min 16 chars, 32+ recommended).
108
+ * If not provided, uses SAYNA_WEBHOOK_SECRET environment variable.
109
+ *
110
+ * @throws {SaynaValidationError} If secret is missing or too short
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * // Explicit secret
115
+ * const receiver = new WebhookReceiver("my-secret-key-at-least-16-chars");
116
+ *
117
+ * // From environment variable
118
+ * const receiver = new WebhookReceiver();
119
+ * ```
120
+ */
121
+ constructor(secret?: string);
122
+ /**
123
+ * Verifies and parses an incoming SIP webhook from Sayna.
124
+ *
125
+ * This method performs the following security checks:
126
+ * 1. Validates presence of required headers
127
+ * 2. Verifies timestamp is within acceptable window (prevents replay attacks)
128
+ * 3. Computes HMAC-SHA256 signature over canonical string
129
+ * 4. Performs constant-time comparison to prevent timing attacks
130
+ * 5. Parses and validates the webhook payload structure
131
+ *
132
+ * @param headers - HTTP request headers (case-insensitive)
133
+ * @param body - Raw request body as string (not parsed JSON)
134
+ *
135
+ * @returns Parsed and validated webhook payload
136
+ *
137
+ * @throws {SaynaValidationError} If signature verification fails or payload is invalid
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const receiver = new WebhookReceiver("your-secret");
142
+ *
143
+ * // Express example
144
+ * app.post('/webhook', express.json({ verify: (req, res, buf) => {
145
+ * req.rawBody = buf.toString();
146
+ * }}), (req, res) => {
147
+ * const webhook = receiver.receive(req.headers, req.rawBody);
148
+ * // webhook is now a validated WebhookSIPOutput object
149
+ * });
150
+ * ```
151
+ */
152
+ receive(headers: Record<string, string | string[] | undefined>, body: string): WebhookSIPOutput;
153
+ /**
154
+ * Normalizes HTTP headers to lowercase for case-insensitive access.
155
+ * Handles both single string values and arrays of strings.
156
+ *
157
+ * @internal
158
+ */
159
+ private normalizeHeaders;
160
+ /**
161
+ * Retrieves a required header value or throws a validation error.
162
+ *
163
+ * @internal
164
+ */
165
+ private getRequiredHeader;
166
+ /**
167
+ * Validates the timestamp is within the acceptable window.
168
+ *
169
+ * @internal
170
+ */
171
+ private validateTimestamp;
172
+ /**
173
+ * Performs constant-time string comparison to prevent timing attacks.
174
+ *
175
+ * @internal
176
+ */
177
+ private constantTimeEqual;
178
+ /**
179
+ * Parses and validates the webhook payload structure.
180
+ *
181
+ * @internal
182
+ */
183
+ private parseAndValidatePayload;
184
+ /**
185
+ * Validates the participant object structure.
186
+ *
187
+ * @internal
188
+ */
189
+ private validateParticipant;
190
+ /**
191
+ * Validates the room object structure.
192
+ *
193
+ * @internal
194
+ */
195
+ private validateRoom;
196
+ /**
197
+ * Validates a required string field.
198
+ *
199
+ * @internal
200
+ */
201
+ private validateStringField;
202
+ }
203
+ //# sourceMappingURL=webhook-receiver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-receiver.d.ts","sourceRoot":"","sources":["../../src/webhook-receiver.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAehD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmGG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC;;;;;;;;;;;;;;;;OAgBG;gBACS,MAAM,CAAC,EAAE,MAAM;IAsB3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,OAAO,CACL,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EACtD,IAAI,EAAE,MAAM,GACX,gBAAgB;IAyDnB;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAkBxB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAazB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAuBzB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAgC/B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAwB3B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAapB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;CAa5B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=errors.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../tests/errors.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sayna-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sayna-client.test.d.ts","sourceRoot":"","sources":["../../tests/sayna-client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.test.d.ts","sourceRoot":"","sources":["../../tests/types.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=webhook-receiver.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-receiver.test.d.ts","sourceRoot":"","sources":["../../tests/webhook-receiver.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayna-ai/node-sdk",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Node.js SDK for Sayna.ai server-side WebSocket connections",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,6 +19,8 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "build": "bun run build.ts",
22
+ "test": "bun test",
23
+ "test:watch": "bun test --watch",
22
24
  "typecheck": "tsc --noEmit",
23
25
  "lint": "eslint .",
24
26
  "lint:fix": "eslint . --fix",
@@ -33,16 +35,16 @@
33
35
  "sdk",
34
36
  "server"
35
37
  ],
36
- "author": "",
38
+ "author": "@sayna-ai",
37
39
  "license": "MIT",
38
40
  "repository": {
39
41
  "type": "git",
40
- "url": ""
42
+ "url": "https://github.com/sayna/saysdk"
41
43
  },
42
44
  "bugs": {
43
- "url": ""
45
+ "url": "https://github.com/sayna/saysdk/issues"
44
46
  },
45
- "homepage": "",
47
+ "homepage": "https://sayna.ai",
46
48
  "engines": {
47
49
  "node": ">=18.0.0"
48
50
  },
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAK5B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,UAAU;gBACxC,OAAO,GAAE,MAA2C;CAKjE;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,UAAU;gBAE9C,OAAO,GAAE,MAA0F;CAMtG;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,UAAU;IAClD,SAAyB,KAAK,CAAC,EAAE,OAAO,CAAC;gBAE7B,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM7C;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,UAAU;gBACtC,OAAO,EAAE,MAAM;CAK5B;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,UAAU;gBAClC,OAAO,EAAE,MAAM;CAK5B"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE9D,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AAEzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,aAAa,CAAC,EAAE,aAAa,EAC7B,YAAY,GAAE,OAAe,GAC5B,OAAO,CAAC,WAAW,CAAC,CAUtB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"sayna-client.d.ts","sourceRoot":"","sources":["../src/sayna-client.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
File without changes