@breeze.blue/sdk 0.7.0 → 0.8.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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.8.0
6
+
7
+ - Realtime TTS sessions prefer the optional query-free direct WebSocket URL
8
+ and authenticate with a non-echoed token-bearing subprotocol. Existing
9
+ public session URLs remain the fallback for older deployments and clients.
10
+
5
11
  ## 0.7.0
6
12
 
7
13
  - Realtime TTS connections can update synthesis instructions between turns
package/README.md CHANGED
@@ -278,9 +278,15 @@ const session = await client.textToSpeech.realtime.createSession("voc_...", {
278
278
  // Later, when the first text is ready:
279
279
  const connection = await client.textToSpeech.realtime.connect("voc_...", {
280
280
  clientSecret: session.clientSecret,
281
+ websocketUrl: session.websocketUrl,
282
+ directWebsocketUrl: session.directWebsocketUrl,
281
283
  });
282
284
  ```
283
285
 
286
+ When the session response includes `directWebsocketUrl`, the SDK prefers its
287
+ query-free origin and authenticates with the WebSocket subprotocol. It falls
288
+ back to `websocketUrl` for services that have not enabled the direct transport.
289
+
284
290
  The same `clientSecret` handoff lets a browser connect without ever seeing your
285
291
  API key: create the session on your server, hand `session.clientSecret` to the
286
292
  page, and build a key-less client there:
@@ -315,7 +321,7 @@ const connection = await browserClient.textToSpeech.realtime.connectManaged("voc
315
321
  reconnect,
316
322
  });
317
323
  }
318
- return response.json(); // { clientSecret, websocketUrl? }
324
+ return response.json(); // { clientSecret, websocketUrl?, directWebsocketUrl? }
319
325
  },
320
326
  });
321
327
  ```
package/dist/client.d.ts CHANGED
@@ -54,6 +54,7 @@ export declare class RealtimeTextToSpeechConnection implements AsyncIterable<Rea
54
54
  static open(socket: BreezeBlueWebSocket, options?: {
55
55
  timeout?: number;
56
56
  signal?: AbortSignal;
57
+ requiredSubprotocol?: string;
57
58
  }): Promise<RealtimeTextToSpeechConnection>;
58
59
  startTurn(turnId?: string): void;
59
60
  appendText(text: string): void;
package/dist/client.js CHANGED
@@ -15,6 +15,9 @@ const MAX_REALTIME_RECONNECT_DELAY_MS = 4_000;
15
15
  const REALTIME_SETTLEMENT_DRAIN_TIMEOUT_MS = 5_000;
16
16
  const REALTIME_CLOSE_EVENT_GRACE_MS = 50;
17
17
  const REALTIME_MAX_INSTRUCTIONS_CHARACTERS = 1_000;
18
+ const REALTIME_SAFE_SUBPROTOCOL = "breeze-realtime-v1";
19
+ const REALTIME_TOKEN_SUBPROTOCOL_PREFIX = "breeze-realtime-token.";
20
+ const REALTIME_MAX_TOKEN_SUBPROTOCOL_LENGTH = 7 * 1024;
18
21
  const WEBSOCKET_OPEN = 1;
19
22
  export class BreezeBlueClient {
20
23
  apiKey;
@@ -129,31 +132,55 @@ class RealtimeTextToSpeechResource {
129
132
  return this.client.requestJson("POST", `/v1/text-to-speech/${encodeURIComponent(voiceId)}/realtime-sessions`, undefined, request, options);
130
133
  }
131
134
  async connect(voiceId, options = {}) {
132
- if ((options.clientSecret !== undefined || options.websocketUrl !== undefined) && hasRealtimeSessionParams(options)) {
135
+ if ((options.clientSecret !== undefined ||
136
+ options.websocketUrl !== undefined ||
137
+ options.directWebsocketUrl !== undefined) && hasRealtimeSessionParams(options)) {
133
138
  console.warn("@breeze.blue/sdk: realtime.connect ignores modelId, languageCode, instructions, voiceSettings, " +
134
- "inactivityTimeoutSeconds, and enableLogging when clientSecret or websocketUrl is provided. " +
139
+ "inactivityTimeoutSeconds, and enableLogging when session credentials are provided. " +
135
140
  "Configure these initial values when creating the realtime session instead. " +
136
141
  "To change instructions between turns, call connection.updateInstructions() and wait for session.updated.");
137
142
  }
138
- const websocketUrl = options.websocketUrl ??
139
- (options.clientSecret
140
- ? buildUrl(this.client.baseUrl.replace(/^http/i, "ws"), `/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream-input`, { client_secret: options.clientSecret })
141
- : (await this.createSession(voiceId, {
142
- modelId: options.modelId,
143
- languageCode: options.languageCode,
144
- instructions: options.instructions,
145
- voiceSettings: options.voiceSettings,
146
- inactivityTimeoutSeconds: options.inactivityTimeoutSeconds,
147
- enableLogging: options.enableLogging,
148
- }, {
149
- signal: options.signal,
150
- timeout: options.timeout,
151
- })).websocketUrl);
143
+ let clientSecret = options.clientSecret;
144
+ let websocketUrl = options.websocketUrl;
145
+ let directWebsocketUrl = options.directWebsocketUrl;
146
+ if (!clientSecret && !websocketUrl && !directWebsocketUrl) {
147
+ const session = await this.createSession(voiceId, {
148
+ modelId: options.modelId,
149
+ languageCode: options.languageCode,
150
+ instructions: options.instructions,
151
+ voiceSettings: options.voiceSettings,
152
+ inactivityTimeoutSeconds: options.inactivityTimeoutSeconds,
153
+ enableLogging: options.enableLogging,
154
+ }, {
155
+ signal: options.signal,
156
+ timeout: options.timeout,
157
+ });
158
+ clientSecret = session.clientSecret;
159
+ websocketUrl = session.websocketUrl;
160
+ directWebsocketUrl = session.directWebsocketUrl;
161
+ }
162
+ if (!websocketUrl && clientSecret) {
163
+ websocketUrl = buildUrl(this.client.baseUrl.replace(/^http/i, "ws"), `/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream-input`, { client_secret: clientSecret });
164
+ }
165
+ let protocols;
166
+ let requiredSubprotocol;
167
+ if (directWebsocketUrl) {
168
+ if (!clientSecret) {
169
+ throw new BreezeBlueConfigurationError("realtime directWebsocketUrl requires a clientSecret.");
170
+ }
171
+ protocols = realtimeClientSecretSubprotocols(clientSecret);
172
+ requiredSubprotocol = REALTIME_SAFE_SUBPROTOCOL;
173
+ websocketUrl = directWebsocketUrl;
174
+ }
175
+ if (!websocketUrl) {
176
+ throw new BreezeBlueConfigurationError("realtime.connect requires session credentials or an API-created session.");
177
+ }
152
178
  const WebSocketImpl = requireWebSocket(options.webSocket ?? this.client.webSocket);
153
- const socket = new WebSocketImpl(websocketUrl);
179
+ const socket = new WebSocketImpl(websocketUrl, protocols);
154
180
  return RealtimeTextToSpeechConnection.open(socket, {
155
181
  timeout: options.timeout ?? this.client.timeout ?? DEFAULT_REALTIME_HANDSHAKE_TIMEOUT_MS,
156
182
  signal: options.signal,
183
+ requiredSubprotocol,
157
184
  });
158
185
  }
159
186
  /**
@@ -191,6 +218,7 @@ class RealtimeTextToSpeechResource {
191
218
  return this.connect(voiceId, {
192
219
  clientSecret: credentials.clientSecret,
193
220
  websocketUrl: credentials.websocketUrl,
221
+ directWebsocketUrl: credentials.directWebsocketUrl,
194
222
  webSocket: options.webSocket,
195
223
  signal,
196
224
  timeout: connectionTimeout,
@@ -212,6 +240,16 @@ function hasRealtimeSessionParams(options) {
212
240
  options.inactivityTimeoutSeconds !== undefined ||
213
241
  options.enableLogging !== undefined);
214
242
  }
243
+ function realtimeClientSecretSubprotocols(clientSecret) {
244
+ if (!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{43}$/.test(clientSecret)) {
245
+ throw new BreezeBlueConfigurationError("Realtime clientSecret cannot be encoded as a WebSocket subprotocol token.");
246
+ }
247
+ const tokenProtocol = `${REALTIME_TOKEN_SUBPROTOCOL_PREFIX}${clientSecret}`;
248
+ if (tokenProtocol.length > REALTIME_MAX_TOKEN_SUBPROTOCOL_LENGTH) {
249
+ throw new BreezeBlueConfigurationError("Realtime clientSecret WebSocket subprotocol is too long.");
250
+ }
251
+ return [REALTIME_SAFE_SUBPROTOCOL, tokenProtocol];
252
+ }
215
253
  export class RealtimeTextToSpeechConnection {
216
254
  socket;
217
255
  queue = [];
@@ -240,6 +278,11 @@ export class RealtimeTextToSpeechConnection {
240
278
  const succeed = () => {
241
279
  if (settled)
242
280
  return;
281
+ if (options.requiredSubprotocol !== undefined &&
282
+ socket.protocol !== options.requiredSubprotocol) {
283
+ fail(new BreezeBlueRealtimeError(`Realtime TTS WebSocket did not negotiate ${options.requiredSubprotocol}.`, { code: "GENERATION_INVALID_RESPONSE" }));
284
+ return;
285
+ }
243
286
  settled = true;
244
287
  cleanup();
245
288
  resolve();
package/dist/types.d.ts CHANGED
@@ -28,6 +28,7 @@ export type BreezeBlueWebSocketConstructor = new (url: string, protocols?: strin
28
28
  export interface BreezeBlueWebSocket {
29
29
  binaryType: string;
30
30
  readyState: number;
31
+ readonly protocol?: string;
31
32
  onopen: ((event: Event) => void) | null;
32
33
  onmessage: ((event: MessageEvent) => void) | null;
33
34
  onerror: ((event: Event) => void) | null;
@@ -54,12 +55,14 @@ export interface RealtimeTextToSpeechAudioFormat {
54
55
  export interface RealtimeTextToSpeechSession {
55
56
  clientSecret: string;
56
57
  websocketUrl: string;
58
+ directWebsocketUrl?: string;
57
59
  expiresAt: string;
58
60
  audioFormat: RealtimeTextToSpeechAudioFormat;
59
61
  }
60
62
  export interface RealtimeTextToSpeechConnectOptions extends RealtimeTextToSpeechSessionRequest {
61
63
  clientSecret?: string;
62
64
  websocketUrl?: string;
65
+ directWebsocketUrl?: string;
63
66
  webSocket?: BreezeBlueWebSocketConstructor;
64
67
  signal?: AbortSignal;
65
68
  /** Handshake timeout in milliseconds. Defaults to the client `timeout`. */
@@ -68,9 +71,11 @@ export interface RealtimeTextToSpeechConnectOptions extends RealtimeTextToSpeech
68
71
  export type RealtimeTextToSpeechSessionCredentials = {
69
72
  clientSecret: string;
70
73
  websocketUrl?: string;
74
+ directWebsocketUrl?: string;
71
75
  } | {
72
76
  clientSecret?: string;
73
77
  websocketUrl: string;
78
+ directWebsocketUrl?: string;
74
79
  };
75
80
  export interface RealtimeTextToSpeechSessionFactoryContext {
76
81
  /**
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "0.7.0";
1
+ export declare const SDK_VERSION = "0.8.0";
2
2
  export declare const SDK_NAME = "@breeze.blue/sdk";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const SDK_VERSION = "0.7.0";
1
+ export const SDK_VERSION = "0.8.0";
2
2
  export const SDK_NAME = "@breeze.blue/sdk";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@breeze.blue/sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "ESM-first TypeScript SDK for the Breeze Blue Developer API.",
5
5
  "license": "MIT",
6
6
  "author": "Breeze Blue <support@breeze.blue>",