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