@byokit/realtime 0.3.0 → 0.3.2

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,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.2 (2026-10-17)
6
+
7
+ - Dependency update: ws 8.22.0.
8
+
9
+ ## 0.3.1 (2026-10-01)
10
+
11
+
12
+
13
+ - SECURITY: Lazy WebRTC capture no longer opens the Android microphone or sends untapped speech before `attachMic()`; `releaseMic()` closes recording while keeping the call connected. Apps using lazy capture should update.
14
+
5
15
  ## 0.3.0 (2026-10-01)
6
16
 
7
17
 
package/README.md CHANGED
@@ -194,8 +194,10 @@ function endCall() { client.stop(); }
194
194
  ```
195
195
 
196
196
  `attachMic()` awaits the app's microphone permission/service lease, captures
197
- an audio track and uses `sender.replaceTrack(track)`. `releaseMic()` awaits
198
- `replaceTrack(null)`, stops capture tracks and releases the lease so the app
197
+ an audio track and uses `sender.replaceTrack(track)` before enabling the sender's
198
+ encoding. Lazy peers negotiate `sendrecv` with an inactive encoding: Android's
199
+ audio device cannot start recording just because an answer is applied.
200
+ `releaseMic()` disables that encoding before `replaceTrack(null)`, stops capture tracks and releases the lease so the app
199
201
  can stop its microphone service and clear the OS indicator. Neither operation
200
202
  renegotiates. Repeated calls are safe; release or stop during pending capture
201
203
  cleans up when that capture resolves. Reconnecting starts another lazy peer;
@@ -256,4 +258,4 @@ Credentials, endpoint restrictions and redirect rejection are unchanged.
256
258
  The identity option allows a consumer to compare signaling latency; offline
257
259
  tests prove startup ordering, not a reduction in the remote provider's POST time.
258
260
 
259
- For an explicitly live microphone and subscription-provider proof, run [the bounded voice demo](../../examples/realtime-voice/README.md). It signs in through accounts, keeps credentials only in memory, and records connection facts and turn counts. Offline tests do not establish a live pass.
261
+ For an explicitly live microphone and subscription-provider proof, run [the bounded voice demo](../../examples/realtime-voice/README.md). It signs in through accounts, keeping the sign-in at this computer's machine store, and records connection facts and turn counts. Offline tests do not establish a live pass.
package/dist/chatgpt.js CHANGED
@@ -1,5 +1,5 @@
1
- import { cleanProse, cleanProseBytes } from "./prose.js";
2
- import { MAX_REALTIME_SDP_BYTES } from "./frames.js";
1
+ import { cleanProse, cleanProseBytes } from './prose.js';
2
+ import { MAX_REALTIME_SDP_BYTES } from './frames.js';
3
3
  export const CHATGPT_SIGNALING_URL = 'https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas';
4
4
  function emit(frame) { process.stdout.write(`${JSON.stringify(frame)}\n`); }
5
5
  export async function boundedBody(response, max = MAX_REALTIME_SDP_BYTES) {
package/dist/child.js CHANGED
@@ -8,8 +8,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import { createInterface } from 'node:readline';
10
10
  import { randomUUID } from 'node:crypto';
11
- import { parseRealtimeClientFrame } from "./frames.js";
12
- import { createChatgptAdapter } from "./chatgpt.js";
11
+ import { parseRealtimeClientFrame } from './frames.js';
12
+ import { createChatgptAdapter } from './chatgpt.js';
13
13
  const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
14
14
  let adapter;
15
15
  const pending = new Map();
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { parseRealtimeClientFrame, parseRealtimeHostFrame, realtimePcm16ByteLength } from "./frames.js";
1
+ import { parseRealtimeClientFrame, parseRealtimeHostFrame, realtimePcm16ByteLength } from './frames.js';
2
2
  /** Provider-blind client with bounded reconnects and exclusive media ownership. */
3
3
  export function realtimeClient(options) {
4
4
  const audio = options.audio;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- export * from "./frames.js";
2
- export * from "./types.js";
3
- export * from "./words.js";
4
- export * from "./prose.js";
5
- export * from "./client.js";
6
- export * from "./auth.js";
7
- export { providers } from "./catalogue.js";
1
+ export * from './frames.js';
2
+ export * from './types.js';
3
+ export * from './words.js';
4
+ export * from './prose.js';
5
+ export * from './client.js';
6
+ export * from './auth.js';
7
+ export { providers } from './catalogue.js';
package/dist/node.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { parseRealtimeClientFrame, parseRealtimeHostFrame } from "./frames.js";
5
- import { RealtimeError } from "./types.js";
6
- import { cleanProseBytes } from "./prose.js";
7
- import { realtimeAuthCheck } from "./auth.js";
8
- export { toolBridge, appBridge, delegationHandler } from "./tools.js";
9
- export { realtimeAuthCheck } from "./auth.js";
4
+ import { parseRealtimeClientFrame, parseRealtimeHostFrame } from './frames.js';
5
+ import { RealtimeError } from './types.js';
6
+ import { cleanProseBytes } from './prose.js';
7
+ import { realtimeAuthCheck } from './auth.js';
8
+ export { toolBridge, appBridge, delegationHandler } from './tools.js';
9
+ export { realtimeAuthCheck } from './auth.js';
10
10
  /** Provider sockets and SDP signaling run in a kit-owned child, never in the app process. */
11
11
  export function realtimeEngine(options) {
12
12
  if (!['chatgpt', 'openai', 'gemini', 'xai'].includes(options.engine))
@@ -1,6 +1,6 @@
1
1
  import WebSocket from 'ws';
2
- import { responseFence } from "../policy.js";
3
- import { cleanProse, providerRefusal as refusal } from "../prose.js";
2
+ import { responseFence } from '../policy.js';
3
+ import { cleanProse, providerRefusal as refusal } from '../prose.js';
4
4
  // Lifted wire behavior; this module is loaded only by the kit child process.
5
5
  export function createAdapter(options) {
6
6
  const MODEL = options.model ?? 'gemini-3.8-live';
@@ -1,6 +1,6 @@
1
1
  import WebSocket from 'ws';
2
- import { responseFence } from "../policy.js";
3
- import { cleanProse, providerRefusal as refusal } from "../prose.js";
2
+ import { responseFence } from '../policy.js';
3
+ import { cleanProse, providerRefusal as refusal } from '../prose.js';
4
4
  // Lifted wire behavior; this module is loaded only by the kit child process.
5
5
  export function createAdapter(options) {
6
6
  const MODEL = options.model ?? 'gpt-realtime-2.1';
@@ -1,6 +1,6 @@
1
1
  import WebSocket from 'ws';
2
- import { responseFence } from "../policy.js";
3
- import { cleanProse, providerRefusal as refusal } from "../prose.js";
2
+ import { responseFence } from '../policy.js';
3
+ import { cleanProse, providerRefusal as refusal } from '../prose.js';
4
4
  // Lifted wire behavior; this module is loaded only by the kit child process.
5
5
  export function createAdapter(options) {
6
6
  const MODEL = options.model ?? 'grok-voice-think-fast-2.0';
package/dist/rn-webrtc.js CHANGED
@@ -1,4 +1,4 @@
1
- import { webRtcPeer as createPeer } from "./webrtc.js";
1
+ import { webRtcPeer as createPeer } from './webrtc.js';
2
2
  /** Native media stays unloaded until the app actually starts a voice session. */
3
3
  export async function webRtcPeer(options) {
4
4
  if (options.platform)
package/dist/tools.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { cleanProseBytes } from "./prose.js";
2
+ import { cleanProseBytes } from './prose.js';
3
3
  /** Bounded, deduplicated tool execution. Product authorization stays in handlers. */
4
4
  export function toolBridge(options) {
5
5
  const lifetime = new AbortController();
package/dist/webrtc.js CHANGED
@@ -1,4 +1,4 @@
1
- import { MAX_REALTIME_SDP_BYTES, MAX_REALTIME_WEBRTC_DATA_BYTES } from "./frames.js";
1
+ import { MAX_REALTIME_SDP_BYTES, MAX_REALTIME_WEBRTC_DATA_BYTES } from './frames.js';
2
2
  function sdp(value) {
3
3
  if (typeof value !== 'string' || !value.startsWith('v=0') || value.includes('\0') || new TextEncoder().encode(value).length > MAX_REALTIME_SDP_BYTES)
4
4
  throw new Error('Invalid voice connection answer.');
@@ -47,6 +47,19 @@ export async function webRtcPeer(options) {
47
47
  options.onError(error);
48
48
  stop();
49
49
  } };
50
+ const setSending = async (active) => {
51
+ if (options.capture !== 'lazy')
52
+ return;
53
+ const parameters = sender.getParameters();
54
+ if (!parameters.encodings.length)
55
+ throw new Error('Voice capture control is unavailable.');
56
+ parameters.encodings.forEach(encoding => { encoding.active = active; });
57
+ await sender.setParameters(parameters);
58
+ // The native bridge can resolve even when the engine rejects setParameters.
59
+ const applied = sender.getParameters().encodings;
60
+ if (!applied.length || applied.some(encoding => encoding.active !== active))
61
+ throw new Error('Voice capture control failed.');
62
+ };
50
63
  const captureMic = async (generation) => {
51
64
  await options.audio.microphone.acquire();
52
65
  acquired = true;
@@ -79,14 +92,28 @@ export async function webRtcPeer(options) {
79
92
  const captured = await captureMic(generation);
80
93
  if (captured) {
81
94
  await sender.replaceTrack(captured.getAudioTracks()[0]);
95
+ if (!stopped && generation === micEpoch)
96
+ await setSending(true);
82
97
  if (stopped || generation !== micEpoch) {
83
- if (!stopped)
98
+ if (!stopped) {
99
+ await setSending(false);
84
100
  await sender.replaceTrack(null);
101
+ }
85
102
  clearMic();
86
103
  }
87
104
  }
88
105
  }
89
106
  catch (error) {
107
+ // Never leave a trackless native send stream recording after a failure.
108
+ try {
109
+ if (!stopped && options.capture === 'lazy') {
110
+ await setSending(false);
111
+ await sender.replaceTrack(null);
112
+ }
113
+ }
114
+ catch {
115
+ stop();
116
+ }
90
117
  clearMic();
91
118
  throw error;
92
119
  }
@@ -97,8 +124,15 @@ export async function webRtcPeer(options) {
97
124
  stream?.getAudioTracks().forEach(track => { track.enabled = false; });
98
125
  return serializeMic(async () => {
99
126
  try {
100
- if (!stopped && stream)
127
+ if (!stopped && stream) {
128
+ await setSending(false);
101
129
  await sender.replaceTrack(null);
130
+ }
131
+ }
132
+ catch (error) {
133
+ if (options.capture === 'lazy')
134
+ stop();
135
+ throw error;
102
136
  }
103
137
  finally {
104
138
  clearMic();
@@ -153,8 +187,12 @@ export async function webRtcPeer(options) {
153
187
  options.onConnectionState('connecting'); };
154
188
  current.ontrack = event => { const track = event.track; if (track.kind !== 'audio' || stopped)
155
189
  return; options.onRemoteAudio(!track.muted); track.onmute = () => options.onRemoteAudio(false); track.onunmute = () => options.onRemoteAudio(true); track.onended = () => options.onRemoteAudio(false); };
156
- if (options.capture === 'lazy')
157
- sender = current.addTransceiver('audio', { direction: 'sendrecv' }).sender;
190
+ if (options.capture === 'lazy') {
191
+ // Android starts the device module for an active send stream even without
192
+ // a track. Disable the encoding before either description can be applied.
193
+ sender = current.addTransceiver('audio', { direction: 'sendrecv', sendEncodings: [{ active: false }] }).sender;
194
+ await setSending(false);
195
+ }
158
196
  else {
159
197
  const input = stream.getAudioTracks()[0];
160
198
  if (!input)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/realtime",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Realtime voice sessions with host-held credentials and injected audio.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "ws": "8.21.3"
46
+ "ws": "8.22.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react-native-webrtc": "124.0.8"