@soniox/node 1.0.0

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/dist/index.cjs ADDED
@@ -0,0 +1,3638 @@
1
+
2
+ //#region src/constants.ts
3
+ const SONIOX_API_BASE_URL = "https://api.soniox.com";
4
+ const SONIOX_API_WS_URL = "wss://stt-rt.soniox.com/transcribe-websocket";
5
+ const SONIOX_TMP_API_KEY_USAGE_TYPE = "transcribe_websocket";
6
+ const SONIOX_TMP_API_KEY_DURATION_MIN = 1;
7
+ const SONIOX_TMP_API_KEY_DURATION_MAX = 3600;
8
+ const SONIOX_API_WEBHOOK_HEADER_ENV = "SONIOX_API_WEBHOOK_HEADER";
9
+ const SONIOX_API_WEBHOOK_SECRET_ENV = "SONIOX_API_WEBHOOK_SECRET";
10
+
11
+ //#endregion
12
+ //#region src/async/auth.ts
13
+ var SonioxAuthAPI = class {
14
+ constructor(http) {
15
+ this.http = http;
16
+ }
17
+ /**
18
+ * Creates a temporary API key for client-side use.
19
+ *
20
+ * @param request - Request parameters for the temporary key
21
+ * @param signal - Optional AbortSignal for cancellation
22
+ * @returns The temporary API key response
23
+ */
24
+ async createTemporaryKey(request, signal) {
25
+ if (request.expires_in_seconds < 1 || request.expires_in_seconds > 3600) throw new Error("expires_in_seconds must be between 1 and 3600");
26
+ return (await this.http.request({
27
+ method: "POST",
28
+ path: "/v1/auth/temporary-api-key",
29
+ body: request,
30
+ ...signal && { signal }
31
+ })).data;
32
+ }
33
+ };
34
+
35
+ //#endregion
36
+ //#region src/http/errors.ts
37
+ /** Maximum body text length to include in error details (4KB) */
38
+ const MAX_BODY_TEXT_LENGTH = 4096;
39
+ /**
40
+ * Base error class for all Soniox SDK errors.
41
+ *
42
+ * All SDK errors extend this class for error handling across both REST (HTTP) and WebSocket (Real-time) APIs.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * try {
47
+ * await client.transcribe(file);
48
+ * await session.connect();
49
+ * } catch (error) {
50
+ * if (error instanceof SonioxError) {
51
+ * console.log(error.code); // 'auth_error', 'network_error', etc.
52
+ * console.log(error.statusCode); // 401, 500, etc. (when applicable)
53
+ * console.log(error.toJSON()); // Consistent serialization
54
+ * }
55
+ * }
56
+ * ```
57
+ */
58
+ var SonioxError = class extends Error {
59
+ /**
60
+ * Error code describing the type of error
61
+ */
62
+ code;
63
+ /**
64
+ * HTTP status code when applicable (e.g., 401 for auth errors, 500 for server errors).
65
+ */
66
+ statusCode;
67
+ /**
68
+ * The underlying error that caused this error, if any.
69
+ */
70
+ cause;
71
+ constructor(message, code = "soniox_error", statusCode, cause) {
72
+ super(message);
73
+ this.name = "SonioxError";
74
+ this.code = code;
75
+ this.statusCode = statusCode;
76
+ this.cause = cause;
77
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
78
+ Object.setPrototypeOf(this, new.target.prototype);
79
+ }
80
+ /**
81
+ * Creates a human-readable string representation
82
+ */
83
+ toString() {
84
+ const parts = [`${this.name} [${this.code}]: ${this.message}`];
85
+ if (this.statusCode !== void 0) parts.push(` Status: ${this.statusCode}`);
86
+ return parts.join("\n");
87
+ }
88
+ /**
89
+ * Converts to a plain object for logging/serialization
90
+ */
91
+ toJSON() {
92
+ return {
93
+ name: this.name,
94
+ code: this.code,
95
+ message: this.message,
96
+ ...this.statusCode !== void 0 && { statusCode: this.statusCode }
97
+ };
98
+ }
99
+ };
100
+ /**
101
+ * HTTP error class for all HTTP-related failures (REST API).
102
+ *
103
+ * Thrown when HTTP requests fail due to network issues, timeouts,
104
+ * server errors, or response parsing failures.
105
+ */
106
+ var SonioxHttpError = class extends SonioxError {
107
+ /** Request URL */
108
+ url;
109
+ /** HTTP method */
110
+ method;
111
+ /** Response headers (only for http_error) */
112
+ headers;
113
+ /** Response body text, capped at 4KB (only for http_error/parse_error) */
114
+ bodyText;
115
+ constructor(details) {
116
+ super(details.message, details.code, details.statusCode, details.cause);
117
+ this.name = "SonioxHttpError";
118
+ this.url = details.url;
119
+ this.method = details.method;
120
+ this.headers = details.headers;
121
+ this.bodyText = details.bodyText;
122
+ }
123
+ /**
124
+ * Creates a human-readable string representation
125
+ */
126
+ toString() {
127
+ const parts = [`SonioxHttpError [${this.code}]: ${this.message}`];
128
+ parts.push(` Method: ${this.method}`);
129
+ parts.push(` URL: ${this.url}`);
130
+ if (this.statusCode !== void 0) parts.push(` Status: ${this.statusCode}`);
131
+ return parts.join("\n");
132
+ }
133
+ /**
134
+ * Converts to a plain object for logging/serialization
135
+ */
136
+ toJSON() {
137
+ return {
138
+ name: this.name,
139
+ code: this.code,
140
+ message: this.message,
141
+ url: this.url,
142
+ method: this.method,
143
+ ...this.statusCode !== void 0 && { statusCode: this.statusCode },
144
+ ...this.headers !== void 0 && { headers: this.headers },
145
+ ...this.bodyText !== void 0 && { bodyText: this.bodyText }
146
+ };
147
+ }
148
+ };
149
+ /**
150
+ * Creates a network error
151
+ */
152
+ function createNetworkError(url, method, cause) {
153
+ return new SonioxHttpError({
154
+ code: "network_error",
155
+ message: `Network error: ${cause instanceof Error ? cause.message : "Network request failed"}`,
156
+ url,
157
+ method,
158
+ cause
159
+ });
160
+ }
161
+ /**
162
+ * Creates a timeout error
163
+ */
164
+ function createTimeoutError(url, method, timeoutMs) {
165
+ return new SonioxHttpError({
166
+ code: "timeout",
167
+ message: `Request timed out after ${timeoutMs}ms`,
168
+ url,
169
+ method
170
+ });
171
+ }
172
+ /**
173
+ * Creates an abort error
174
+ */
175
+ function createAbortError(url, method, cause) {
176
+ return new SonioxHttpError({
177
+ code: "aborted",
178
+ message: "Request was aborted",
179
+ url,
180
+ method,
181
+ cause
182
+ });
183
+ }
184
+ /**
185
+ * Creates an HTTP error (non-2xx status)
186
+ */
187
+ function createHttpError(url, method, statusCode, headers, bodyText) {
188
+ const cappedBody = truncateBodyText(bodyText);
189
+ return new SonioxHttpError({
190
+ code: "http_error",
191
+ message: `HTTP ${statusCode}`,
192
+ url,
193
+ method,
194
+ statusCode,
195
+ headers,
196
+ bodyText: cappedBody
197
+ });
198
+ }
199
+ /**
200
+ * Creates a parse error (invalid JSON, etc.)
201
+ */
202
+ function createParseError(url, method, bodyText, cause) {
203
+ const message = cause instanceof Error ? cause.message : "Failed to parse response";
204
+ const cappedBody = truncateBodyText(bodyText);
205
+ return new SonioxHttpError({
206
+ code: "parse_error",
207
+ message: `Parse error: ${message}`,
208
+ url,
209
+ method,
210
+ bodyText: cappedBody,
211
+ cause
212
+ });
213
+ }
214
+ /**
215
+ * Truncates body text to the maximum allowed length
216
+ */
217
+ function truncateBodyText(text) {
218
+ if (text.length <= MAX_BODY_TEXT_LENGTH) return text;
219
+ return text.slice(0, MAX_BODY_TEXT_LENGTH) + "... [truncated]";
220
+ }
221
+ /**
222
+ * Type guard to check if an error is an AbortError
223
+ */
224
+ function isAbortError(error) {
225
+ if (error instanceof Error) return error.name === "AbortError" || error.name === "TimeoutError";
226
+ return false;
227
+ }
228
+ /**
229
+ * Type guard to check if an error is any SonioxError (base class).
230
+ * This catches all SDK errors including HTTP and real-time errors.
231
+ */
232
+ function isSonioxError(error) {
233
+ return error instanceof SonioxError;
234
+ }
235
+ /**
236
+ * Type guard to check if an error is a SonioxHttpError
237
+ */
238
+ function isSonioxHttpError(error) {
239
+ return error instanceof SonioxHttpError;
240
+ }
241
+ /**
242
+ * Checks if an error is a 404 Not Found error
243
+ */
244
+ function isNotFoundError(error) {
245
+ return isSonioxHttpError(error) && error.statusCode === 404;
246
+ }
247
+
248
+ //#endregion
249
+ //#region src/async/files.ts
250
+ /**
251
+ * Uploaded file
252
+ */
253
+ var SonioxFile = class {
254
+ id;
255
+ filename;
256
+ size;
257
+ created_at;
258
+ client_reference_id;
259
+ constructor(data, _http) {
260
+ this._http = _http;
261
+ this.id = data.id;
262
+ this.filename = data.filename;
263
+ this.size = data.size;
264
+ this.created_at = data.created_at;
265
+ if (data.client_reference_id) {
266
+ if (data.client_reference_id.length > 256) throw new Error("client_reference_id exceeds maximum length of 256 characters");
267
+ this.client_reference_id = data.client_reference_id;
268
+ }
269
+ }
270
+ /**
271
+ * Returns the raw data for this file.
272
+ */
273
+ toJSON() {
274
+ return {
275
+ id: this.id,
276
+ filename: this.filename,
277
+ size: this.size,
278
+ created_at: this.created_at,
279
+ client_reference_id: this.client_reference_id
280
+ };
281
+ }
282
+ /**
283
+ * Permanently deletes this file.
284
+ * This operation is idempotent - succeeds even if the file doesn't exist.
285
+ *
286
+ * @param signal - Optional AbortSignal for cancellation
287
+ * @throws {SonioxHttpError} On API errors (except 404)
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * const file = await client.files.get('550e8400-e29b-41d4-a716-446655440000');
292
+ * if (file) {
293
+ * await file.delete();
294
+ * }
295
+ * ```
296
+ */
297
+ async delete(signal) {
298
+ try {
299
+ await this._http.request({
300
+ method: "DELETE",
301
+ path: `/v1/files/${this.id}`,
302
+ ...signal && { signal }
303
+ });
304
+ } catch (error) {
305
+ if (!isNotFoundError(error)) throw error;
306
+ }
307
+ }
308
+ };
309
+ /**
310
+ * Result set for file listing
311
+ */
312
+ var FileListResult = class {
313
+ /**
314
+ * Files from the first page of results
315
+ */
316
+ files;
317
+ /**
318
+ * Pagination cursor for the next page. Null if no more pages
319
+ */
320
+ next_page_cursor;
321
+ constructor(initialResponse, _http, _limit, _signal = void 0) {
322
+ this._http = _http;
323
+ this._limit = _limit;
324
+ this._signal = _signal;
325
+ this.files = initialResponse.files.map((data) => new SonioxFile(data, _http));
326
+ this.next_page_cursor = initialResponse.next_page_cursor;
327
+ }
328
+ /**
329
+ * Returns the raw data for this list result.
330
+ * Also used by JSON.stringify() to prevent serialization of internal HTTP client.
331
+ */
332
+ toJSON() {
333
+ return {
334
+ files: this.files.map((f) => f.toJSON()),
335
+ next_page_cursor: this.next_page_cursor
336
+ };
337
+ }
338
+ /**
339
+ * Returns true if there are more pages of results beyond the first page
340
+ */
341
+ isPaged() {
342
+ return this.next_page_cursor !== null;
343
+ }
344
+ /**
345
+ * Async iterator that automatically fetches all pages
346
+ * Use with `for await...of` to iterate through all files
347
+ */
348
+ async *[Symbol.asyncIterator]() {
349
+ for (const file of this.files) yield file;
350
+ let cursor = this.next_page_cursor;
351
+ while (cursor !== null) {
352
+ const response = await this._http.request({
353
+ method: "GET",
354
+ path: "/v1/files",
355
+ query: {
356
+ limit: this._limit,
357
+ cursor
358
+ },
359
+ ...this._signal && { signal: this._signal }
360
+ });
361
+ for (const data of response.data.files) yield new SonioxFile(data, this._http);
362
+ cursor = response.data.next_page_cursor;
363
+ }
364
+ }
365
+ };
366
+ /**
367
+ * Helper to extract file ID from a FileIdentifier
368
+ */
369
+ function getFileId(file) {
370
+ return typeof file === "string" ? file : file.id;
371
+ }
372
+ /**
373
+ * Maximum file size allowed by the API (1 GB)
374
+ */
375
+ const MAX_FILE_SIZE = 1073741824;
376
+ /**
377
+ * Default filename when none can be inferred
378
+ */
379
+ const DEFAULT_FILENAME = "file";
380
+ /**
381
+ * Checks if the input is an async-iterable Node.js readable stream
382
+ */
383
+ function isNodeReadableStream(input) {
384
+ return typeof input === "object" && input !== null && "pipe" in input && typeof input.pipe === "function" && Symbol.asyncIterator in input;
385
+ }
386
+ /**
387
+ * Checks if the input is a Web ReadableStream
388
+ */
389
+ function isWebReadableStream(input) {
390
+ return typeof input === "object" && input !== null && "getReader" in input && typeof input.getReader === "function";
391
+ }
392
+ /**
393
+ * Collects chunks from a Node.js readable stream into a Buffer
394
+ * Aborts early if size exceeds MAX_FILE_SIZE to prevent OOM
395
+ */
396
+ async function collectNodeStream(stream) {
397
+ const chunks = [];
398
+ let totalLength = 0;
399
+ for await (const chunk of stream) {
400
+ if (typeof chunk === "string") throw new Error("Stream returned string chunks. Use a binary stream (e.g., fs.createReadStream without encoding option).");
401
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
402
+ totalLength += buf.length;
403
+ if (totalLength > MAX_FILE_SIZE) throw new Error(`File size exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`);
404
+ chunks.push(buf);
405
+ }
406
+ return Buffer.concat(chunks);
407
+ }
408
+ /**
409
+ * Collects chunks from a Web ReadableStream into a Uint8Array
410
+ * Aborts early if size exceeds MAX_FILE_SIZE to prevent OOM
411
+ */
412
+ async function collectWebStream(stream) {
413
+ const reader = stream.getReader();
414
+ const chunks = [];
415
+ let totalLength = 0;
416
+ try {
417
+ while (true) {
418
+ const { done, value } = await reader.read();
419
+ if (done) break;
420
+ totalLength += value.length;
421
+ if (totalLength > MAX_FILE_SIZE) throw new Error(`File size exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`);
422
+ chunks.push(value);
423
+ }
424
+ } finally {
425
+ reader.releaseLock();
426
+ }
427
+ const result = new Uint8Array(totalLength);
428
+ let offset = 0;
429
+ for (const chunk of chunks) {
430
+ result.set(chunk, offset);
431
+ offset += chunk.length;
432
+ }
433
+ return result;
434
+ }
435
+ /**
436
+ * Resolves the file input to a Blob and filename
437
+ */
438
+ async function resolveFileInput(input, filenameOverride) {
439
+ if (input instanceof Blob) {
440
+ if (input.size > MAX_FILE_SIZE) throw new Error(`File size (${input.size} bytes) exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`);
441
+ return {
442
+ blob: input,
443
+ filename: filenameOverride ?? ("name" in input && typeof input.name === "string" ? input.name : DEFAULT_FILENAME)
444
+ };
445
+ }
446
+ if (Buffer.isBuffer(input)) {
447
+ if (input.length > MAX_FILE_SIZE) throw new Error(`File size (${input.length} bytes) exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`);
448
+ return {
449
+ blob: new Blob([new Uint8Array(input)]),
450
+ filename: filenameOverride ?? DEFAULT_FILENAME
451
+ };
452
+ }
453
+ if (input instanceof Uint8Array) {
454
+ if (input.length > MAX_FILE_SIZE) throw new Error(`File size (${input.length} bytes) exceeds maximum allowed size (${MAX_FILE_SIZE} bytes)`);
455
+ return {
456
+ blob: new Blob([new Uint8Array(input)]),
457
+ filename: filenameOverride ?? DEFAULT_FILENAME
458
+ };
459
+ }
460
+ if (isWebReadableStream(input)) {
461
+ const data = await collectWebStream(input);
462
+ return {
463
+ blob: new Blob([new Uint8Array(data)]),
464
+ filename: filenameOverride ?? DEFAULT_FILENAME
465
+ };
466
+ }
467
+ if (isNodeReadableStream(input)) {
468
+ const buffer = await collectNodeStream(input);
469
+ return {
470
+ blob: new Blob([new Uint8Array(buffer)]),
471
+ filename: filenameOverride ?? DEFAULT_FILENAME
472
+ };
473
+ }
474
+ throw new Error("Invalid file input. Expected Buffer, Uint8Array, Blob, or ReadableStream.");
475
+ }
476
+ var SonioxFilesAPI = class {
477
+ constructor(http) {
478
+ this.http = http;
479
+ }
480
+ /**
481
+ * Uploads a file to Soniox for transcription
482
+ *
483
+ * @param file - Buffer, Uint8Array, Blob, or ReadableStream
484
+ * @param options - Upload options
485
+ * @returns The uploaded file metadata
486
+ * @throws {SonioxHttpError} On API errors
487
+ * @throws {Error} On validation errors (file too large, invalid input)
488
+ *
489
+ * @example Upload from file path (Node.js)
490
+ * ```typescript
491
+ * import * as fs from 'node:fs';
492
+ *
493
+ * const buffer = await fs.promises.readFile('/path/to/audio.mp3');
494
+ * const file = await client.files.upload(buffer, { filename: 'audio.mp3' });
495
+ * ```
496
+ *
497
+ * @example Upload from file path (Bun)
498
+ * ```typescript
499
+ * const file = await client.files.upload(Bun.file('/path/to/audio.mp3'));
500
+ * ```
501
+ *
502
+ * @example Upload with tracking ID
503
+ * ```typescript
504
+ * const file = await client.files.upload(buffer, {
505
+ * filename: 'audio.mp3',
506
+ * client_reference_id: 'order-12345',
507
+ * });
508
+ * ```
509
+ *
510
+ * @example Upload with cancellation
511
+ * ```typescript
512
+ * const controller = new AbortController();
513
+ * setTimeout(() => controller.abort(), 30000);
514
+ *
515
+ * const file = await client.files.upload(buffer, {
516
+ * filename: 'audio.mp3',
517
+ * signal: controller.signal,
518
+ * });
519
+ * ```
520
+ */
521
+ async upload(file, options = {}) {
522
+ const { filename, client_reference_id, signal, timeout_ms } = options;
523
+ if (client_reference_id !== void 0 && client_reference_id.length > 256) throw new Error(`client_reference_id exceeds maximum length of 256 characters (got ${client_reference_id.length})`);
524
+ const { blob, filename: resolvedFilename } = await resolveFileInput(file, filename);
525
+ const formData = new FormData();
526
+ formData.append("file", blob, resolvedFilename);
527
+ if (client_reference_id !== void 0) formData.append("client_reference_id", client_reference_id);
528
+ const requestOptions = {
529
+ method: "POST",
530
+ path: "/v1/files",
531
+ body: formData
532
+ };
533
+ if (signal !== void 0) requestOptions.signal = signal;
534
+ if (timeout_ms !== void 0) requestOptions.timeoutMs = timeout_ms;
535
+ return new SonioxFile((await this.http.request(requestOptions)).data, this.http);
536
+ }
537
+ /**
538
+ * Retrieves list of uploaded files
539
+ *
540
+ * The returned result is async iterable - use `for await...of`
541
+ *
542
+ * @param options - Optional pagination and cancellation parameters
543
+ * @returns FileListResult
544
+ * @throws {SonioxHttpError}
545
+ *
546
+ * @example
547
+ * ```typescript
548
+ * const result = await client.files.list();
549
+ *
550
+ * // Automatic paging - iterates through ALL files across all pages
551
+ * for await (const file of result) {
552
+ * console.log(file.filename, file.size);
553
+ * }
554
+ *
555
+ * // Or access just the first page
556
+ * for (const file of result.files) {
557
+ * console.log(file.filename);
558
+ * }
559
+ *
560
+ * // Check if there are more pages
561
+ * if (result.isPaged()) {
562
+ * console.log('More pages available');
563
+ * }
564
+ *
565
+ * // Manual paging using cursor
566
+ * const page1 = await client.files.list({ limit: 10 });
567
+ * if (page1.next_page_cursor) {
568
+ * const page2 = await client.files.list({ cursor: page1.next_page_cursor });
569
+ * }
570
+ *
571
+ * // With cancellation
572
+ * const controller = new AbortController();
573
+ * const result = await client.files.list({ signal: controller.signal });
574
+ * ```
575
+ */
576
+ async list(options = {}) {
577
+ const { limit, cursor, signal } = options;
578
+ return new FileListResult((await this.http.request({
579
+ method: "GET",
580
+ path: "/v1/files",
581
+ query: {
582
+ limit,
583
+ cursor
584
+ },
585
+ ...signal && { signal }
586
+ })).data, this.http, limit, signal);
587
+ }
588
+ /**
589
+ * Retrieve metadata for an uploaded file.
590
+ *
591
+ * @param file - The UUID of the file or a SonioxFile instance
592
+ * @param signal - Optional AbortSignal for cancellation
593
+ * @returns The file instance, or null if not found
594
+ * @throws {SonioxHttpError} On API errors (except 404)
595
+ *
596
+ * @example
597
+ * ```typescript
598
+ * const file = await client.files.get('550e8400-e29b-41d4-a716-446655440000');
599
+ * if (file) {
600
+ * console.log(file.filename, file.size);
601
+ * }
602
+ * ```
603
+ */
604
+ async get(file, signal) {
605
+ const file_id = getFileId(file);
606
+ try {
607
+ return new SonioxFile((await this.http.request({
608
+ method: "GET",
609
+ path: `/v1/files/${file_id}`,
610
+ ...signal && { signal }
611
+ })).data, this.http);
612
+ } catch (error) {
613
+ if (isNotFoundError(error)) return null;
614
+ throw error;
615
+ }
616
+ }
617
+ /**
618
+ * Permanently deletes a file.
619
+ * This operation is idempotent - succeeds even if the file doesn't exist.
620
+ *
621
+ * @param file - The UUID of the file or a SonioxFile instance
622
+ * @param signal - Optional AbortSignal for cancellation
623
+ * @throws {SonioxHttpError} On API errors (except 404)
624
+ *
625
+ * @example
626
+ * ```typescript
627
+ * // Delete by ID
628
+ * await client.files.delete('550e8400-e29b-41d4-a716-446655440000');
629
+ *
630
+ * // Or delete a file instance
631
+ * const file = await client.files.get('550e8400-e29b-41d4-a716-446655440000');
632
+ * if (file) {
633
+ * await client.files.delete(file);
634
+ * }
635
+ *
636
+ * // Or just use the instance method
637
+ * await file.delete();
638
+ * ```
639
+ */
640
+ async delete(file, signal) {
641
+ const file_id = getFileId(file);
642
+ try {
643
+ await this.http.request({
644
+ method: "DELETE",
645
+ path: `/v1/files/${file_id}`,
646
+ ...signal && { signal }
647
+ });
648
+ } catch (error) {
649
+ if (!isNotFoundError(error)) throw error;
650
+ }
651
+ }
652
+ /**
653
+ * Permanently deletes all uploaded files.
654
+ * Iterates through all pages of files and deletes each one.
655
+ *
656
+ * @param options - Optional signal and progress callback.
657
+ * @returns The number of files deleted.
658
+ * @throws {SonioxHttpError} On API errors.
659
+ * @throws {Error} If the operation is aborted via signal.
660
+ *
661
+ * @example
662
+ * ```typescript
663
+ * // Delete all files
664
+ * const { deleted } = await client.files.purge();
665
+ * console.log(`Deleted ${deleted} files.`);
666
+ *
667
+ * // With progress logging
668
+ * const { deleted } = await client.files.purge({
669
+ * on_progress: (file, index) => {
670
+ * console.log(`Deleting file: ${file.id} (${index + 1})`);
671
+ * },
672
+ * });
673
+ *
674
+ * // With cancellation
675
+ * const controller = new AbortController();
676
+ * const { deleted } = await client.files.purge({ signal: controller.signal });
677
+ * ```
678
+ */
679
+ async purge(options = {}) {
680
+ const { signal, on_progress } = options;
681
+ const result = await this.list({ signal });
682
+ let deleted = 0;
683
+ for await (const file of result) {
684
+ signal?.throwIfAborted();
685
+ on_progress?.(file.toJSON(), deleted);
686
+ await this.delete(file, signal);
687
+ deleted++;
688
+ }
689
+ return { deleted };
690
+ }
691
+ };
692
+
693
+ //#endregion
694
+ //#region src/async/models.ts
695
+ var SonioxModelsAPI = class {
696
+ constructor(http) {
697
+ this.http = http;
698
+ }
699
+ /**
700
+ * List of available models and their attributes.
701
+ * @see https://soniox.com/docs/stt/api-reference/models/get_models
702
+ * @param signal - Optional AbortSignal for cancellation
703
+ * @returns List of available models and their attributes.
704
+ */
705
+ async list(signal) {
706
+ return (await this.http.request({
707
+ method: "GET",
708
+ path: "/v1/models",
709
+ ...signal && { signal }
710
+ })).data.models;
711
+ }
712
+ };
713
+
714
+ //#endregion
715
+ //#region src/async/segments.ts
716
+ const DEFAULT_GROUP_BY = ["speaker", "language"];
717
+ function segmentTokens(tokens, options, buildSegment$2) {
718
+ if (tokens.length === 0) return [];
719
+ const groupBy = options?.group_by ?? DEFAULT_GROUP_BY;
720
+ const groupBySpeaker = groupBy.includes("speaker");
721
+ const groupByLanguage = groupBy.includes("language");
722
+ const segments = [];
723
+ let currentTokens = [];
724
+ let currentSpeaker;
725
+ let currentLanguage;
726
+ for (const token of tokens) {
727
+ const speakerChanged = groupBySpeaker && token.speaker !== currentSpeaker;
728
+ const languageChanged = groupByLanguage && token.language !== currentLanguage;
729
+ if (currentTokens.length > 0 && (speakerChanged || languageChanged)) {
730
+ segments.push(buildSegment$2(currentTokens, currentSpeaker, currentLanguage));
731
+ currentTokens = [];
732
+ }
733
+ currentTokens.push(token);
734
+ currentSpeaker = token.speaker;
735
+ currentLanguage = token.language;
736
+ }
737
+ if (currentTokens.length > 0) segments.push(buildSegment$2(currentTokens, currentSpeaker, currentLanguage));
738
+ return segments;
739
+ }
740
+
741
+ //#endregion
742
+ //#region src/async/stt.ts
743
+ /**
744
+ * Minimum polling interval in ms
745
+ */
746
+ const MIN_POLL_INTERVAL_MS = 1e3;
747
+ /**
748
+ * Default polling interval in ms
749
+ */
750
+ const DEFAULT_POLL_INTERVAL_MS = 1e3;
751
+ /**
752
+ * Default timeout for waiting in ms (5 minutes)
753
+ */
754
+ const DEFAULT_TIMEOUT_MS$1 = 3e5;
755
+ /**
756
+ * Helper to sleep for a given number of ms, interruptible by abort signal
757
+ */
758
+ function sleep(ms, signal) {
759
+ return new Promise((resolve, reject) => {
760
+ if (signal?.aborted) {
761
+ reject(/* @__PURE__ */ new Error("Transcription wait aborted"));
762
+ return;
763
+ }
764
+ const timeoutId = setTimeout(() => {
765
+ signal?.removeEventListener("abort", onAbort);
766
+ resolve();
767
+ }, ms);
768
+ function onAbort() {
769
+ clearTimeout(timeoutId);
770
+ reject(/* @__PURE__ */ new Error("Transcription wait aborted"));
771
+ }
772
+ signal?.addEventListener("abort", onAbort, { once: true });
773
+ });
774
+ }
775
+ /**
776
+ * Helper to extract transcription ID from a TranscriptionIdentifier
777
+ */
778
+ function getTranscriptionId(transcription) {
779
+ return typeof transcription === "string" ? transcription : transcription.id;
780
+ }
781
+ /**
782
+ * Creates an AbortSignal that fires when either the timeout expires or the provided signal aborts
783
+ * Returns the signal and a cleanup function to clear the timeout
784
+ */
785
+ function createTimeoutSignal(timeout_ms, signal) {
786
+ if (timeout_ms === void 0 && signal === void 0) return {
787
+ signal: void 0,
788
+ cleanup: () => {}
789
+ };
790
+ if (timeout_ms === void 0) return {
791
+ signal,
792
+ cleanup: () => {}
793
+ };
794
+ if (!Number.isFinite(timeout_ms) || timeout_ms <= 0) throw new Error("timeout_ms must be a finite positive number");
795
+ const timeoutController = new AbortController();
796
+ const timeoutId = setTimeout(() => {
797
+ timeoutController.abort(/* @__PURE__ */ new Error(`Operation timed out after ${timeout_ms}ms`));
798
+ }, timeout_ms);
799
+ const cleanup = () => clearTimeout(timeoutId);
800
+ if (signal === void 0) return {
801
+ signal: timeoutController.signal,
802
+ cleanup
803
+ };
804
+ const combinedController = new AbortController();
805
+ const onAbort = () => {
806
+ cleanup();
807
+ timeoutController.signal.removeEventListener("abort", onTimeout);
808
+ combinedController.abort(signal.reason ?? /* @__PURE__ */ new Error("Operation aborted"));
809
+ };
810
+ const onTimeout = () => {
811
+ signal.removeEventListener("abort", onAbort);
812
+ combinedController.abort(timeoutController.signal.reason);
813
+ };
814
+ if (signal.aborted) {
815
+ cleanup();
816
+ combinedController.abort(signal.reason ?? /* @__PURE__ */ new Error("Operation aborted"));
817
+ } else {
818
+ signal.addEventListener("abort", onAbort, { once: true });
819
+ timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
820
+ }
821
+ return {
822
+ signal: combinedController.signal,
823
+ cleanup: () => {
824
+ cleanup();
825
+ signal.removeEventListener("abort", onAbort);
826
+ timeoutController.signal.removeEventListener("abort", onTimeout);
827
+ }
828
+ };
829
+ }
830
+ /**
831
+ * Groups contiguous tokens into segments based on specified grouping keys.
832
+ * A new segment starts when any of the `group_by` fields changes
833
+ *
834
+ * @param tokens - Array of transcript tokens to segment
835
+ * @param options - Segmentation options
836
+ * @param options.group_by - Fields to group by (default: ['speaker', 'language'])
837
+ * @returns Array of segments with combined text and timing
838
+ *
839
+ * @example
840
+ * ```typescript
841
+ * const transcript = await transcription.getTranscript();
842
+ *
843
+ * // Group by both speaker and language (default)
844
+ * const segments = segmentTranscript(transcript.tokens);
845
+ *
846
+ * // Group by speaker only
847
+ * const bySpeaker = segmentTranscript(transcript.tokens, { group_by: ['speaker'] });
848
+ *
849
+ * // Group by language only
850
+ * const byLanguage = segmentTranscript(transcript.tokens, { group_by: ['language'] });
851
+ *
852
+ * for (const seg of segments) {
853
+ * console.log(`[Speaker ${seg.speaker}] ${seg.text}`);
854
+ * }
855
+ * ```
856
+ */
857
+ function segmentTranscript(tokens, options = {}) {
858
+ return segmentTokens(tokens, options, buildSegment$1);
859
+ }
860
+ /**
861
+ * Helper to build a segment from a list of tokens
862
+ */
863
+ function buildSegment$1(tokens, speaker, language) {
864
+ const firstToken = tokens[0];
865
+ const lastToken = tokens[tokens.length - 1];
866
+ if (!firstToken || !lastToken) throw new Error("Cannot build segment from an empty token array");
867
+ return {
868
+ text: tokens.map((t) => t.text).join(""),
869
+ start_ms: firstToken.start_ms,
870
+ end_ms: lastToken.end_ms,
871
+ ...speaker != null && { speaker },
872
+ ...language != null && { language },
873
+ tokens
874
+ };
875
+ }
876
+ /**
877
+ * A Transcript result containing the transcribed text and tokens.
878
+ */
879
+ var SonioxTranscript = class {
880
+ /**
881
+ * Unique identifier of the transcription this transcript belongs to.
882
+ */
883
+ id;
884
+ /**
885
+ * Complete transcribed text content.
886
+ */
887
+ text;
888
+ /**
889
+ * List of detailed token information with timestamps and metadata.
890
+ */
891
+ tokens;
892
+ constructor(data) {
893
+ this.id = data.id;
894
+ this.text = data.text;
895
+ this.tokens = data.tokens;
896
+ }
897
+ /**
898
+ * Groups tokens into segments based on specified grouping keys.
899
+ *
900
+ * A new segment starts when any of the `group_by` fields changes.
901
+ *
902
+ * @param options - Segmentation options
903
+ * @param options.group_by - Fields to group by (default: ['speaker', 'language'])
904
+ * @returns Array of segments with combined text and timing
905
+ *
906
+ * @example
907
+ * ```typescript
908
+ * const transcript = await transcription.getTranscript();
909
+ *
910
+ * // Group by both speaker and language (default)
911
+ * const segments = transcript.segments();
912
+ *
913
+ * // Group by speaker only
914
+ * const bySpeaker = transcript.segments({ group_by: ['speaker'] });
915
+ *
916
+ * for (const s of segments) {
917
+ * console.log(`[Speaker ${s.speaker}] ${s.text}`);
918
+ * }
919
+ * ```
920
+ */
921
+ segments(options) {
922
+ return segmentTranscript(this.tokens, options);
923
+ }
924
+ };
925
+ /**
926
+ * A Transcription instance
927
+ */
928
+ var SonioxTranscription = class SonioxTranscription {
929
+ /**
930
+ * Unique identifier of the transcription.
931
+ */
932
+ id;
933
+ /**
934
+ * Current status of the transcription.
935
+ */
936
+ status;
937
+ /**
938
+ * UTC timestamp when the transcription was created.
939
+ */
940
+ created_at;
941
+ /**
942
+ * Speech-to-text model used.
943
+ */
944
+ model;
945
+ /**
946
+ * URL of the audio file being transcribed.
947
+ */
948
+ audio_url;
949
+ /**
950
+ * ID of the uploaded file being transcribed.
951
+ */
952
+ file_id;
953
+ /**
954
+ * Name of the file being transcribed.
955
+ */
956
+ filename;
957
+ /**
958
+ * Expected languages in the audio.
959
+ */
960
+ language_hints;
961
+ /**
962
+ * When true, speakers are identified and separated in the transcription output.
963
+ */
964
+ enable_speaker_diarization;
965
+ /**
966
+ * When true, language is detected for each part of the transcription.
967
+ */
968
+ enable_language_identification;
969
+ /**
970
+ * Duration of the audio in milliseconds. Only available after processing begins.
971
+ */
972
+ audio_duration_ms;
973
+ /**
974
+ * Error type if transcription failed.
975
+ */
976
+ error_type;
977
+ /**
978
+ * Error message if transcription failed.
979
+ */
980
+ error_message;
981
+ /**
982
+ * URL to receive webhook notifications.
983
+ */
984
+ webhook_url;
985
+ /**
986
+ * Name of the authentication header sent with webhook notifications.
987
+ */
988
+ webhook_auth_header_name;
989
+ /**
990
+ * Authentication header value (masked).
991
+ */
992
+ webhook_auth_header_value;
993
+ /**
994
+ * HTTP status code received when webhook was delivered.
995
+ */
996
+ webhook_status_code;
997
+ /**
998
+ * Optional tracking identifier.
999
+ */
1000
+ client_reference_id;
1001
+ /**
1002
+ * Additional context provided for the transcription.
1003
+ */
1004
+ context;
1005
+ /**
1006
+ * Pre-fetched transcript. Only available when using `transcribe()` with `wait: true`,
1007
+ * `fetch_transcript !== false`, and the transcription completed successfully
1008
+ */
1009
+ transcript;
1010
+ constructor(data, _http, transcript) {
1011
+ this._http = _http;
1012
+ this.id = data.id;
1013
+ this.status = data.status;
1014
+ this.created_at = data.created_at;
1015
+ this.model = data.model;
1016
+ this.audio_url = data.audio_url;
1017
+ this.file_id = data.file_id;
1018
+ this.filename = data.filename;
1019
+ this.language_hints = data.language_hints ?? void 0;
1020
+ this.enable_speaker_diarization = data.enable_speaker_diarization;
1021
+ this.enable_language_identification = data.enable_language_identification;
1022
+ this.audio_duration_ms = data.audio_duration_ms;
1023
+ this.error_type = data.error_type;
1024
+ this.error_message = data.error_message;
1025
+ this.webhook_url = data.webhook_url;
1026
+ this.webhook_auth_header_name = data.webhook_auth_header_name;
1027
+ this.webhook_auth_header_value = data.webhook_auth_header_value;
1028
+ this.webhook_status_code = data.webhook_status_code;
1029
+ this.client_reference_id = data.client_reference_id;
1030
+ this.context = data.context;
1031
+ this.transcript = transcript;
1032
+ }
1033
+ /**
1034
+ * Returns the raw data for this transcription.
1035
+ */
1036
+ toJSON() {
1037
+ return {
1038
+ id: this.id,
1039
+ status: this.status,
1040
+ created_at: this.created_at,
1041
+ model: this.model,
1042
+ audio_url: this.audio_url,
1043
+ file_id: this.file_id,
1044
+ filename: this.filename,
1045
+ language_hints: this.language_hints,
1046
+ enable_speaker_diarization: this.enable_speaker_diarization,
1047
+ enable_language_identification: this.enable_language_identification,
1048
+ audio_duration_ms: this.audio_duration_ms,
1049
+ error_type: this.error_type,
1050
+ error_message: this.error_message,
1051
+ webhook_url: this.webhook_url,
1052
+ webhook_auth_header_name: this.webhook_auth_header_name,
1053
+ webhook_auth_header_value: this.webhook_auth_header_value,
1054
+ webhook_status_code: this.webhook_status_code,
1055
+ client_reference_id: this.client_reference_id,
1056
+ context: this.context
1057
+ };
1058
+ }
1059
+ /**
1060
+ * Permanently deletes this transcription.
1061
+ * This operation is idempotent - succeeds even if the transcription doesn't exist.
1062
+ *
1063
+ * @throws {SonioxHttpError} On API errors (except 404)
1064
+ *
1065
+ * @example
1066
+ * ```typescript
1067
+ * const transcription = await client.stt.get('550e8400-e29b-41d4-a716-446655440000');
1068
+ * await transcription.delete();
1069
+ * ```
1070
+ */
1071
+ async delete() {
1072
+ try {
1073
+ await this._http.request({
1074
+ method: "DELETE",
1075
+ path: `/v1/transcriptions/${this.id}`
1076
+ });
1077
+ } catch (error) {
1078
+ if (!isNotFoundError(error)) throw error;
1079
+ }
1080
+ }
1081
+ /**
1082
+ * Permanently deletes this transcription and its associated file (if any).
1083
+ * This operation is idempotent - succeeds even if resources don't exist.
1084
+ *
1085
+ * @throws {SonioxHttpError} On API errors (except 404)
1086
+ *
1087
+ * @example
1088
+ * ```typescript
1089
+ * // Clean up both transcription and uploaded file
1090
+ * const transcription = await client.stt.transcribe({
1091
+ * model: 'stt-async-v4',
1092
+ * file: buffer,
1093
+ * wait: true,
1094
+ * });
1095
+ * // ... use transcription ...
1096
+ * await transcription.destroy(); // Deletes both transcription and file
1097
+ * ```
1098
+ */
1099
+ async destroy() {
1100
+ await this.delete();
1101
+ if (this.file_id) try {
1102
+ await this._http.request({
1103
+ method: "DELETE",
1104
+ path: `/v1/files/${this.file_id}`
1105
+ });
1106
+ } catch (error) {
1107
+ if (!isNotFoundError(error)) throw error;
1108
+ }
1109
+ }
1110
+ /**
1111
+ * Retrieves the full transcript text and tokens for this transcription.
1112
+ * Only available for successfully completed transcriptions.
1113
+ *
1114
+ * Returns cached transcript if available (when using `transcribe()` with `wait: true`).
1115
+ * Use `force: true` to bypass the cache and fetch fresh data from the API.
1116
+ *
1117
+ * @param options - Optional settings
1118
+ * @param options.force - If true, bypasses cached transcript and fetches from API
1119
+ * @param options.signal - Optional AbortSignal for request cancellation
1120
+ * @returns The transcript with text and detailed tokens, or null if not found.
1121
+ * @throws {SonioxHttpError} On API errors (except 404).
1122
+ *
1123
+ * @example
1124
+ * ```typescript
1125
+ * const transcription = await client.stt.get('550e8400-e29b-41d4-a716-446655440000');
1126
+ * if (transcription) {
1127
+ * const transcript = await transcription.getTranscript();
1128
+ * if (transcript) {
1129
+ * console.log(transcript.text);
1130
+ * }
1131
+ * }
1132
+ *
1133
+ * // Force re-fetch from API
1134
+ * const freshTranscript = await transcription.getTranscript({ force: true });
1135
+ * ```
1136
+ */
1137
+ async getTranscript(options) {
1138
+ const { force, signal } = options ?? {};
1139
+ if (!force && this.transcript !== void 0) return this.transcript;
1140
+ try {
1141
+ return new SonioxTranscript((await this._http.request({
1142
+ method: "GET",
1143
+ path: `/v1/transcriptions/${this.id}/transcript`,
1144
+ ...signal && { signal }
1145
+ })).data);
1146
+ } catch (error) {
1147
+ if (isNotFoundError(error)) return null;
1148
+ throw error;
1149
+ }
1150
+ }
1151
+ /**
1152
+ * Re-fetches this transcription to get the latest status.
1153
+ * @param signal - Optional AbortSignal for request cancellation.
1154
+ * @returns A new SonioxTranscription instance with updated data.
1155
+ * @throws {SonioxHttpError}
1156
+ *
1157
+ * @example
1158
+ * ```typescript
1159
+ * let transcription = await client.stt.get('550e8400-e29b-41d4-a716-446655440000');
1160
+ * transcription = await transcription.refresh();
1161
+ * console.log(transcription.status);
1162
+ * ```
1163
+ */
1164
+ async refresh(signal) {
1165
+ return new SonioxTranscription((await this._http.request({
1166
+ method: "GET",
1167
+ path: `/v1/transcriptions/${this.id}`,
1168
+ ...signal && { signal }
1169
+ })).data, this._http);
1170
+ }
1171
+ /**
1172
+ * Waits for the transcription to complete or fail.
1173
+ * Polls the API at the specified interval until the status is 'completed' or 'error'.
1174
+ *
1175
+ * @param options - Wait options including polling interval, timeout, and callbacks.
1176
+ * @returns The completed or errored transcription.
1177
+ * @throws {Error} If the wait times out or is aborted.
1178
+ * @throws {SonioxHttpError} On API errors.
1179
+ *
1180
+ * @example
1181
+ * ```typescript
1182
+ * const transcription = await client.stt.create({
1183
+ * model: 'stt-async-v4',
1184
+ * audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
1185
+ * });
1186
+ *
1187
+ * // Simple wait
1188
+ * const completed = await transcription.wait();
1189
+ *
1190
+ * // Wait with progress callback
1191
+ * const completed = await transcription.wait({
1192
+ * interval_ms: 2000,
1193
+ * on_status_change: (status) => console.log(`Status: ${status}`),
1194
+ * });
1195
+ * ```
1196
+ */
1197
+ async wait(options = {}) {
1198
+ const { interval_ms: requestedInterval = DEFAULT_POLL_INTERVAL_MS, timeout_ms = DEFAULT_TIMEOUT_MS$1, on_status_change, signal } = options;
1199
+ const interval_ms = Math.max(requestedInterval, MIN_POLL_INTERVAL_MS);
1200
+ if (this.status !== "queued" && this.status !== "processing") return this;
1201
+ if (signal?.aborted) throw new Error("Transcription wait aborted");
1202
+ const startTime = Date.now();
1203
+ const checkTimeout = () => {
1204
+ if (Date.now() - startTime > timeout_ms) throw new Error(`Transcription wait timed out after ${timeout_ms}ms`);
1205
+ };
1206
+ checkTimeout();
1207
+ let lastStatus = this.status;
1208
+ let current = await this.refresh(signal);
1209
+ while (current.status === "queued" || current.status === "processing") {
1210
+ if (current.status !== lastStatus) {
1211
+ on_status_change?.(current.status, current.toJSON());
1212
+ lastStatus = current.status;
1213
+ }
1214
+ checkTimeout();
1215
+ await sleep(interval_ms, signal);
1216
+ checkTimeout();
1217
+ current = await current.refresh(signal);
1218
+ }
1219
+ if (current.status !== lastStatus) on_status_change?.(current.status, current.toJSON());
1220
+ return current;
1221
+ }
1222
+ };
1223
+ /**
1224
+ * Result set for transcription listing.
1225
+ */
1226
+ var TranscriptionListResult = class {
1227
+ /**
1228
+ * Transcriptions from the first page of results.
1229
+ */
1230
+ transcriptions;
1231
+ /**
1232
+ * Pagination cursor for the next page. Null if no more pages.
1233
+ */
1234
+ next_page_cursor;
1235
+ constructor(initialResponse, _http, _options) {
1236
+ this._http = _http;
1237
+ this._options = _options;
1238
+ this.transcriptions = initialResponse.transcriptions.map((data) => new SonioxTranscription(data, _http));
1239
+ this.next_page_cursor = initialResponse.next_page_cursor;
1240
+ }
1241
+ /**
1242
+ * Returns the raw data for this list result
1243
+ */
1244
+ toJSON() {
1245
+ return {
1246
+ transcriptions: this.transcriptions.map((t) => t.toJSON()),
1247
+ next_page_cursor: this.next_page_cursor
1248
+ };
1249
+ }
1250
+ /**
1251
+ * Returns true if there are more pages of results beyond the first page.
1252
+ */
1253
+ isPaged() {
1254
+ return this.next_page_cursor !== null;
1255
+ }
1256
+ /**
1257
+ * Async iterator that automatically fetches all pages.
1258
+ * Use with `for await...of` to iterate through all transcriptions.
1259
+ */
1260
+ async *[Symbol.asyncIterator]() {
1261
+ for (const transcription of this.transcriptions) yield transcription;
1262
+ let cursor = this.next_page_cursor;
1263
+ while (cursor !== null) {
1264
+ const response = await this._http.request({
1265
+ method: "GET",
1266
+ path: "/v1/transcriptions",
1267
+ query: {
1268
+ limit: this._options.limit,
1269
+ cursor
1270
+ }
1271
+ });
1272
+ for (const data of response.data.transcriptions) yield new SonioxTranscription(data, this._http);
1273
+ cursor = response.data.next_page_cursor;
1274
+ }
1275
+ }
1276
+ };
1277
+ var SonioxSttApi = class {
1278
+ constructor(http, filesApi) {
1279
+ this.http = http;
1280
+ this.filesApi = filesApi;
1281
+ }
1282
+ /**
1283
+ * Creates a new transcription from audio_url or file_id
1284
+ *
1285
+ * @param options - Transcription options including model and audio source.
1286
+ * @returns The created transcription.
1287
+ * @throws {SonioxHttpError} On API errors.
1288
+ *
1289
+ * @example
1290
+ * ```typescript
1291
+ * // Transcribe from URL
1292
+ * const transcription = await client.stt.create({
1293
+ * model: 'stt-async-v4',
1294
+ * audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
1295
+ * });
1296
+ *
1297
+ * // Transcribe from uploaded file
1298
+ * const file = await client.files.upload(buffer);
1299
+ * const transcription = await client.stt.create({
1300
+ * model: 'stt-async-v4',
1301
+ * file_id: file.id,
1302
+ * });
1303
+ *
1304
+ * // With speaker diarization
1305
+ * const transcription = await client.stt.create({
1306
+ * model: 'stt-async-v4',
1307
+ * audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
1308
+ * enable_speaker_diarization: true,
1309
+ * });
1310
+ * ```
1311
+ */
1312
+ async create(options, signal) {
1313
+ if (options.client_reference_id !== void 0 && options.client_reference_id.length > 256) throw new Error(`client_reference_id exceeds maximum length of 256 characters (got ${options.client_reference_id.length})`);
1314
+ return new SonioxTranscription((await this.http.request({
1315
+ method: "POST",
1316
+ path: "/v1/transcriptions",
1317
+ body: options,
1318
+ ...signal && { signal }
1319
+ })).data, this.http);
1320
+ }
1321
+ /**
1322
+ * Retrieves list of transcriptions
1323
+ *
1324
+ * The returned result is async iterable - use `for await...of` to iterate through all pages
1325
+ *
1326
+ * @param options - Optional pagination and filter parameters.
1327
+ * @returns TranscriptionListResult with async iteration support.
1328
+ * @throws {SonioxHttpError} On API errors.
1329
+ *
1330
+ * @example
1331
+ * ```typescript
1332
+ * const result = await client.stt.list();
1333
+ *
1334
+ * // Automatic paging - iterates through ALL transcriptions across all pages
1335
+ * for await (const transcription of result) {
1336
+ * console.log(transcription.id, transcription.status);
1337
+ * }
1338
+ *
1339
+ * // Or access just the first page
1340
+ * for (const transcription of result.transcriptions) {
1341
+ * console.log(transcription.id);
1342
+ * }
1343
+ *
1344
+ * // Check if there are more pages
1345
+ * if (result.isPaged()) {
1346
+ * console.log('More pages available');
1347
+ * }
1348
+ * ```
1349
+ */
1350
+ async list(options = {}) {
1351
+ return new TranscriptionListResult((await this.http.request({
1352
+ method: "GET",
1353
+ path: "/v1/transcriptions",
1354
+ query: {
1355
+ limit: options.limit,
1356
+ cursor: options.cursor
1357
+ }
1358
+ })).data, this.http, options);
1359
+ }
1360
+ /**
1361
+ * Retrieves a transcription by ID
1362
+ *
1363
+ * @param id - The UUID of the transcription or a SonioxTranscription instance.
1364
+ * @returns The transcription, or null if not found.
1365
+ * @throws {SonioxHttpError} On API errors (except 404).
1366
+ *
1367
+ * @example
1368
+ * ```typescript
1369
+ * const transcription = await client.stt.get('550e8400-e29b-41d4-a716-446655440000');
1370
+ * if (transcription) {
1371
+ * console.log(transcription.status, transcription.model);
1372
+ * }
1373
+ * ```
1374
+ */
1375
+ async get(id) {
1376
+ const transcription_id = getTranscriptionId(id);
1377
+ try {
1378
+ return new SonioxTranscription((await this.http.request({
1379
+ method: "GET",
1380
+ path: `/v1/transcriptions/${transcription_id}`
1381
+ })).data, this.http);
1382
+ } catch (error) {
1383
+ if (isNotFoundError(error)) return null;
1384
+ throw error;
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Permanently deletes a transcription.
1389
+ * This operation is idempotent - succeeds even if the transcription doesn't exist.
1390
+ *
1391
+ * @param id - The UUID of the transcription or a SonioxTranscription instance
1392
+ * @throws {SonioxHttpError} On API errors (except 404)
1393
+ *
1394
+ * @example
1395
+ * ```typescript
1396
+ * // Delete by ID
1397
+ * await client.stt.delete('550e8400-e29b-41d4-a716-446655440000');
1398
+ *
1399
+ * // Or delete a transcription instance
1400
+ * const transcription = await client.stt.get('550e8400-e29b-41d4-a716-446655440000');
1401
+ * if (transcription) {
1402
+ * await client.stt.delete(transcription);
1403
+ * }
1404
+ * ```
1405
+ */
1406
+ async delete(id) {
1407
+ const transcription_id = getTranscriptionId(id);
1408
+ try {
1409
+ await this.http.request({
1410
+ method: "DELETE",
1411
+ path: `/v1/transcriptions/${transcription_id}`
1412
+ });
1413
+ } catch (error) {
1414
+ if (!isNotFoundError(error)) throw error;
1415
+ }
1416
+ }
1417
+ /**
1418
+ * Permanently deletes a transcription and its associated file (if any).
1419
+ * This operation is idempotent - succeeds even if resources don't exist.
1420
+ *
1421
+ * @param id - The UUID of the transcription or a SonioxTranscription instance
1422
+ * @throws {SonioxHttpError} On API errors (except 404)
1423
+ *
1424
+ * @example
1425
+ * ```typescript
1426
+ * // Clean up both transcription and uploaded file
1427
+ * const transcription = await client.stt.transcribe({
1428
+ * model: 'stt-async-v4',
1429
+ * file: buffer,
1430
+ * wait: true,
1431
+ * });
1432
+ * // ... use transcription ...
1433
+ * await client.stt.destroy(transcription); // Deletes both
1434
+ *
1435
+ * // Or by ID
1436
+ * await client.stt.destroy('550e8400-e29b-41d4-a716-446655440000');
1437
+ * ```
1438
+ */
1439
+ async destroy(id) {
1440
+ const transcription = await this.get(id);
1441
+ if (!transcription) return;
1442
+ await this.delete(transcription);
1443
+ if (transcription.file_id) try {
1444
+ await this.filesApi.delete(transcription.file_id);
1445
+ } catch (error) {
1446
+ if (!isNotFoundError(error)) throw error;
1447
+ }
1448
+ }
1449
+ /**
1450
+ * Retrieves the full transcript text and tokens for a completed transcription.
1451
+ * Only available for successfully completed transcriptions.
1452
+ *
1453
+ * @param id - The UUID of the transcription or a SonioxTranscription instance
1454
+ * @returns The transcript with text and detailed tokens, or null if not found
1455
+ * @throws {SonioxHttpError} On API errors (except 404)
1456
+ *
1457
+ * @example
1458
+ * ```typescript
1459
+ * const transcript = await client.stt.getTranscript('550e8400-e29b-41d4-a716-446655440000');
1460
+ * if (transcript) {
1461
+ * console.log(transcript.text);
1462
+ * for (const token of transcript.tokens) {
1463
+ * console.log(token.text, token.start_ms, token.end_ms, token.confidence);
1464
+ * }
1465
+ * }
1466
+ * ```
1467
+ */
1468
+ async getTranscript(id, signal) {
1469
+ const transcription_id = getTranscriptionId(id);
1470
+ try {
1471
+ return new SonioxTranscript((await this.http.request({
1472
+ method: "GET",
1473
+ path: `/v1/transcriptions/${transcription_id}/transcript`,
1474
+ ...signal && { signal }
1475
+ })).data);
1476
+ } catch (error) {
1477
+ if (isNotFoundError(error)) return null;
1478
+ throw error;
1479
+ }
1480
+ }
1481
+ /**
1482
+ * Waits for a transcription to complete
1483
+ *
1484
+ * @param id - The UUID of the transcription or a SonioxTranscription instance.
1485
+ * @param options - Wait options including polling interval, timeout, and callbacks.
1486
+ * @returns The completed or errored transcription.
1487
+ * @throws {Error} If the wait times out or is aborted.
1488
+ * @throws {SonioxHttpError} On API errors.
1489
+ *
1490
+ * @example
1491
+ * ```typescript
1492
+ * const completed = await client.stt.wait('550e8400-e29b-41d4-a716-446655440000');
1493
+ *
1494
+ * // With progress callback
1495
+ * const completed = await client.stt.wait('id', {
1496
+ * interval_ms: 2000,
1497
+ * on_status_change: (status) => console.log(`Status: ${status}`),
1498
+ * });
1499
+ * ```
1500
+ */
1501
+ async wait(id, options) {
1502
+ const transcription = await this.get(id);
1503
+ if (!transcription) throw new Error(`Transcription not found: ${getTranscriptionId(id)}`);
1504
+ return transcription.wait(options);
1505
+ }
1506
+ /**
1507
+ * Wrapper to transcribe from a URL.
1508
+ *
1509
+ * @param audio_url - Publicly accessible audio URL
1510
+ * @param options - Transcription options (excluding audio_url)
1511
+ * @returns The transcription (completed if wait=true, otherwise in queued/processing state).
1512
+ */
1513
+ async transcribeFromUrl(audio_url, options) {
1514
+ return this.transcribe({
1515
+ ...options,
1516
+ audio_url
1517
+ });
1518
+ }
1519
+ /**
1520
+ * Wrapper to transcribe from an uploaded file ID.
1521
+ *
1522
+ * @param file_id - ID of a previously uploaded file
1523
+ * @param options - Transcription options (excluding file_id)
1524
+ * @returns The transcription (completed if wait=true, otherwise in queued/processing state).
1525
+ */
1526
+ async transcribeFromFileId(file_id, options) {
1527
+ return this.transcribe({
1528
+ ...options,
1529
+ file_id
1530
+ });
1531
+ }
1532
+ /**
1533
+ * Wrapper to transcribe from raw file data.
1534
+ *
1535
+ * @param file - Buffer, Uint8Array, Blob, or ReadableStream
1536
+ * @param options - Transcription options (excluding file)
1537
+ * @returns The transcription (completed if wait=true, otherwise in queued/processing state).
1538
+ */
1539
+ async transcribeFromFile(file, options) {
1540
+ return this.transcribe({
1541
+ ...options,
1542
+ file
1543
+ });
1544
+ }
1545
+ /**
1546
+ * Unified transcribe method - supports direct file upload
1547
+ *
1548
+ * When `file` is provided, uploads it first then creates a transcription
1549
+ * When `wait: true`, waits for completion before returning
1550
+ * When `cleanup` is specified (requires `wait: true`), cleans up resources after completion or on error/timeout
1551
+ *
1552
+ * @param options - Transcribe options including model, audio source, and wait settings.
1553
+ * @returns The transcription (completed if wait=true, otherwise in queued/processing state).
1554
+ * @throws {SonioxHttpError} On API errors.
1555
+ * @throws {Error} On validation errors or wait timeout.
1556
+ *
1557
+ * @example
1558
+ * ```typescript
1559
+ * // Transcribe from URL and wait for completion
1560
+ * const result = await client.stt.transcribe({
1561
+ * model: 'stt-async-v4',
1562
+ * audio_url: 'https://soniox.com/media/examples/coffee_shop.mp3',
1563
+ * wait: true,
1564
+ * });
1565
+ *
1566
+ * // Upload file and transcribe in one call
1567
+ * const result = await client.stt.transcribe({
1568
+ * model: 'stt-async-v4',
1569
+ * file: buffer, // or Blob, ReadableStream
1570
+ * filename: 'meeting.mp3',
1571
+ * enable_speaker_diarization: true,
1572
+ * wait: true,
1573
+ * });
1574
+ *
1575
+ * // With wait progress callback
1576
+ * const result = await client.stt.transcribe({
1577
+ * model: 'stt-async-v4',
1578
+ * file: buffer,
1579
+ * wait: true,
1580
+ * wait_options: {
1581
+ * interval_ms: 2000,
1582
+ * on_status_change: (status) => console.log(`Status: ${status}`),
1583
+ * },
1584
+ * });
1585
+ *
1586
+ * // Auto-cleanup uploaded file after transcription
1587
+ * const result = await client.stt.transcribe({
1588
+ * model: 'stt-async-v4',
1589
+ * file: buffer,
1590
+ * wait: true,
1591
+ * cleanup: ['file'], // Deletes uploaded file, keeps transcription record
1592
+ * });
1593
+ *
1594
+ * // Auto-cleanup everything after transcription
1595
+ * const result = await client.stt.transcribe({
1596
+ * model: 'stt-async-v4',
1597
+ * file: buffer,
1598
+ * wait: true,
1599
+ * cleanup: ['file', 'transcription'], // Deletes both file and transcription record
1600
+ * });
1601
+ * ```
1602
+ */
1603
+ async transcribe(options) {
1604
+ const sourceCount = [
1605
+ options.file !== void 0,
1606
+ options.file_id !== void 0,
1607
+ options.audio_url !== void 0
1608
+ ].filter(Boolean).length;
1609
+ if (sourceCount === 0) throw new Error("One of file, file_id, or audio_url must be provided");
1610
+ if (sourceCount > 1) throw new Error("Only one of file, file_id, or audio_url can be provided");
1611
+ if (options.audio_url !== void 0) {
1612
+ if (!/^https?:\/\/[^\s]+$/.test(options.audio_url)) throw new Error("audio_url must be a valid HTTP or HTTPS URL");
1613
+ }
1614
+ if (options.webhook_auth_header_name !== void 0 !== (options.webhook_auth_header_value !== void 0)) throw new Error("webhook_auth_header_name and webhook_auth_header_value must be provided together");
1615
+ if (options.client_reference_id !== void 0 && options.client_reference_id.length > 256) throw new Error(`client_reference_id exceeds maximum length of 256 characters (got ${options.client_reference_id.length})`);
1616
+ if (options.cleanup?.length && !options.wait) throw new Error("cleanup can only be used when wait=true");
1617
+ const { signal: combinedSignal, cleanup: cleanupSignal } = createTimeoutSignal(options.timeout_ms, options.signal);
1618
+ let fileIdToCleanup;
1619
+ let transcriptionId;
1620
+ const performCleanup = async (finalFileId) => {
1621
+ if (!options.cleanup?.length) return;
1622
+ const fileId = finalFileId ?? fileIdToCleanup;
1623
+ if (options.cleanup.includes("file") && fileId) try {
1624
+ await this.filesApi.delete(fileId);
1625
+ } catch {}
1626
+ if (options.cleanup.includes("transcription") && transcriptionId) try {
1627
+ await this.delete(transcriptionId);
1628
+ } catch {}
1629
+ };
1630
+ try {
1631
+ let file_id = options.file_id;
1632
+ if (options.file) {
1633
+ const uploaded = await this.filesApi.upload(options.file, {
1634
+ filename: options.filename,
1635
+ client_reference_id: options.client_reference_id,
1636
+ signal: combinedSignal
1637
+ });
1638
+ file_id = uploaded.id;
1639
+ fileIdToCleanup = uploaded.id;
1640
+ } else if (options.file_id) fileIdToCleanup = options.file_id;
1641
+ let webhook_url = options.webhook_url;
1642
+ if (webhook_url && options.webhook_query) {
1643
+ const url = new URL(webhook_url);
1644
+ (options.webhook_query instanceof URLSearchParams ? options.webhook_query : typeof options.webhook_query === "string" ? new URLSearchParams(options.webhook_query) : new URLSearchParams(options.webhook_query)).forEach((value, key) => {
1645
+ url.searchParams.append(key, value);
1646
+ });
1647
+ webhook_url = url.toString();
1648
+ }
1649
+ const createOptions = {
1650
+ model: options.model,
1651
+ audio_url: options.audio_url,
1652
+ file_id,
1653
+ language_hints: options.language_hints,
1654
+ language_hints_strict: options.language_hints_strict,
1655
+ enable_language_identification: options.enable_language_identification,
1656
+ enable_speaker_diarization: options.enable_speaker_diarization,
1657
+ context: options.context,
1658
+ translation: options.translation,
1659
+ webhook_url,
1660
+ webhook_auth_header_name: options.webhook_auth_header_name,
1661
+ webhook_auth_header_value: options.webhook_auth_header_value,
1662
+ client_reference_id: options.client_reference_id
1663
+ };
1664
+ const transcription = await this.create(createOptions, combinedSignal);
1665
+ transcriptionId = transcription.id;
1666
+ if (transcription.file_id) fileIdToCleanup = transcription.file_id;
1667
+ if (options.wait) {
1668
+ const waitOptions = {
1669
+ ...options.wait_options,
1670
+ signal: combinedSignal ?? options.wait_options?.signal
1671
+ };
1672
+ const completed = await transcription.wait(waitOptions);
1673
+ const shouldFetchTranscript = options.fetch_transcript !== false;
1674
+ let transcript;
1675
+ if (completed.status === "completed") {
1676
+ if (shouldFetchTranscript) transcript = await completed.getTranscript(combinedSignal ? { signal: combinedSignal } : void 0);
1677
+ } else transcript = null;
1678
+ const result = new SonioxTranscription(completed.toJSON(), this.http, transcript);
1679
+ await performCleanup(completed.file_id);
1680
+ return result;
1681
+ }
1682
+ return transcription;
1683
+ } catch (error) {
1684
+ if (options.wait) await performCleanup();
1685
+ throw error;
1686
+ } finally {
1687
+ cleanupSignal();
1688
+ }
1689
+ }
1690
+ /**
1691
+ * Permanently deletes all transcriptions.
1692
+ * Iterates through all pages of transcriptions and deletes each one.
1693
+ *
1694
+ * @param options - Optional signal and progress callback.
1695
+ * @returns The number of transcriptions deleted.
1696
+ * @throws {SonioxHttpError} On API errors.
1697
+ * @throws {Error} If the operation is aborted via signal.
1698
+ *
1699
+ * @example
1700
+ * ```typescript
1701
+ * // Delete all transcriptions
1702
+ * const { deleted } = await client.stt.purge();
1703
+ * console.log(`Deleted ${deleted} transcriptions.`);
1704
+ *
1705
+ * // With progress logging
1706
+ * const { deleted } = await client.stt.purge({
1707
+ * on_progress: (transcription, index) => {
1708
+ * console.log(`Deleting transcription: ${transcription.id} (${index + 1})`);
1709
+ * },
1710
+ * });
1711
+ *
1712
+ * // With cancellation
1713
+ * const controller = new AbortController();
1714
+ * const { deleted } = await client.stt.purge({ signal: controller.signal });
1715
+ * ```
1716
+ */
1717
+ async purge(options = {}) {
1718
+ const { signal, on_progress } = options;
1719
+ const result = await this.list();
1720
+ let deleted = 0;
1721
+ for await (const transcription of result) {
1722
+ signal?.throwIfAborted();
1723
+ on_progress?.(transcription.toJSON(), deleted);
1724
+ await this.delete(transcription);
1725
+ deleted++;
1726
+ }
1727
+ return { deleted };
1728
+ }
1729
+ };
1730
+
1731
+ //#endregion
1732
+ //#region src/async/webhooks.ts
1733
+ const VALID_STATUSES = ["completed", "error"];
1734
+ /**
1735
+ * Get webhook authentication configuration from environment variables.
1736
+ *
1737
+ * Reads `SONIOX_API_WEBHOOK_HEADER` and `SONIOX_API_WEBHOOK_SECRET` environment variables.
1738
+ * Returns undefined if either variable is not set (both are required for authentication).
1739
+ *
1740
+ * @returns WebhookAuthConfig if both env vars are set, undefined otherwise
1741
+ *
1742
+ * @example
1743
+ * ```typescript
1744
+ * // Set environment variables:
1745
+ * // SONIOX_API_WEBHOOK_HEADER=X-Webhook-Secret
1746
+ * // SONIOX_API_WEBHOOK_SECRET=my-secret-token
1747
+ *
1748
+ * const auth = getWebhookAuthFromEnv();
1749
+ * // Returns: { name: 'X-Webhook-Secret', value: 'my-secret-token' }
1750
+ * ```
1751
+ */
1752
+ function getWebhookAuthFromEnv() {
1753
+ const headerName = process.env[SONIOX_API_WEBHOOK_HEADER_ENV];
1754
+ const headerValue = process.env[SONIOX_API_WEBHOOK_SECRET_ENV];
1755
+ if (headerName && headerValue) return {
1756
+ name: headerName,
1757
+ value: headerValue
1758
+ };
1759
+ }
1760
+ /**
1761
+ * Resolve webhook authentication configuration.
1762
+ *
1763
+ * If explicit auth is provided, it is used. Otherwise, attempts to read from
1764
+ * environment variables (`SONIOX_API_WEBHOOK_HEADER` and `SONIOX_API_WEBHOOK_SECRET`).
1765
+ *
1766
+ * @param auth - Explicit authentication configuration (takes precedence)
1767
+ * @returns Resolved WebhookAuthConfig or undefined if not configured
1768
+ */
1769
+ function resolveWebhookAuth(auth) {
1770
+ return auth ?? getWebhookAuthFromEnv();
1771
+ }
1772
+ /**
1773
+ * Type guard to check if a value is a valid WebhookEvent
1774
+ *
1775
+ * @param payload - Value to check
1776
+ * @returns True if payload is a valid WebhookEvent
1777
+ *
1778
+ * @example
1779
+ * ```typescript
1780
+ * if (isWebhookEvent(body)) {
1781
+ * console.log(body.id, body.status);
1782
+ * }
1783
+ * ```
1784
+ */
1785
+ function isWebhookEvent(payload) {
1786
+ if (typeof payload !== "object" || payload === null) return false;
1787
+ const obj = payload;
1788
+ if (typeof obj.id !== "string" || obj.id.length === 0) return false;
1789
+ if (!VALID_STATUSES.includes(obj.status)) return false;
1790
+ return true;
1791
+ }
1792
+ /**
1793
+ * Parse and validate a webhook event payload
1794
+ *
1795
+ * @param payload - Raw payload to parse (object or JSON string)
1796
+ * @returns Validated WebhookEvent
1797
+ * @throws Error with descriptive message if payload is invalid
1798
+ *
1799
+ * @example
1800
+ * ```typescript
1801
+ * try {
1802
+ * const event = parseWebhookEvent(req.body);
1803
+ * console.log(event.id, event.status);
1804
+ * } catch (error) {
1805
+ * console.error('Invalid webhook payload:', error.message);
1806
+ * }
1807
+ * ```
1808
+ */
1809
+ function parseWebhookEvent(payload) {
1810
+ if (typeof payload === "string") try {
1811
+ payload = JSON.parse(payload);
1812
+ } catch {
1813
+ throw new Error("Invalid webhook payload: not valid JSON");
1814
+ }
1815
+ if (typeof payload !== "object" || payload === null) throw new Error("Invalid webhook payload: expected an object");
1816
+ const obj = payload;
1817
+ if (typeof obj.id !== "string") throw new Error("Invalid webhook payload: missing or invalid \"id\" field");
1818
+ if (obj.id.length === 0) throw new Error("Invalid webhook payload: \"id\" field cannot be empty");
1819
+ if (!VALID_STATUSES.includes(obj.status)) throw new Error(`Invalid webhook payload: "status" must be "completed" or "error", got "${String(obj.status)}"`);
1820
+ return {
1821
+ id: obj.id,
1822
+ status: obj.status
1823
+ };
1824
+ }
1825
+ /**
1826
+ * Get a header value from various header formats (case-insensitive)
1827
+ */
1828
+ function getHeaderValue(headers, name) {
1829
+ const lowerName = name.toLowerCase();
1830
+ if (headers instanceof Headers) return headers.get(lowerName);
1831
+ if (typeof headers.get === "function") return headers.get(lowerName);
1832
+ const record = headers;
1833
+ if (lowerName in record) {
1834
+ const value = record[lowerName];
1835
+ return Array.isArray(value) ? value[0] ?? null : value ?? null;
1836
+ }
1837
+ for (const key of Object.keys(record)) if (key.toLowerCase() === lowerName) {
1838
+ const value = record[key];
1839
+ return Array.isArray(value) ? value[0] ?? null : value ?? null;
1840
+ }
1841
+ return null;
1842
+ }
1843
+ /**
1844
+ * Verify webhook authentication header
1845
+ *
1846
+ * @param headers - Request headers
1847
+ * @param auth - Authentication configuration with expected header name and value
1848
+ * @returns True if authentication passes, false otherwise
1849
+ *
1850
+ * @example
1851
+ * ```typescript
1852
+ * const isValid = verifyWebhookAuth(req.headers, {
1853
+ * name: 'X-Webhook-Secret',
1854
+ * value: process.env.WEBHOOK_SECRET,
1855
+ * });
1856
+ *
1857
+ * if (!isValid) {
1858
+ * return res.status(401).send('Unauthorized');
1859
+ * }
1860
+ * ```
1861
+ */
1862
+ function verifyWebhookAuth(headers, auth) {
1863
+ return getHeaderValue(headers, auth.name) === auth.value;
1864
+ }
1865
+ /**
1866
+ * Framework-agnostic webhook handler
1867
+ *
1868
+ * Validates the HTTP method, authentication (if configured), and parses the webhook payload.
1869
+ * Returns a result object that can be used to construct an HTTP response.
1870
+ *
1871
+ * Authentication is resolved in this order:
1872
+ * 1. Explicit `auth` option if provided
1873
+ * 2. Environment variables `SONIOX_API_WEBHOOK_HEADER` and `SONIOX_API_WEBHOOK_SECRET`
1874
+ * 3. No authentication if neither is configured
1875
+ *
1876
+ * @param options - Handler options including method, headers, body, and optional auth
1877
+ * @returns Result with ok status, HTTP status code, and either event or error
1878
+ *
1879
+ * @example
1880
+ * ```typescript
1881
+ * // Option 1: Set environment variables (recommended)
1882
+ * // SONIOX_API_WEBHOOK_HEADER=X-Webhook-Secret
1883
+ * // SONIOX_API_WEBHOOK_SECRET=my-secret
1884
+ * const result = handleWebhook({
1885
+ * method: req.method,
1886
+ * headers: req.headers,
1887
+ * body: req.body,
1888
+ * });
1889
+ *
1890
+ * // Option 2: Explicit auth config (overrides env vars)
1891
+ * const result = handleWebhook({
1892
+ * method: req.method,
1893
+ * headers: req.headers,
1894
+ * body: req.body,
1895
+ * auth: {
1896
+ * name: 'X-Webhook-Secret',
1897
+ * value: process.env.WEBHOOK_SECRET,
1898
+ * },
1899
+ * });
1900
+ *
1901
+ * if (result.ok) {
1902
+ * console.log('Transcription completed:', result.event.id);
1903
+ * }
1904
+ *
1905
+ * res.status(result.status).json(result.ok ? { received: true } : { error: result.error });
1906
+ * ```
1907
+ */
1908
+ function handleWebhook(options) {
1909
+ const { method, headers, body, auth } = options;
1910
+ if (method.toUpperCase() !== "POST") return {
1911
+ ok: false,
1912
+ status: 405,
1913
+ error: "Method not allowed"
1914
+ };
1915
+ const resolvedAuth = resolveWebhookAuth(auth);
1916
+ if (resolvedAuth) {
1917
+ if (!verifyWebhookAuth(headers, resolvedAuth)) return {
1918
+ ok: false,
1919
+ status: 401,
1920
+ error: "Unauthorized"
1921
+ };
1922
+ }
1923
+ try {
1924
+ return {
1925
+ ok: true,
1926
+ status: 200,
1927
+ event: parseWebhookEvent(body)
1928
+ };
1929
+ } catch (error) {
1930
+ return {
1931
+ ok: false,
1932
+ status: 400,
1933
+ error: error instanceof Error ? error.message : "Invalid webhook payload"
1934
+ };
1935
+ }
1936
+ }
1937
+ /**
1938
+ * Handle a webhook from a Fetch API Request (Bun/Deno/Node 18+)
1939
+ *
1940
+ * @param request - Fetch API Request object
1941
+ * @param auth - Optional authentication configuration
1942
+ * @returns Result with ok status, HTTP status code, and either event or error
1943
+ *
1944
+ * @example
1945
+ * ```typescript
1946
+ * // Bun.serve handler
1947
+ * Bun.serve({
1948
+ * async fetch(req) {
1949
+ * if (new URL(req.url).pathname === '/webhook') {
1950
+ * const result = await handleWebhookRequest(req);
1951
+ *
1952
+ * if (result.ok) {
1953
+ * console.log('Received webhook:', result.event.id);
1954
+ * }
1955
+ *
1956
+ * return new Response(
1957
+ * JSON.stringify(result.ok ? { received: true } : { error: result.error }),
1958
+ * { status: result.status, headers: { 'Content-Type': 'application/json' } }
1959
+ * );
1960
+ * }
1961
+ * },
1962
+ * });
1963
+ * ```
1964
+ */
1965
+ async function handleWebhookRequest(request, auth) {
1966
+ if (request.method.toUpperCase() !== "POST") return {
1967
+ ok: false,
1968
+ status: 405,
1969
+ error: "Method not allowed"
1970
+ };
1971
+ let body;
1972
+ try {
1973
+ body = await request.json();
1974
+ } catch {
1975
+ return {
1976
+ ok: false,
1977
+ status: 400,
1978
+ error: "Invalid webhook payload: not valid JSON"
1979
+ };
1980
+ }
1981
+ const options = {
1982
+ method: request.method,
1983
+ headers: request.headers,
1984
+ body
1985
+ };
1986
+ if (auth) options.auth = auth;
1987
+ return handleWebhook(options);
1988
+ }
1989
+ /**
1990
+ * Handle a webhook from an Express-like request
1991
+ *
1992
+ * @param req - Express request object
1993
+ * @param auth - Optional authentication configuration
1994
+ * @returns Result with ok status, HTTP status code, and either event or error
1995
+ *
1996
+ * @example
1997
+ * ```typescript
1998
+ * import express from 'express';
1999
+ * import { handleWebhookExpress } from '@soniox/node';
2000
+ *
2001
+ * const app = express();
2002
+ * app.use(express.json());
2003
+ *
2004
+ * app.post('/webhook', (req, res) => {
2005
+ * const result = handleWebhookExpress(req);
2006
+ *
2007
+ * if (result.ok) {
2008
+ * console.log('Received webhook:', result.event.id);
2009
+ * }
2010
+ *
2011
+ * res.status(result.status).json(
2012
+ * result.ok ? { received: true } : { error: result.error }
2013
+ * );
2014
+ * });
2015
+ * ```
2016
+ */
2017
+ function handleWebhookExpress(req, auth) {
2018
+ const options = {
2019
+ method: req.method,
2020
+ headers: req.headers,
2021
+ body: req.body
2022
+ };
2023
+ if (auth) options.auth = auth;
2024
+ return handleWebhook(options);
2025
+ }
2026
+ /**
2027
+ * Handle a webhook from a Fastify request
2028
+ *
2029
+ * @param req - Fastify request object
2030
+ * @param auth - Optional authentication configuration
2031
+ * @returns Result with ok status, HTTP status code, and either event or error
2032
+ *
2033
+ * @example
2034
+ * ```typescript
2035
+ * import Fastify from 'fastify';
2036
+ * import { handleWebhookFastify } from '@soniox/node';
2037
+ *
2038
+ * const fastify = Fastify();
2039
+ *
2040
+ * fastify.post('/webhook', async (req, reply) => {
2041
+ * const result = handleWebhookFastify(req);
2042
+ *
2043
+ * if (result.ok) {
2044
+ * console.log('Received webhook:', result.event.id);
2045
+ * }
2046
+ *
2047
+ * return reply.status(result.status).send(
2048
+ * result.ok ? { received: true } : { error: result.error }
2049
+ * );
2050
+ * });
2051
+ * ```
2052
+ */
2053
+ function handleWebhookFastify(req, auth) {
2054
+ const options = {
2055
+ method: req.method,
2056
+ headers: req.headers,
2057
+ body: req.body
2058
+ };
2059
+ if (auth) options.auth = auth;
2060
+ return handleWebhook(options);
2061
+ }
2062
+ /**
2063
+ * Handle a webhook from a NestJS request
2064
+ *
2065
+ * Works with NestJS using either Express or Fastify adapter.
2066
+ *
2067
+ * @param req - NestJS request object (injected via @Req() decorator)
2068
+ * @param auth - Optional authentication configuration (overrides env vars)
2069
+ * @returns Result with ok status, HTTP status code, and either event or error
2070
+ *
2071
+ * @example
2072
+ * ```typescript
2073
+ * import { Controller, Post, Req, Res, HttpStatus } from '@nestjs/common';
2074
+ * import { Request, Response } from 'express';
2075
+ * import { handleWebhookNestJS } from '@soniox/node';
2076
+ *
2077
+ * @Controller('webhook')
2078
+ * export class WebhookController {
2079
+ * @Post()
2080
+ * handleWebhook(@Req() req: Request, @Res() res: Response) {
2081
+ * const result = handleWebhookNestJS(req);
2082
+ *
2083
+ * if (result.ok) {
2084
+ * console.log('Received webhook:', result.event.id);
2085
+ * }
2086
+ *
2087
+ * return res.status(result.status).json(
2088
+ * result.ok ? { received: true } : { error: result.error }
2089
+ * );
2090
+ * }
2091
+ * }
2092
+ * ```
2093
+ */
2094
+ function handleWebhookNestJS(req, auth) {
2095
+ const options = {
2096
+ method: req.method,
2097
+ headers: req.headers,
2098
+ body: req.body
2099
+ };
2100
+ if (auth) options.auth = auth;
2101
+ return handleWebhook(options);
2102
+ }
2103
+ /**
2104
+ * Handle a webhook from a Hono context
2105
+ *
2106
+ * @param c - Hono context object
2107
+ * @param auth - Optional authentication configuration
2108
+ * @returns Result with ok status, HTTP status code, and either event or error
2109
+ *
2110
+ * @example
2111
+ * ```typescript
2112
+ * import { Hono } from 'hono';
2113
+ * import { handleWebhookHono } from '@soniox/node';
2114
+ *
2115
+ * const app = new Hono();
2116
+ *
2117
+ * app.post('/webhook', async (c) => {
2118
+ * const result = await handleWebhookHono(c);
2119
+ *
2120
+ * if (result.ok) {
2121
+ * console.log('Received webhook:', result.event.id);
2122
+ * }
2123
+ *
2124
+ * return c.json(
2125
+ * result.ok ? { received: true } : { error: result.error },
2126
+ * result.status
2127
+ * );
2128
+ * });
2129
+ * ```
2130
+ */
2131
+ async function handleWebhookHono(c, auth) {
2132
+ let body;
2133
+ try {
2134
+ body = await c.req.json();
2135
+ } catch {
2136
+ return {
2137
+ ok: false,
2138
+ status: 400,
2139
+ error: "Invalid webhook payload: not valid JSON"
2140
+ };
2141
+ }
2142
+ const options = {
2143
+ method: c.req.method,
2144
+ headers: { get(name) {
2145
+ return c.req.header(name) ?? null;
2146
+ } },
2147
+ body
2148
+ };
2149
+ if (auth) options.auth = auth;
2150
+ return handleWebhook(options);
2151
+ }
2152
+ /**
2153
+ * Webhook utilities API accessible via client.webhooks
2154
+ *
2155
+ * Provides methods for handling incoming Soniox webhook requests.
2156
+ * When used via the client, results include lazy fetch helpers for transcripts.
2157
+ */
2158
+ var SonioxWebhooksAPI = class {
2159
+ stt;
2160
+ /**
2161
+ * @internal
2162
+ */
2163
+ constructor(stt) {
2164
+ this.stt = stt;
2165
+ }
2166
+ /**
2167
+ * Enhance a webhook result with fetch helpers
2168
+ */
2169
+ withFetchHelpers(result) {
2170
+ const stt = this.stt;
2171
+ const event = result.event;
2172
+ if (!stt || !event) return {
2173
+ ...result,
2174
+ fetchTranscript: void 0,
2175
+ fetchTranscription: void 0
2176
+ };
2177
+ const transcriptionId = event.id;
2178
+ return {
2179
+ ...result,
2180
+ fetchTranscript: event.status === "completed" ? () => stt.getTranscript(transcriptionId) : void 0,
2181
+ fetchTranscription: () => stt.get(transcriptionId)
2182
+ };
2183
+ }
2184
+ /**
2185
+ * Get webhook authentication configuration from environment variables.
2186
+ *
2187
+ * Reads `SONIOX_API_WEBHOOK_HEADER` and `SONIOX_API_WEBHOOK_SECRET` environment variables.
2188
+ * Returns undefined if either variable is not set (both are required for authentication).
2189
+ */
2190
+ getAuthFromEnv() {
2191
+ return getWebhookAuthFromEnv();
2192
+ }
2193
+ /**
2194
+ * Type guard to check if a value is a valid WebhookEvent
2195
+ */
2196
+ isEvent(payload) {
2197
+ return isWebhookEvent(payload);
2198
+ }
2199
+ /**
2200
+ * Parse and validate a webhook event payload
2201
+ */
2202
+ parseEvent(payload) {
2203
+ return parseWebhookEvent(payload);
2204
+ }
2205
+ /**
2206
+ * Verify webhook authentication header
2207
+ */
2208
+ verifyAuth(headers, auth) {
2209
+ return verifyWebhookAuth(headers, auth);
2210
+ }
2211
+ /**
2212
+ * Framework-agnostic webhook handler
2213
+ */
2214
+ handle(options) {
2215
+ return this.withFetchHelpers(handleWebhook(options));
2216
+ }
2217
+ /**
2218
+ * Handle a webhook from a Fetch API Request
2219
+ */
2220
+ async handleRequest(request, auth) {
2221
+ const result = await handleWebhookRequest(request, auth);
2222
+ return this.withFetchHelpers(result);
2223
+ }
2224
+ /**
2225
+ * Handle a webhook from an Express-like request
2226
+ *
2227
+ * @example
2228
+ * ```typescript
2229
+ * app.post('/webhook', async (req, res) => {
2230
+ * const result = soniox.webhooks.handleExpress(req);
2231
+ *
2232
+ * if (result.ok && result.event.status === 'completed') {
2233
+ * const transcript = await result.fetchTranscript();
2234
+ * console.log(transcript?.text);
2235
+ * }
2236
+ *
2237
+ * res.status(result.status).json({ received: true });
2238
+ * });
2239
+ * ```
2240
+ */
2241
+ handleExpress(req, auth) {
2242
+ return this.withFetchHelpers(handleWebhookExpress(req, auth));
2243
+ }
2244
+ /**
2245
+ * Handle a webhook from a Fastify request
2246
+ */
2247
+ handleFastify(req, auth) {
2248
+ return this.withFetchHelpers(handleWebhookFastify(req, auth));
2249
+ }
2250
+ /**
2251
+ * Handle a webhook from a NestJS request
2252
+ */
2253
+ handleNestJS(req, auth) {
2254
+ return this.withFetchHelpers(handleWebhookNestJS(req, auth));
2255
+ }
2256
+ /**
2257
+ * Handle a webhook from a Hono context
2258
+ */
2259
+ async handleHono(c, auth) {
2260
+ const result = await handleWebhookHono(c, auth);
2261
+ return this.withFetchHelpers(result);
2262
+ }
2263
+ };
2264
+
2265
+ //#endregion
2266
+ //#region src/http/url.ts
2267
+ /**
2268
+ * Builds a complete URL from base URL, path, and query parameters
2269
+ */
2270
+ function buildUrl(baseUrl, path, query) {
2271
+ let url = joinUrl(baseUrl ?? "", path);
2272
+ if (query) {
2273
+ const params = new URLSearchParams();
2274
+ for (const [key, value] of Object.entries(query)) if (value !== void 0) params.append(key, String(value));
2275
+ const queryString = params.toString();
2276
+ if (queryString) url += (url.includes("?") ? "&" : "?") + queryString;
2277
+ }
2278
+ return url;
2279
+ }
2280
+ /**
2281
+ * Joins a base URL with a path
2282
+ */
2283
+ function joinUrl(baseUrl, path) {
2284
+ if (!baseUrl) return path;
2285
+ if (!path) return baseUrl;
2286
+ if (/^https?:\/\//i.test(path)) return path;
2287
+ return (baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl) + (path.startsWith("/") ? path : `/${path}`);
2288
+ }
2289
+ /**
2290
+ * Normalizes fetch Headers to a plain object with lowercase keys
2291
+ */
2292
+ function normalizeHeaders(headers) {
2293
+ const result = {};
2294
+ headers.forEach((value, key) => {
2295
+ result[key.toLowerCase()] = value;
2296
+ });
2297
+ return result;
2298
+ }
2299
+ /**
2300
+ * Merges header objects
2301
+ */
2302
+ function mergeHeaders(...headerObjects) {
2303
+ const result = {};
2304
+ for (const headers of headerObjects) if (headers) for (const [key, value] of Object.entries(headers)) result[key.toLowerCase()] = value;
2305
+ return result;
2306
+ }
2307
+
2308
+ //#endregion
2309
+ //#region src/http/fetch-adapter.ts
2310
+ /** Default timeout in milliseconds (30 seconds) TODO: Move to constants? */
2311
+ const DEFAULT_TIMEOUT_MS = 3e4;
2312
+ /**
2313
+ * Determines the Content-Type header based on the body type.
2314
+ */
2315
+ function getContentTypeForBody(body) {
2316
+ if (body === null || body === void 0) return;
2317
+ if (typeof body === "string") return "text/plain; charset=utf-8";
2318
+ if (typeof FormData !== "undefined" && body instanceof FormData) return;
2319
+ if (body instanceof ArrayBuffer || body instanceof Uint8Array) return "application/octet-stream";
2320
+ return "application/json";
2321
+ }
2322
+ /**
2323
+ * Prepares the request body for fetch.
2324
+ */
2325
+ function prepareBody(body) {
2326
+ if (body === null || body === void 0) return;
2327
+ if (typeof body === "string") return body;
2328
+ if (typeof FormData !== "undefined" && body instanceof FormData) return body;
2329
+ if (body instanceof ArrayBuffer) return body;
2330
+ if (body instanceof Uint8Array) return body;
2331
+ return JSON.stringify(body);
2332
+ }
2333
+ /**
2334
+ * Parses the response based on the expected type.
2335
+ */
2336
+ async function parseResponse(response, responseType, url, method) {
2337
+ const contentLength = response.headers.get("content-length");
2338
+ if (response.status === 204 || contentLength === "0") switch (responseType) {
2339
+ case "arrayBuffer": return /* @__PURE__ */ new ArrayBuffer(0);
2340
+ case "text": return "";
2341
+ case "json":
2342
+ default: return null;
2343
+ }
2344
+ switch (responseType) {
2345
+ case "text": return await response.text();
2346
+ case "arrayBuffer": return await response.arrayBuffer();
2347
+ case "json":
2348
+ default: {
2349
+ const text = await response.text();
2350
+ if (!text) return null;
2351
+ try {
2352
+ return JSON.parse(text);
2353
+ } catch (error) {
2354
+ throw createParseError(url, method, text, error);
2355
+ }
2356
+ }
2357
+ }
2358
+ }
2359
+ /**
2360
+ * Default fetch-based HTTP client.
2361
+ *
2362
+ * @example Basic usage
2363
+ * ```typescript
2364
+ * const client = new FetchHttpClient({
2365
+ * base_url: 'https://api.example.com/v1',
2366
+ * default_headers: {
2367
+ * 'Authorization': 'Bearer token',
2368
+ * },
2369
+ * });
2370
+ *
2371
+ * const response = await client.request<{ users: User[] }>({
2372
+ * method: 'GET',
2373
+ * path: '/users',
2374
+ * query: { active: true },
2375
+ * });
2376
+ * ```
2377
+ *
2378
+ * @example With custom fetch (e.g., for testing)
2379
+ * ```typescript
2380
+ * const mockFetch = vi.fn().mockResolvedValue(new Response('{}'));
2381
+ * const client = new FetchHttpClient({
2382
+ * base_url: 'https://api.example.com',
2383
+ * fetch: mockFetch,
2384
+ * });
2385
+ * ```
2386
+ *
2387
+ * @example Sending different body types
2388
+ * ```typescript
2389
+ * // JSON body (default for objects)
2390
+ * await client.request({
2391
+ * method: 'POST',
2392
+ * path: '/data',
2393
+ * body: { name: 'test', value: 123 },
2394
+ * });
2395
+ *
2396
+ * // Text body
2397
+ * await client.request({
2398
+ * method: 'POST',
2399
+ * path: '/text',
2400
+ * body: 'plain text content',
2401
+ * headers: { 'Content-Type': 'text/plain' },
2402
+ * });
2403
+ *
2404
+ * // Binary body
2405
+ * await client.request({
2406
+ * method: 'POST',
2407
+ * path: '/upload',
2408
+ * body: new Uint8Array([1, 2, 3]),
2409
+ * });
2410
+ *
2411
+ * // FormData (for file uploads)
2412
+ * const formData = new FormData();
2413
+ * formData.append('file', fileBlob, 'document.pdf');
2414
+ * await client.request({
2415
+ * method: 'POST',
2416
+ * path: '/files',
2417
+ * body: formData,
2418
+ * });
2419
+ * ```
2420
+ */
2421
+ var FetchHttpClient = class {
2422
+ baseUrl;
2423
+ defaultHeaders;
2424
+ defaultTimeoutMs;
2425
+ hooks;
2426
+ fetchImpl;
2427
+ constructor(options = {}) {
2428
+ this.baseUrl = options.base_url;
2429
+ this.defaultHeaders = options.default_headers ?? {};
2430
+ this.defaultTimeoutMs = options.default_timeout_ms ?? DEFAULT_TIMEOUT_MS;
2431
+ this.hooks = options.hooks ?? {};
2432
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
2433
+ if (!this.fetchImpl) throw new Error("fetch is not available. Please provide a fetch implementation via options.fetch");
2434
+ }
2435
+ /**
2436
+ * Performs an HTTP request.
2437
+ *
2438
+ * @param request - Request configuration
2439
+ * @returns Promise resolving to the response
2440
+ * @throws {SonioxHttpError}
2441
+ */
2442
+ async request(request) {
2443
+ const startTime = Date.now();
2444
+ const url = buildUrl(this.baseUrl, request.path, request.query);
2445
+ const method = request.method;
2446
+ const responseType = request.responseType ?? "json";
2447
+ const contentTypeHeader = getContentTypeForBody(request.body);
2448
+ const headers = mergeHeaders(typeof FormData !== "undefined" && request.body instanceof FormData ? Object.fromEntries(Object.entries(this.defaultHeaders).filter(([key]) => key.toLowerCase() !== "content-type")) : this.defaultHeaders, contentTypeHeader ? { "Content-Type": contentTypeHeader } : void 0, request.headers);
2449
+ const requestMeta = {
2450
+ startTime,
2451
+ url,
2452
+ method,
2453
+ headers
2454
+ };
2455
+ this.hooks.onRequest?.(request, requestMeta);
2456
+ const timeoutMs = request.timeoutMs ?? this.defaultTimeoutMs;
2457
+ const timeoutController = new AbortController();
2458
+ let timeoutId;
2459
+ if (timeoutMs > 0) timeoutId = setTimeout(() => {
2460
+ timeoutController.abort(/* @__PURE__ */ new Error("Request timeout"));
2461
+ }, timeoutMs);
2462
+ const combined = request.signal ? combineAbortSignals(timeoutController.signal, request.signal) : null;
2463
+ const signal = combined ? combined.signal : timeoutController.signal;
2464
+ try {
2465
+ const preparedBody = prepareBody(request.body);
2466
+ const response = await this.fetchImpl(url, {
2467
+ method,
2468
+ headers,
2469
+ signal,
2470
+ ...preparedBody !== void 0 && { body: preparedBody }
2471
+ });
2472
+ if (timeoutId) clearTimeout(timeoutId);
2473
+ const responseHeaders = normalizeHeaders(response.headers);
2474
+ if (!response.ok) {
2475
+ const bodyText = await response.text().catch(() => "");
2476
+ throw createHttpError(url, method, response.status, responseHeaders, bodyText);
2477
+ }
2478
+ const data = await parseResponse(response, responseType, url, method);
2479
+ const result = {
2480
+ status: response.status,
2481
+ headers: responseHeaders,
2482
+ data
2483
+ };
2484
+ const responseMeta = {
2485
+ ...requestMeta,
2486
+ durationMs: Date.now() - startTime,
2487
+ status: response.status
2488
+ };
2489
+ this.hooks.onResponse?.(result, responseMeta);
2490
+ return result;
2491
+ } catch (error) {
2492
+ if (timeoutId) clearTimeout(timeoutId);
2493
+ const errorMeta = {
2494
+ ...requestMeta,
2495
+ durationMs: Date.now() - startTime
2496
+ };
2497
+ const normalizedError = this.normalizeError(error, url, method, timeoutMs, timeoutController);
2498
+ this.hooks.onError?.(normalizedError, errorMeta);
2499
+ throw normalizedError;
2500
+ } finally {
2501
+ combined?.cleanup();
2502
+ }
2503
+ }
2504
+ /**
2505
+ * Normalizes various error types into SonioxHttpError.
2506
+ */
2507
+ normalizeError(error, url, method, timeoutMs, timeoutController) {
2508
+ if (error instanceof SonioxHttpError) return error;
2509
+ if (timeoutController.signal.aborted && isAbortError(error)) return createTimeoutError(url, method, timeoutMs);
2510
+ if (isAbortError(error)) return createAbortError(url, method, error);
2511
+ if (error instanceof TypeError) return createNetworkError(url, method, error);
2512
+ return createNetworkError(url, method, error);
2513
+ }
2514
+ };
2515
+ /**
2516
+ * Combines multiple AbortSignals into one.
2517
+ * The resulting signal will abort if any of the input signals abort.
2518
+ * Returns the combined signal and a cleanup function to remove listeners.
2519
+ */
2520
+ function combineAbortSignals(...signals) {
2521
+ const controller = new AbortController();
2522
+ const handlers = [];
2523
+ const cleanup = () => {
2524
+ for (const { signal, handler } of handlers) signal.removeEventListener("abort", handler);
2525
+ handlers.length = 0;
2526
+ };
2527
+ for (const signal of signals) {
2528
+ if (signal.aborted) {
2529
+ controller.abort(signal.reason);
2530
+ return {
2531
+ signal: controller.signal,
2532
+ cleanup
2533
+ };
2534
+ }
2535
+ const handler = () => {
2536
+ controller.abort(signal.reason);
2537
+ };
2538
+ handlers.push({
2539
+ signal,
2540
+ handler
2541
+ });
2542
+ signal.addEventListener("abort", handler, { once: true });
2543
+ }
2544
+ return {
2545
+ signal: controller.signal,
2546
+ cleanup
2547
+ };
2548
+ }
2549
+
2550
+ //#endregion
2551
+ //#region src/realtime/async-queue.ts
2552
+ /**
2553
+ * Generic async event queue that supports iteration with proper error propagation.
2554
+ *
2555
+ * This utility enables `for await...of` consumption of events while properly
2556
+ * surfacing errors to consumers instead of silently ending iteration.
2557
+ */
2558
+ var AsyncEventQueue = class {
2559
+ queue = [];
2560
+ waiters = [];
2561
+ done = false;
2562
+ error = null;
2563
+ /**
2564
+ * Push an event to the queue.
2565
+ * If there are waiting consumers, delivers immediately.
2566
+ */
2567
+ push(event) {
2568
+ if (this.done) return;
2569
+ const waiter = this.waiters.shift();
2570
+ if (waiter) waiter.resolve({
2571
+ value: event,
2572
+ done: false
2573
+ });
2574
+ else this.queue.push(event);
2575
+ }
2576
+ /**
2577
+ * End the queue normally.
2578
+ * Waiting consumers will receive `{ done: true }`.
2579
+ */
2580
+ end() {
2581
+ if (this.done) return;
2582
+ this.done = true;
2583
+ this.flushWaiters();
2584
+ }
2585
+ /**
2586
+ * End the queue with an error.
2587
+ * Waiting consumers will have their promises rejected.
2588
+ * Future `next()` calls will also reject with this error.
2589
+ * Any queued events are discarded.
2590
+ */
2591
+ abort(error) {
2592
+ if (this.done) return;
2593
+ this.done = true;
2594
+ this.error = error;
2595
+ this.queue = [];
2596
+ this.flushWaiters();
2597
+ }
2598
+ /**
2599
+ * Whether the queue has ended (normally or with error).
2600
+ */
2601
+ get isDone() {
2602
+ return this.done;
2603
+ }
2604
+ /**
2605
+ * Async iterator implementation.
2606
+ */
2607
+ [Symbol.asyncIterator]() {
2608
+ return { next: () => this.next() };
2609
+ }
2610
+ /**
2611
+ * Get the next event from the queue.
2612
+ */
2613
+ next() {
2614
+ const error = this.error;
2615
+ if (error) return Promise.reject(error);
2616
+ const event = this.queue.shift();
2617
+ if (event !== void 0) return Promise.resolve({
2618
+ value: event,
2619
+ done: false
2620
+ });
2621
+ if (this.done) return Promise.resolve({
2622
+ value: void 0,
2623
+ done: true
2624
+ });
2625
+ return new Promise((resolve, reject) => {
2626
+ this.waiters.push({
2627
+ resolve,
2628
+ reject
2629
+ });
2630
+ });
2631
+ }
2632
+ /**
2633
+ * Flush all waiting consumers when queue ends.
2634
+ */
2635
+ flushWaiters() {
2636
+ for (const { resolve, reject } of this.waiters) if (this.error) reject(this.error);
2637
+ else resolve({
2638
+ value: void 0,
2639
+ done: true
2640
+ });
2641
+ this.waiters = [];
2642
+ }
2643
+ };
2644
+
2645
+ //#endregion
2646
+ //#region src/realtime/emitter.ts
2647
+ /**
2648
+ * A minimal, runtime-agnostic typed event emitter.
2649
+ * Does not depend on Node.js EventEmitter.
2650
+ */
2651
+ var TypedEmitter = class {
2652
+ listeners = /* @__PURE__ */ new Map();
2653
+ errorEvent = "error";
2654
+ /**
2655
+ * Register an event handler.
2656
+ */
2657
+ on(event, handler) {
2658
+ let handlers = this.listeners.get(event);
2659
+ if (!handlers) {
2660
+ handlers = /* @__PURE__ */ new Set();
2661
+ this.listeners.set(event, handlers);
2662
+ }
2663
+ handlers.add(handler);
2664
+ return this;
2665
+ }
2666
+ /**
2667
+ * Register a one-time event handler.
2668
+ */
2669
+ once(event, handler) {
2670
+ const wrapper = ((...args) => {
2671
+ this.off(event, wrapper);
2672
+ handler(...args);
2673
+ });
2674
+ return this.on(event, wrapper);
2675
+ }
2676
+ /**
2677
+ * Remove an event handler.
2678
+ */
2679
+ off(event, handler) {
2680
+ const handlers = this.listeners.get(event);
2681
+ if (handlers) {
2682
+ handlers.delete(handler);
2683
+ if (handlers.size === 0) this.listeners.delete(event);
2684
+ }
2685
+ return this;
2686
+ }
2687
+ /**
2688
+ * Emit an event to all registered handlers.
2689
+ * Handler errors do not prevent other handlers from running.
2690
+ * Errors are reported to an `error` event if present, otherwise rethrown async.
2691
+ */
2692
+ emit(event, ...args) {
2693
+ const handlers = this.listeners.get(event);
2694
+ if (handlers) for (const handler of [...handlers]) try {
2695
+ handler(...args);
2696
+ } catch (error) {
2697
+ if (event === this.errorEvent) this.scheduleThrow(this.normalizeError(error));
2698
+ else this.reportListenerError(error);
2699
+ }
2700
+ }
2701
+ /**
2702
+ * Remove all event handlers.
2703
+ */
2704
+ removeAllListeners(event) {
2705
+ if (event !== void 0) this.listeners.delete(event);
2706
+ else this.listeners.clear();
2707
+ }
2708
+ reportListenerError(error) {
2709
+ const normalizedError = this.normalizeError(error);
2710
+ const handlers = this.listeners.get(this.errorEvent);
2711
+ if (!handlers || handlers.size === 0) {
2712
+ this.scheduleThrow(normalizedError);
2713
+ return;
2714
+ }
2715
+ for (const handler of [...handlers]) try {
2716
+ handler(normalizedError);
2717
+ } catch (handlerError) {
2718
+ this.scheduleThrow(this.normalizeError(handlerError));
2719
+ }
2720
+ }
2721
+ normalizeError(error) {
2722
+ if (error instanceof Error) return error;
2723
+ return new Error(String(error));
2724
+ }
2725
+ scheduleThrow(error) {
2726
+ setTimeout(() => {
2727
+ throw error;
2728
+ }, 0);
2729
+ }
2730
+ };
2731
+
2732
+ //#endregion
2733
+ //#region src/realtime/errors.ts
2734
+ /**
2735
+ * Real-time (WebSocket) API error classes for the Soniox SDK
2736
+ * All real-time errors extend SonioxError
2737
+ */
2738
+ /**
2739
+ * Base error class for all real-time (WebSocket) SDK errors
2740
+ */
2741
+ var RealtimeError = class extends SonioxError {
2742
+ /**
2743
+ * Original response payload for debugging.
2744
+ * Contains the raw WebSocket message that caused the error.
2745
+ */
2746
+ raw;
2747
+ constructor(message, code = "realtime_error", statusCode, raw) {
2748
+ super(message, code, statusCode);
2749
+ this.name = "RealtimeError";
2750
+ this.raw = raw;
2751
+ }
2752
+ /**
2753
+ * Creates a human-readable string representation
2754
+ */
2755
+ toString() {
2756
+ const parts = [`${this.name} [${this.code}]: ${this.message}`];
2757
+ if (this.statusCode !== void 0) parts.push(` Status: ${this.statusCode}`);
2758
+ return parts.join("\n");
2759
+ }
2760
+ /**
2761
+ * Converts to a plain object for logging/serialization
2762
+ */
2763
+ toJSON() {
2764
+ return {
2765
+ name: this.name,
2766
+ code: this.code,
2767
+ message: this.message,
2768
+ ...this.statusCode !== void 0 && { statusCode: this.statusCode },
2769
+ ...this.raw !== void 0 && { raw: this.raw }
2770
+ };
2771
+ }
2772
+ };
2773
+ /**
2774
+ * Authentication error (401).
2775
+ * Thrown when the API key is invalid or expired.
2776
+ */
2777
+ var AuthError = class extends RealtimeError {
2778
+ constructor(message, statusCode, raw) {
2779
+ super(message, "auth_error", statusCode, raw);
2780
+ this.name = "AuthError";
2781
+ }
2782
+ };
2783
+ /**
2784
+ * Bad request error (400).
2785
+ * Thrown for invalid configuration or parameters.
2786
+ */
2787
+ var BadRequestError = class extends RealtimeError {
2788
+ constructor(message, statusCode, raw) {
2789
+ super(message, "bad_request", statusCode, raw);
2790
+ this.name = "BadRequestError";
2791
+ }
2792
+ };
2793
+ /**
2794
+ * Quota error (402, 429).
2795
+ * Thrown when rate limits are exceeded or quota is exhausted.
2796
+ */
2797
+ var QuotaError = class extends RealtimeError {
2798
+ constructor(message, statusCode, raw) {
2799
+ super(message, "quota_exceeded", statusCode, raw);
2800
+ this.name = "QuotaError";
2801
+ }
2802
+ };
2803
+ /**
2804
+ * Connection error.
2805
+ * Thrown for WebSocket connection failures and transport errors.
2806
+ */
2807
+ var ConnectionError = class extends RealtimeError {
2808
+ constructor(message, raw) {
2809
+ super(message, "connection_error", void 0, raw);
2810
+ this.name = "ConnectionError";
2811
+ }
2812
+ };
2813
+ /**
2814
+ * Network error.
2815
+ * Thrown for server-side network issues (408, 500, 503).
2816
+ */
2817
+ var NetworkError = class extends RealtimeError {
2818
+ constructor(message, statusCode, raw) {
2819
+ super(message, "network_error", statusCode, raw);
2820
+ this.name = "NetworkError";
2821
+ }
2822
+ };
2823
+ /**
2824
+ * Abort error.
2825
+ * Thrown when an operation is cancelled via AbortSignal.
2826
+ */
2827
+ var AbortError = class extends RealtimeError {
2828
+ constructor(message = "Operation aborted") {
2829
+ super(message, "aborted");
2830
+ this.name = "AbortError";
2831
+ }
2832
+ };
2833
+ /**
2834
+ * State error.
2835
+ * Thrown when an operation is attempted in an invalid state.
2836
+ */
2837
+ var StateError = class extends RealtimeError {
2838
+ constructor(message) {
2839
+ super(message, "state_error");
2840
+ this.name = "StateError";
2841
+ }
2842
+ };
2843
+ /**
2844
+ * Map a Soniox error response to a typed error class.
2845
+ *
2846
+ * @param response - Error response from the WebSocket
2847
+ * @returns Appropriate error subclass
2848
+ */
2849
+ function mapErrorResponse(response) {
2850
+ const { error_code, error_message } = response;
2851
+ const message = error_message ?? "Unknown error";
2852
+ switch (error_code) {
2853
+ case 401: return new AuthError(message, error_code, response);
2854
+ case 400: return new BadRequestError(message, error_code, response);
2855
+ case 402:
2856
+ case 429: return new QuotaError(message, error_code, response);
2857
+ case 408:
2858
+ case 500:
2859
+ case 503: return new NetworkError(message, error_code, response);
2860
+ default: return new RealtimeError(message, "realtime_error", error_code, response);
2861
+ }
2862
+ }
2863
+
2864
+ //#endregion
2865
+ //#region src/realtime/stt.ts
2866
+ const DEFAULT_KEEPALIVE_INTERVAL_MS = 5e3;
2867
+ /**
2868
+ * Convert audio data to Uint8Array
2869
+ * Handles Buffer, Uint8Array, and ArrayBuffer
2870
+ */
2871
+ function toUint8Array(data) {
2872
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
2873
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
2874
+ }
2875
+ /**
2876
+ * Build the configuration message to send after WebSocket connection
2877
+ */
2878
+ function buildConfigMessage(config, apiKey) {
2879
+ return {
2880
+ api_key: apiKey,
2881
+ model: config.model,
2882
+ audio_format: config.audio_format ?? "auto",
2883
+ sample_rate: config.sample_rate,
2884
+ num_channels: config.num_channels,
2885
+ language_hints: config.language_hints,
2886
+ language_hints_strict: config.language_hints_strict,
2887
+ enable_speaker_diarization: config.enable_speaker_diarization,
2888
+ enable_language_identification: config.enable_language_identification,
2889
+ enable_endpoint_detection: config.enable_endpoint_detection,
2890
+ client_reference_id: config.client_reference_id,
2891
+ context: config.context,
2892
+ translation: config.translation
2893
+ };
2894
+ }
2895
+ /**
2896
+ * Parse a result message from the WebSocket
2897
+ */
2898
+ function parseResultMessage(data) {
2899
+ const raw = JSON.parse(data);
2900
+ if ("error_code" in raw || "error_message" in raw) throw mapErrorResponse(raw);
2901
+ return {
2902
+ tokens: (raw.tokens ?? []).map((t) => ({
2903
+ text: typeof t.text === "string" ? t.text : "",
2904
+ start_ms: typeof t.start_ms === "number" ? t.start_ms : void 0,
2905
+ end_ms: typeof t.end_ms === "number" ? t.end_ms : void 0,
2906
+ confidence: typeof t.confidence === "number" ? t.confidence : 0,
2907
+ is_final: Boolean(t.is_final),
2908
+ speaker: typeof t.speaker === "string" ? t.speaker : void 0,
2909
+ language: typeof t.language === "string" ? t.language : void 0,
2910
+ translation_status: t.translation_status === "none" || t.translation_status === "original" || t.translation_status === "translation" ? t.translation_status : void 0,
2911
+ source_language: typeof t.source_language === "string" ? t.source_language : void 0
2912
+ })),
2913
+ final_audio_proc_ms: typeof raw.final_audio_proc_ms === "number" ? raw.final_audio_proc_ms : 0,
2914
+ total_audio_proc_ms: typeof raw.total_audio_proc_ms === "number" ? raw.total_audio_proc_ms : 0,
2915
+ finished: raw.finished === true,
2916
+ raw
2917
+ };
2918
+ }
2919
+ /**
2920
+ * Check if a token is a special control token
2921
+ */
2922
+ function isSpecialToken(text) {
2923
+ return text === "<end>" || text === "<fin>";
2924
+ }
2925
+ /**
2926
+ * Filter out special control tokens from tokens array
2927
+ */
2928
+ function filterSpecialTokens(tokens) {
2929
+ return tokens.filter((t) => !isSpecialToken(t.text));
2930
+ }
2931
+ /**
2932
+ * Real-time Speech-to-Text session
2933
+ *
2934
+ * Provides WebSocket-based streaming transcription with support for:
2935
+ * - Event-based and async iterator consumption
2936
+ * - Pause/resume with automatic keepalive
2937
+ * - AbortSignal cancellation
2938
+ *
2939
+ * @example
2940
+ * ```typescript
2941
+ * const session = client.realtime.stt({ model: 'stt-rt-preview' });
2942
+ *
2943
+ * session.on('result', (result) => {
2944
+ * console.log(result.tokens.map(t => t.text).join(''));
2945
+ * });
2946
+ *
2947
+ * await session.connect();
2948
+ * await session.sendAudio(audioChunk);
2949
+ * await session.finish();
2950
+ * ```
2951
+ */
2952
+ var RealtimeSttSession = class {
2953
+ emitter = new TypedEmitter();
2954
+ eventQueue = new AsyncEventQueue();
2955
+ apiKey;
2956
+ wsBaseUrl;
2957
+ config;
2958
+ keepaliveIntervalMs;
2959
+ keepaliveEnabled;
2960
+ signal;
2961
+ ws = null;
2962
+ _state = "idle";
2963
+ _paused = false;
2964
+ keepaliveInterval = null;
2965
+ finishResolver = null;
2966
+ finishRejecter = null;
2967
+ abortHandler = null;
2968
+ constructor(apiKey, wsBaseUrl, config, options) {
2969
+ this.apiKey = apiKey;
2970
+ this.wsBaseUrl = wsBaseUrl;
2971
+ this.config = config;
2972
+ this.keepaliveIntervalMs = options?.keepalive_interval_ms ?? DEFAULT_KEEPALIVE_INTERVAL_MS;
2973
+ this.keepaliveEnabled = options?.keepalive ?? false;
2974
+ this.signal = options?.signal;
2975
+ if (this.signal) {
2976
+ this.abortHandler = () => this.handleAbort();
2977
+ this.signal.addEventListener("abort", this.abortHandler);
2978
+ }
2979
+ }
2980
+ /**
2981
+ * Current session state.
2982
+ */
2983
+ get state() {
2984
+ return this._state;
2985
+ }
2986
+ /**
2987
+ * Whether the session is currently paused.
2988
+ */
2989
+ get paused() {
2990
+ return this._paused;
2991
+ }
2992
+ /**
2993
+ * Connect to the Soniox WebSocket API.
2994
+ *
2995
+ * @throws {AbortError} If aborted
2996
+ * @throws {NetworkError} If connection fails
2997
+ * @throws {StateError} If already connected
2998
+ */
2999
+ async connect() {
3000
+ if (this._state !== "idle") throw new StateError(`Cannot connect: session is in "${this._state}" state`);
3001
+ this.checkAborted();
3002
+ this.setState("connecting");
3003
+ try {
3004
+ await this.createWebSocket();
3005
+ this.setState("connected");
3006
+ this.emitter.emit("connected");
3007
+ this.updateKeepalive();
3008
+ } catch (error) {
3009
+ this.setState("error");
3010
+ throw error;
3011
+ }
3012
+ }
3013
+ /**
3014
+ * Send audio data to the server
3015
+ *
3016
+ * @param data - Audio data as Buffer, Uint8Array, or ArrayBuffer
3017
+ * @throws {AbortError} If aborted
3018
+ * @throws {StateError} If not connected
3019
+ */
3020
+ sendAudio(data) {
3021
+ this.checkAborted();
3022
+ if (this._state !== "connected") throw new StateError(`Cannot send audio: session is in "${this._state}" state`);
3023
+ if (this._paused) return;
3024
+ const chunk = toUint8Array(data);
3025
+ this.sendMessage(chunk, true);
3026
+ }
3027
+ /**
3028
+ * Stream audio data from an async iterable source.
3029
+ *
3030
+ * Reads chunks from the iterable and sends each via {@link sendAudio}.
3031
+ * Works with Node.js ReadableStreams, Web ReadableStreams, async generators,
3032
+ * and any other `AsyncIterable<AudioData>`.
3033
+ *
3034
+ * @param stream - Async iterable yielding audio chunks
3035
+ * @param options - Optional pacing and auto-finish settings
3036
+ * @throws {AbortError} If aborted during streaming
3037
+ * @throws {StateError} If not connected
3038
+ *
3039
+ * @example
3040
+ * ```typescript
3041
+ * // Stream from a Node.js file
3042
+ * import fs from 'fs';
3043
+ * await session.sendStream(fs.createReadStream('audio.mp3'), { finish: true });
3044
+ *
3045
+ * // Stream with simulated real-time pacing
3046
+ * await session.sendStream(
3047
+ * fs.createReadStream('audio.pcm_s16le', { highWaterMark: 3840 }),
3048
+ * { pace_ms: 120, finish: true }
3049
+ * );
3050
+ *
3051
+ * // Stream from a Web fetch response
3052
+ * const response = await fetch('https://soniox.com/media/examples/coffee_shop.mp3');
3053
+ * await session.sendStream(response.body, { finish: true });
3054
+ * ```
3055
+ */
3056
+ async sendStream(stream, options) {
3057
+ for await (const chunk of stream) {
3058
+ this.sendAudio(chunk);
3059
+ if (options?.pace_ms) await new Promise((resolve) => setTimeout(resolve, options.pace_ms));
3060
+ }
3061
+ if (options?.finish) await this.finish();
3062
+ }
3063
+ /**
3064
+ * Pause audio transmission and starts automatic keepalive messages
3065
+ */
3066
+ pause() {
3067
+ if (this._paused) return;
3068
+ this._paused = true;
3069
+ this.updateKeepalive();
3070
+ }
3071
+ /**
3072
+ * Resume audio transmission
3073
+ */
3074
+ resume() {
3075
+ if (!this._paused) return;
3076
+ this._paused = false;
3077
+ this.updateKeepalive();
3078
+ }
3079
+ /**
3080
+ * Requests the server to finalize current transcription
3081
+ */
3082
+ finalize(options) {
3083
+ if (this._state !== "connected" && this._state !== "finishing") return;
3084
+ const message = { type: "finalize" };
3085
+ if (options?.trailing_silence_ms !== void 0) message.trailing_silence_ms = options.trailing_silence_ms;
3086
+ this.sendMessage(JSON.stringify(message), false);
3087
+ }
3088
+ /**
3089
+ * Send a keepalive message
3090
+ */
3091
+ keepAlive() {
3092
+ if (this._state !== "connected" && this._state !== "finishing") return;
3093
+ this.sendMessage(JSON.stringify({ type: "keepalive" }), false);
3094
+ }
3095
+ /**
3096
+ * Gracefully finish the session
3097
+ */
3098
+ async finish() {
3099
+ this.checkAborted();
3100
+ if (this._state !== "connected") throw new StateError(`Cannot finish: session is in "${this._state}" state`);
3101
+ if (this._paused) this.resume();
3102
+ this.setState("finishing");
3103
+ this.updateKeepalive();
3104
+ const finishPromise = new Promise((resolve, reject) => {
3105
+ this.finishResolver = resolve;
3106
+ this.finishRejecter = reject;
3107
+ });
3108
+ this.sendMessage("", false);
3109
+ return finishPromise;
3110
+ }
3111
+ /**
3112
+ * Close (cancel) the session immediately without waiting
3113
+ */
3114
+ close() {
3115
+ this.emitter.emit("disconnected", "client_closed");
3116
+ this.settleFinish(new StateError("Session canceled"));
3117
+ this.cleanup("canceled");
3118
+ }
3119
+ /**
3120
+ * Register an event handler
3121
+ */
3122
+ on(event, handler) {
3123
+ this.emitter.on(event, handler);
3124
+ return this;
3125
+ }
3126
+ /**
3127
+ * Register a one-time event handler
3128
+ */
3129
+ once(event, handler) {
3130
+ this.emitter.once(event, handler);
3131
+ return this;
3132
+ }
3133
+ /**
3134
+ * Remove an event handler
3135
+ */
3136
+ off(event, handler) {
3137
+ this.emitter.off(event, handler);
3138
+ return this;
3139
+ }
3140
+ /**
3141
+ * Async iterator for consuming events.
3142
+ */
3143
+ [Symbol.asyncIterator]() {
3144
+ return this.eventQueue[Symbol.asyncIterator]();
3145
+ }
3146
+ async createWebSocket() {
3147
+ return new Promise((resolve, reject) => {
3148
+ try {
3149
+ const ws = new WebSocket(this.wsBaseUrl);
3150
+ this.ws = ws;
3151
+ ws.binaryType = "arraybuffer";
3152
+ const cleanup = () => {
3153
+ ws.removeEventListener("open", onOpen);
3154
+ ws.removeEventListener("error", onError);
3155
+ };
3156
+ const onOpen = () => {
3157
+ cleanup();
3158
+ const configMessage = buildConfigMessage(this.config, this.apiKey);
3159
+ ws.send(JSON.stringify(configMessage));
3160
+ ws.addEventListener("message", this.handleMessage.bind(this));
3161
+ ws.addEventListener("close", this.handleClose.bind(this));
3162
+ ws.addEventListener("error", this.handleError.bind(this));
3163
+ resolve();
3164
+ };
3165
+ const onError = (event) => {
3166
+ cleanup();
3167
+ reject(new ConnectionError("WebSocket connection failed", event));
3168
+ };
3169
+ ws.addEventListener("open", onOpen);
3170
+ ws.addEventListener("error", onError);
3171
+ if (this.signal) this.signal.addEventListener("abort", () => {
3172
+ cleanup();
3173
+ ws.close();
3174
+ reject(new AbortError());
3175
+ }, { once: true });
3176
+ } catch (error) {
3177
+ reject(new ConnectionError("Failed to create WebSocket", error));
3178
+ }
3179
+ });
3180
+ }
3181
+ handleMessage(event) {
3182
+ if (typeof event.data !== "string") return;
3183
+ const data = event.data;
3184
+ try {
3185
+ const result = parseResultMessage(data);
3186
+ const hasEndpoint = result.tokens.some((t) => t.text === "<end>");
3187
+ const hasFinalized = result.tokens.some((t) => t.text === "<fin>");
3188
+ const userTokens = filterSpecialTokens(result.tokens);
3189
+ for (const token of userTokens) this.emitter.emit("token", token);
3190
+ const filteredResult = {
3191
+ ...result,
3192
+ tokens: userTokens
3193
+ };
3194
+ this.emitter.emit("result", filteredResult);
3195
+ this.eventQueue.push({
3196
+ kind: "result",
3197
+ data: filteredResult
3198
+ });
3199
+ if (hasEndpoint) {
3200
+ this.emitter.emit("endpoint");
3201
+ this.eventQueue.push({ kind: "endpoint" });
3202
+ }
3203
+ if (hasFinalized) {
3204
+ this.emitter.emit("finalized");
3205
+ this.eventQueue.push({ kind: "finalized" });
3206
+ }
3207
+ if (result.finished) {
3208
+ this.emitter.emit("finished");
3209
+ this.eventQueue.push({ kind: "finished" });
3210
+ this.settleFinish();
3211
+ this.cleanup("finished");
3212
+ }
3213
+ } catch (error) {
3214
+ const err = error;
3215
+ this.emitter.emit("error", err);
3216
+ this.settleFinish(err);
3217
+ this.cleanup("error", err);
3218
+ }
3219
+ }
3220
+ handleClose(event) {
3221
+ if (this.isTerminalState(this._state)) return;
3222
+ this.emitter.emit("disconnected", event.reason || void 0);
3223
+ if (this._state === "finishing") {
3224
+ const error = new ConnectionError("WebSocket closed before finished response", event);
3225
+ this.emitter.emit("error", error);
3226
+ this.settleFinish(error);
3227
+ this.cleanup("error", error);
3228
+ return;
3229
+ }
3230
+ this.cleanup("closed");
3231
+ }
3232
+ handleError(event) {
3233
+ const error = new ConnectionError("WebSocket error", event);
3234
+ this.emitter.emit("error", error);
3235
+ this.settleFinish(error);
3236
+ this.cleanup("error", error);
3237
+ }
3238
+ handleAbort() {
3239
+ const error = new AbortError();
3240
+ this.emitter.emit("error", error);
3241
+ this.settleFinish(error);
3242
+ this.cleanup("canceled", error);
3243
+ }
3244
+ setState(newState) {
3245
+ if (this._state === newState) return;
3246
+ const oldState = this._state;
3247
+ this._state = newState;
3248
+ this.emitter.emit("state_change", {
3249
+ old_state: oldState,
3250
+ new_state: newState
3251
+ });
3252
+ }
3253
+ cleanup(finalState, error) {
3254
+ this.setState(finalState);
3255
+ this.stopKeepalive();
3256
+ if (this.signal && this.abortHandler) {
3257
+ this.signal.removeEventListener("abort", this.abortHandler);
3258
+ this.abortHandler = null;
3259
+ }
3260
+ if (this.ws) {
3261
+ this.ws.close();
3262
+ this.ws = null;
3263
+ }
3264
+ if (error) this.eventQueue.abort(error);
3265
+ else this.eventQueue.end();
3266
+ this.emitter.removeAllListeners();
3267
+ }
3268
+ isTerminalState(state) {
3269
+ return state === "closed" || state === "error" || state === "finished" || state === "canceled";
3270
+ }
3271
+ checkAborted() {
3272
+ if (this.signal?.aborted) throw new AbortError();
3273
+ }
3274
+ settleFinish(error) {
3275
+ if (!this.finishResolver && !this.finishRejecter) return;
3276
+ const resolve = this.finishResolver;
3277
+ const reject = this.finishRejecter;
3278
+ this.finishResolver = null;
3279
+ this.finishRejecter = null;
3280
+ if (error) reject?.(error);
3281
+ else resolve?.();
3282
+ }
3283
+ sendMessage(data, shouldThrow) {
3284
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
3285
+ const error = new ConnectionError("WebSocket is not open");
3286
+ this.emitter.emit("error", error);
3287
+ this.settleFinish(error);
3288
+ this.cleanup("error", error);
3289
+ if (shouldThrow) throw error;
3290
+ return;
3291
+ }
3292
+ try {
3293
+ this.ws.send(data);
3294
+ } catch (err) {
3295
+ const error = new ConnectionError("WebSocket send failed", err);
3296
+ this.emitter.emit("error", error);
3297
+ this.settleFinish(error);
3298
+ this.cleanup("error", error);
3299
+ if (shouldThrow) throw error;
3300
+ }
3301
+ }
3302
+ startKeepalive() {
3303
+ if (this.keepaliveInterval) return;
3304
+ this.keepaliveInterval = setInterval(() => {
3305
+ this.keepAlive();
3306
+ }, this.keepaliveIntervalMs);
3307
+ }
3308
+ stopKeepalive() {
3309
+ if (this.keepaliveInterval) {
3310
+ clearInterval(this.keepaliveInterval);
3311
+ this.keepaliveInterval = null;
3312
+ }
3313
+ }
3314
+ updateKeepalive() {
3315
+ if ((this._state === "connected" || this._state === "finishing") && (this._paused || this.keepaliveEnabled)) this.startKeepalive();
3316
+ else this.stopKeepalive();
3317
+ }
3318
+ };
3319
+
3320
+ //#endregion
3321
+ //#region src/realtime/segments.ts
3322
+ /**
3323
+ * Groups real-time tokens into segments based on specified grouping keys.
3324
+ *
3325
+ * A new segment starts when any of the `group_by` fields changes.
3326
+ * Tokens are concatenated as-is.
3327
+ *
3328
+ * @param tokens - Array of real-time tokens to segment
3329
+ * @param options - Segmentation options
3330
+ * @param options.group_by - Fields to group by (default: ['speaker', 'language'])
3331
+ * @param options.final_only - When true, only finalized tokens are included
3332
+ * @returns Array of segments with combined text and timing (if available)
3333
+ */
3334
+ function segmentRealtimeTokens(tokens, options = {}) {
3335
+ return segmentTokens(options.final_only ? tokens.filter((token) => token.is_final) : tokens, options, buildSegment);
3336
+ }
3337
+ function buildSegment(tokens, speaker, language) {
3338
+ const firstToken = tokens[0];
3339
+ const lastToken = tokens[tokens.length - 1];
3340
+ if (!firstToken || !lastToken) throw new Error("Cannot build segment from an empty token array");
3341
+ return {
3342
+ text: tokens.map((t) => t.text).join(""),
3343
+ start_ms: firstToken.start_ms,
3344
+ end_ms: lastToken.end_ms,
3345
+ ...!!speaker && { speaker },
3346
+ ...!!language && { language },
3347
+ tokens
3348
+ };
3349
+ }
3350
+
3351
+ //#endregion
3352
+ //#region src/realtime/segment-buffer.ts
3353
+ const DEFAULT_MAX_TOKENS = 2e3;
3354
+ /**
3355
+ * Rolling buffer for turning real-time results into stable segments.
3356
+ */
3357
+ var RealtimeSegmentBuffer = class {
3358
+ tokens = [];
3359
+ groupBy;
3360
+ finalOnly;
3361
+ maxTokens;
3362
+ maxMs;
3363
+ constructor(options = {}) {
3364
+ validatePositive("max_tokens", options.max_tokens);
3365
+ validatePositive("max_ms", options.max_ms);
3366
+ this.groupBy = options.group_by;
3367
+ this.finalOnly = options.final_only ?? true;
3368
+ this.maxTokens = options.max_tokens ?? DEFAULT_MAX_TOKENS;
3369
+ this.maxMs = options.max_ms;
3370
+ }
3371
+ /**
3372
+ * Number of tokens currently buffered.
3373
+ */
3374
+ get size() {
3375
+ return this.tokens.length;
3376
+ }
3377
+ /**
3378
+ * Add a real-time result and return stable segments.
3379
+ */
3380
+ add(result) {
3381
+ const incoming = this.finalOnly ? result.tokens.filter((token) => token.is_final) : result.tokens;
3382
+ if (incoming.length > 0) this.tokens.push(...incoming);
3383
+ const stableSegments = this.flushStable(result.final_audio_proc_ms);
3384
+ this.trim();
3385
+ return stableSegments;
3386
+ }
3387
+ /**
3388
+ * Clear all buffered tokens.
3389
+ */
3390
+ reset() {
3391
+ this.tokens = [];
3392
+ }
3393
+ /**
3394
+ * Flush all buffered tokens into segments and clear the buffer.
3395
+ *
3396
+ * Includes tokens that are not yet stable by final_audio_proc_ms.
3397
+ */
3398
+ flushAll() {
3399
+ if (this.tokens.length === 0) return [];
3400
+ const segments = segmentRealtimeTokens(this.tokens, { group_by: this.groupBy });
3401
+ this.tokens = [];
3402
+ return segments;
3403
+ }
3404
+ flushStable(finalAudioProcMs) {
3405
+ if (!Number.isFinite(finalAudioProcMs) || finalAudioProcMs <= 0) return [];
3406
+ const segments = segmentRealtimeTokens(this.tokens, { group_by: this.groupBy });
3407
+ const stableSegments = [];
3408
+ let dropCount = 0;
3409
+ for (let i = 0; i < segments.length - 1; i++) {
3410
+ const segment = segments[i];
3411
+ const endMs = segment.tokens[segment.tokens.length - 1]?.end_ms;
3412
+ if (endMs === void 0 || endMs > finalAudioProcMs) break;
3413
+ stableSegments.push(segment);
3414
+ dropCount += segment.tokens.length;
3415
+ }
3416
+ if (dropCount > 0) this.tokens = this.tokens.slice(dropCount);
3417
+ return stableSegments;
3418
+ }
3419
+ trim() {
3420
+ if (this.maxTokens !== void 0 && this.tokens.length > this.maxTokens) this.tokens = this.tokens.slice(this.tokens.length - this.maxTokens);
3421
+ if (this.maxMs === void 0) return;
3422
+ const latestEndMs = findLatestEndMs(this.tokens);
3423
+ if (latestEndMs === void 0) return;
3424
+ const cutoff = latestEndMs - this.maxMs;
3425
+ if (cutoff <= 0) return;
3426
+ let dropIndex = 0;
3427
+ while (dropIndex < this.tokens.length) {
3428
+ const token = this.tokens[dropIndex];
3429
+ if (token?.end_ms === void 0 || token.end_ms >= cutoff) break;
3430
+ dropIndex += 1;
3431
+ }
3432
+ if (dropIndex > 0) this.tokens = this.tokens.slice(dropIndex);
3433
+ }
3434
+ };
3435
+ function findLatestEndMs(tokens) {
3436
+ for (let i = tokens.length - 1; i >= 0; i -= 1) {
3437
+ const endMs = tokens[i]?.end_ms;
3438
+ if (typeof endMs === "number") return endMs;
3439
+ }
3440
+ }
3441
+ function validatePositive(name, value) {
3442
+ if (value === void 0) return;
3443
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a finite positive number`);
3444
+ }
3445
+
3446
+ //#endregion
3447
+ //#region src/realtime/utterance-buffer.ts
3448
+ /**
3449
+ * Collects real-time results into utterances for endpoint-driven workflows.
3450
+ */
3451
+ var RealtimeUtteranceBuffer = class {
3452
+ segmentBuffer;
3453
+ pendingSegments = [];
3454
+ lastFinalAudioProcMs;
3455
+ lastTotalAudioProcMs;
3456
+ constructor(options = {}) {
3457
+ this.segmentBuffer = new RealtimeSegmentBuffer(options);
3458
+ }
3459
+ /**
3460
+ * Add a real-time result and collect stable segments.
3461
+ */
3462
+ addResult(result) {
3463
+ this.lastFinalAudioProcMs = result.final_audio_proc_ms;
3464
+ this.lastTotalAudioProcMs = result.total_audio_proc_ms;
3465
+ const stableSegments = this.segmentBuffer.add(result);
3466
+ if (stableSegments.length > 0) this.pendingSegments.push(...stableSegments);
3467
+ return stableSegments;
3468
+ }
3469
+ /**
3470
+ * Mark an endpoint and flush the current utterance.
3471
+ */
3472
+ markEndpoint() {
3473
+ const trailingSegments = this.segmentBuffer.flushAll();
3474
+ const segments = [...this.pendingSegments, ...trailingSegments];
3475
+ this.pendingSegments = [];
3476
+ if (segments.length === 0) return;
3477
+ return buildUtterance(segments, this.lastFinalAudioProcMs, this.lastTotalAudioProcMs);
3478
+ }
3479
+ /**
3480
+ * Clear buffered segments and tokens.
3481
+ */
3482
+ reset() {
3483
+ this.pendingSegments = [];
3484
+ this.segmentBuffer.reset();
3485
+ }
3486
+ };
3487
+ function buildUtterance(segments, finalAudioProcMs, totalAudioProcMs) {
3488
+ const tokens = segments.flatMap((segment) => segment.tokens);
3489
+ return {
3490
+ text: segments.map((segment) => segment.text).join(""),
3491
+ segments,
3492
+ tokens,
3493
+ start_ms: segments[0]?.start_ms,
3494
+ end_ms: segments[segments.length - 1]?.end_ms,
3495
+ speaker: getCommonValue(segments.map((segment) => segment.speaker)),
3496
+ language: getCommonValue(segments.map((segment) => segment.language)),
3497
+ final_audio_proc_ms: finalAudioProcMs,
3498
+ total_audio_proc_ms: totalAudioProcMs
3499
+ };
3500
+ }
3501
+ function getCommonValue(values) {
3502
+ let common;
3503
+ for (const value of values) {
3504
+ if (value === void 0) return;
3505
+ if (common === void 0) {
3506
+ common = value;
3507
+ continue;
3508
+ }
3509
+ if (value !== common) return;
3510
+ }
3511
+ return common;
3512
+ }
3513
+
3514
+ //#endregion
3515
+ //#region src/realtime/index.ts
3516
+ /**
3517
+ * Real-time API factory for creating STT sessions.
3518
+ *
3519
+ * @example
3520
+ * ```typescript
3521
+ * const session = client.realtime.stt({
3522
+ * model: 'stt-rt-preview',
3523
+ * enable_endpoint_detection: true,
3524
+ * });
3525
+ *
3526
+ * await session.connect();
3527
+ * ```
3528
+ */
3529
+ var SonioxRealtimeApi = class {
3530
+ options;
3531
+ constructor(options) {
3532
+ this.options = options;
3533
+ }
3534
+ /**
3535
+ * Create a new Speech-to-Text session.
3536
+ *
3537
+ * @param config - Session configuration (sent to server)
3538
+ * @param options - Session options (SDK-level settings)
3539
+ * @returns New STT session instance
3540
+ */
3541
+ stt(config, options) {
3542
+ const mergedOptions = {
3543
+ ...this.options.default_session_options,
3544
+ ...options
3545
+ };
3546
+ return new RealtimeSttSession(this.options.api_key, this.options.ws_base_url, config, mergedOptions);
3547
+ }
3548
+ };
3549
+
3550
+ //#endregion
3551
+ //#region src/client.ts
3552
+ /**
3553
+ * Soniox Node Client
3554
+ * @returns {SonioxNodeClient}
3555
+ *
3556
+ * @example
3557
+ * ```typescript
3558
+ * import { SonioxNodeClient } from '@soniox/node';
3559
+ *
3560
+ * const client = new SonioxNodeClient({
3561
+ * api_key: 'your-api-key',
3562
+ * });
3563
+ * ```
3564
+ */
3565
+ var SonioxNodeClient = class {
3566
+ files;
3567
+ stt;
3568
+ models;
3569
+ webhooks;
3570
+ auth;
3571
+ realtime;
3572
+ constructor(options = {}) {
3573
+ const apiKey = options.api_key ?? process.env["SONIOX_API_KEY"];
3574
+ if (!apiKey) throw new Error("Missing API key. Provide it via options.api_key or set the SONIOX_API_KEY environment variable.");
3575
+ const baseURL = options.base_url ?? process.env["SONIOX_API_BASE_URL"] ?? SONIOX_API_BASE_URL;
3576
+ const http = options.http_client ?? new FetchHttpClient({
3577
+ base_url: baseURL,
3578
+ default_headers: {
3579
+ Authorization: `Bearer ${apiKey}`,
3580
+ "Content-Type": "application/json"
3581
+ }
3582
+ });
3583
+ this.files = new SonioxFilesAPI(http);
3584
+ this.stt = new SonioxSttApi(http, this.files);
3585
+ this.models = new SonioxModelsAPI(http);
3586
+ this.webhooks = new SonioxWebhooksAPI(this.stt);
3587
+ this.auth = new SonioxAuthAPI(http);
3588
+ this.realtime = new SonioxRealtimeApi({
3589
+ api_key: apiKey,
3590
+ ws_base_url: options.realtime?.ws_base_url ?? process.env["SONIOX_WS_URL"] ?? SONIOX_API_WS_URL,
3591
+ default_session_options: options.realtime?.default_session_options
3592
+ });
3593
+ }
3594
+ };
3595
+
3596
+ //#endregion
3597
+ exports.AbortError = AbortError;
3598
+ exports.AuthError = AuthError;
3599
+ exports.BadRequestError = BadRequestError;
3600
+ exports.ConnectionError = ConnectionError;
3601
+ exports.FetchHttpClient = FetchHttpClient;
3602
+ exports.FileListResult = FileListResult;
3603
+ exports.NetworkError = NetworkError;
3604
+ exports.QuotaError = QuotaError;
3605
+ exports.RealtimeError = RealtimeError;
3606
+ exports.RealtimeSegmentBuffer = RealtimeSegmentBuffer;
3607
+ exports.RealtimeSttSession = RealtimeSttSession;
3608
+ exports.RealtimeUtteranceBuffer = RealtimeUtteranceBuffer;
3609
+ exports.SONIOX_API_BASE_URL = SONIOX_API_BASE_URL;
3610
+ exports.SONIOX_API_WEBHOOK_HEADER_ENV = SONIOX_API_WEBHOOK_HEADER_ENV;
3611
+ exports.SONIOX_API_WEBHOOK_SECRET_ENV = SONIOX_API_WEBHOOK_SECRET_ENV;
3612
+ exports.SONIOX_API_WS_URL = SONIOX_API_WS_URL;
3613
+ exports.SONIOX_TMP_API_KEY_DURATION_MAX = SONIOX_TMP_API_KEY_DURATION_MAX;
3614
+ exports.SONIOX_TMP_API_KEY_DURATION_MIN = SONIOX_TMP_API_KEY_DURATION_MIN;
3615
+ exports.SONIOX_TMP_API_KEY_USAGE_TYPE = SONIOX_TMP_API_KEY_USAGE_TYPE;
3616
+ exports.SonioxError = SonioxError;
3617
+ exports.SonioxFile = SonioxFile;
3618
+ exports.SonioxHttpError = SonioxHttpError;
3619
+ exports.SonioxNodeClient = SonioxNodeClient;
3620
+ exports.SonioxRealtimeApi = SonioxRealtimeApi;
3621
+ exports.SonioxTranscript = SonioxTranscript;
3622
+ exports.SonioxTranscription = SonioxTranscription;
3623
+ exports.StateError = StateError;
3624
+ exports.TranscriptionListResult = TranscriptionListResult;
3625
+ exports.buildUrl = buildUrl;
3626
+ exports.createAbortError = createAbortError;
3627
+ exports.createHttpError = createHttpError;
3628
+ exports.createNetworkError = createNetworkError;
3629
+ exports.createParseError = createParseError;
3630
+ exports.createTimeoutError = createTimeoutError;
3631
+ exports.isAbortError = isAbortError;
3632
+ exports.isSonioxError = isSonioxError;
3633
+ exports.isSonioxHttpError = isSonioxHttpError;
3634
+ exports.mergeHeaders = mergeHeaders;
3635
+ exports.normalizeHeaders = normalizeHeaders;
3636
+ exports.segmentRealtimeTokens = segmentRealtimeTokens;
3637
+ exports.segmentTranscript = segmentTranscript;
3638
+ //# sourceMappingURL=index.cjs.map