@juspay/neurolink 12.6.1 → 12.7.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.
@@ -16,6 +16,46 @@
16
16
  * client is then still attributable by its own User-Agent rather than
17
17
  * collapsing into one bucket with every other unknown.
18
18
  */
19
+ /**
20
+ * Deliberately NOT mapped, with the measurement that ruled each one out.
21
+ *
22
+ * Copilot CLI is the one client here that cannot be identified from its
23
+ * User-Agent, and both of the strings it sends are actively unsafe to key on:
24
+ *
25
+ * - `OpenAI/JS 5.20.1` — the stock OpenAI JS SDK UA, sent by every caller of
26
+ * that SDK. Mapping it to Copilot would file unrelated OpenAI-SDK traffic
27
+ * under Copilot's name, which is worse than leaving it unattributed.
28
+ - `Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)` — not a
29
+ * CLI's User-Agent at all. This is what `curl` sends on a machine whose
30
+ * `~/.curlrc` sets `user-agent`, so every curl-driven caller on such a host
31
+ * shares it: scripts, agents, health probes. It accounted for the largest
32
+ * single block of `unknown` rows in the log this table was measured against,
33
+ * which is exactly what made it tempting.
34
+ *
35
+ * Recorded because the first pass got this wrong in a way worth naming. The
36
+ * string was seen arriving at two capture servers during Copilot CLI and
37
+ * Gemini CLI runs and was written up as "sent by both CLIs". It was neither:
38
+ * it was the aliveness `curl` fired at each capture server moments before
39
+ * the CLI, picking up that host's curlrc. The paths gave it away on review —
40
+ * the Gemini-side hit was a bare `GET /v1beta/models`, which is the probe's
41
+ * URL and not one the CLI requests. A shared string across two unrelated
42
+ * clients should have read as "shared dependency or shared tooling", not as
43
+ * a property of either client.
44
+ *
45
+ * Copilot does send `x-initiator` and `x-interaction-type`, but neither is
46
+ * exclusive to it either. Attributing it needs a signal nobody has found yet,
47
+ * so it stays `unknown` and remains traceable through the stored raw header.
48
+ */
49
+ /**
50
+ * The client names this table can produce.
51
+ *
52
+ * Exported so a test can check the roster of CLIs the proxy configures against
53
+ * the roster it can actually name. Those two lists drifted apart silently once
54
+ * already: five configurators shipped while the table still knew only Claude
55
+ * Code, and nothing failed, because an unattributed client looks exactly like
56
+ * a quiet one.
57
+ */
58
+ export declare function getMappedClientNames(): ReadonlySet<string>;
19
59
  /**
20
60
  * Derive a stable client name, or "unknown" when the header is absent or
21
61
  * unrecognised. Never throws: attribution must not be able to fail a request.
@@ -49,16 +49,39 @@ const CLIENT_PREFIXES = [
49
49
  * - `OpenAI/JS 5.20.1` — the stock OpenAI JS SDK UA, sent by every caller of
50
50
  * that SDK. Mapping it to Copilot would file unrelated OpenAI-SDK traffic
51
51
  * under Copilot's name, which is worse than leaving it unattributed.
52
- * - `Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)` — a
53
- * spoofed browser string. It looks like a fingerprint and is not one: it was
54
- * captured from **both** Copilot CLI and Gemini CLI, so it identifies no
55
- * client at all. (It also accounts for the largest single block of
56
- * `unknown` rows in this machine's log, which is what made it tempting.)
52
+ - `Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)` — not a
53
+ * CLI's User-Agent at all. This is what `curl` sends on a machine whose
54
+ * `~/.curlrc` sets `user-agent`, so every curl-driven caller on such a host
55
+ * shares it: scripts, agents, health probes. It accounted for the largest
56
+ * single block of `unknown` rows in the log this table was measured against,
57
+ * which is exactly what made it tempting.
58
+ *
59
+ * Recorded because the first pass got this wrong in a way worth naming. The
60
+ * string was seen arriving at two capture servers during Copilot CLI and
61
+ * Gemini CLI runs and was written up as "sent by both CLIs". It was neither:
62
+ * it was the aliveness `curl` fired at each capture server moments before
63
+ * the CLI, picking up that host's curlrc. The paths gave it away on review —
64
+ * the Gemini-side hit was a bare `GET /v1beta/models`, which is the probe's
65
+ * URL and not one the CLI requests. A shared string across two unrelated
66
+ * clients should have read as "shared dependency or shared tooling", not as
67
+ * a property of either client.
57
68
  *
58
69
  * Copilot does send `x-initiator` and `x-interaction-type`, but neither is
59
70
  * exclusive to it either. Attributing it needs a signal nobody has found yet,
60
71
  * so it stays `unknown` and remains traceable through the stored raw header.
61
72
  */
73
+ /**
74
+ * The client names this table can produce.
75
+ *
76
+ * Exported so a test can check the roster of CLIs the proxy configures against
77
+ * the roster it can actually name. Those two lists drifted apart silently once
78
+ * already: five configurators shipped while the table still knew only Claude
79
+ * Code, and nothing failed, because an unattributed client looks exactly like
80
+ * a quiet one.
81
+ */
82
+ export function getMappedClientNames() {
83
+ return new Set(CLIENT_PREFIXES.map(([, name]) => name));
84
+ }
62
85
  /** Cap stored User-Agents. They are attacker-influenced and unbounded. */
63
86
  const MAX_USER_AGENT_CHARS = 200;
64
87
  /**
@@ -352,6 +352,93 @@ export type TTSHandler = {
352
352
  * @throws {TTSError} On synthesis failure, timeout, or configuration issues
353
353
  */
354
354
  synthesize(text: string, options: TTSOptions): Promise<TTSResult>;
355
+ /**
356
+ * Stream provider-native audio for one pre-validated text segment.
357
+ *
358
+ * Return `undefined` when the requested options cannot be delivered
359
+ * incrementally. The processor then uses `synthesize()` and preserves the
360
+ * buffered fallback for handlers and formats without native support.
361
+ * Provider-local indexes, cumulative sizes, and finality are normalized by
362
+ * the processor before chunks reach the public stream — an implementation
363
+ * may leave `isFinal` false on every chunk rather than hold a fragment back
364
+ * to label the last one, and the processor discards reported finality
365
+ * either way.
366
+ *
367
+ * Yield `TTSChunk` fragments. The member is declared `unknown` — not a
368
+ * method signature — on purpose, and that is a deliberate trade.
369
+ *
370
+ * This is an OPTIONAL member added to a public structural type that
371
+ * consumers already implement. Any type narrower than `unknown` rejects some
372
+ * existing handler that already carries a member of this name, which is a
373
+ * source break under Critical Rule 5 whatever that other shape happens to
374
+ * be. That is not hypothetical: a member returning a sync `Generator`, an
375
+ * `async` method returning a `Promise` of an async iterable, a
376
+ * callback-style member returning `void` or `Promise<void>`, and a plain
377
+ * boolean capability flag all compile against `origin/release` today, and
378
+ * every one of them is rejected by a declared method signature — including
379
+ * an intentionally wide one such as `(...args: never[]) => unknown`, which
380
+ * still cannot accept the boolean. Only `unknown` accepts them all.
381
+ *
382
+ * The cost is that this member cannot contextually type an implementation's
383
+ * parameters. Authors annotate their own signature instead — OpenAI TTS
384
+ * declares `synthesizeStream(text: string, options: TTSOptions):
385
+ * AsyncIterable<TTSChunk> | undefined` on the class — which keeps full
386
+ * compiler checking of what that implementation yields. Nothing is checked
387
+ * at this member; usability is decided at runtime by the processor.
388
+ *
389
+ * The processor validates each fragment at runtime instead. A fragment is
390
+ * audio only when it carries a non-empty `data` `Buffer` (or `Uint8Array`);
391
+ * its `format` is honoured only when it names a real `TTSAudioFormat`, and
392
+ * the requested format is used otherwise. A fragment that fails that test —
393
+ * including an empty (`data.length === 0`) read — is skipped and never
394
+ * reaches the consumer. A native stream that completes without yielding a
395
+ * single deliverable fragment is treated as "no incremental delivery after
396
+ * all" and falls back to `synthesize()` for that segment.
397
+ *
398
+ * That filtering is specific to this native path. The buffered path
399
+ * forwards whatever `synthesize()` returns, a zero-byte buffer included, so
400
+ * a consumer of the public stream can still observe an empty chunk when a
401
+ * handler produces one.
402
+ *
403
+ * `undefined` is the only capability signal. Everything the processor does
404
+ * to decide whether this capability exists is asked BEFORE the segment's
405
+ * work starts, and every way that question can fail is a handler bug that
406
+ * the processor answers the same way: it serves that segment from
407
+ * `synthesize()` rather than losing it (the throwing modes also log a
408
+ * warning; a member that is merely not callable falls back silently). None
409
+ * of them should be used as a deliberate fallback mechanism. The modes it
410
+ * anticipates cover each read as well as each call, because reading a
411
+ * property can run a getter or a `Proxy` trap that throws just as a call
412
+ * can:
413
+ *
414
+ * - reading `synthesizeStream` off the handler throws;
415
+ * - the member is present but not callable;
416
+ * - reading `isConfigured` off the handler throws, or calling it throws —
417
+ * the segment falls back to the buffered path, where `synthesize()` asks
418
+ * again and a throw there fails the segment shaped, exactly as it would
419
+ * for a handler with no native member (a handler that merely reports
420
+ * itself unconfigured is not a bug: that segment fails with
421
+ * `TTS_PROVIDER_NOT_CONFIGURED`, as it always has);
422
+ * - calling the member throws;
423
+ * - the returned value's async-iterability cannot be established, including
424
+ * a value whose `Symbol.asyncIterator` property cannot even be read.
425
+ *
426
+ * An error raised once the segment's own work has started, by contrast,
427
+ * fails that segment like any other synthesis failure — it is not re-served
428
+ * from the buffered path. Iteration begins at the `[Symbol.asyncIterator]()`
429
+ * call, so that call throwing, that call handing back something that is not
430
+ * an iterator, and a first read that rejects are all segment failures rather
431
+ * than fallbacks.
432
+ *
433
+ * Implementations MUST enforce their own timeout and cancel any active
434
+ * transport when the returned iterable is closed.
435
+ *
436
+ * The value the processor looks for is a callable of the shape
437
+ * `(text: string, options: TTSOptions) => AsyncIterable<TTSChunk> | undefined`,
438
+ * invoked with the handler as `this`. `text` is one buffered text segment
439
+ * within the provider's length limit.
440
+ */
441
+ synthesizeStream?: unknown;
355
442
  /**
356
443
  * Get available voices for the provider
357
444
  *
@@ -134,6 +134,14 @@ export type VoiceTurn = {
134
134
  };
135
135
  /**
136
136
  * TTS-capable voice provider type
137
+ *
138
+ * @deprecated Use the canonical `TTSHandler` contract instead. Nothing in
139
+ * this package consumes `TTSProvider`; it is kept at its original shape so
140
+ * existing external callers keep compiling. `TTSHandler` is not a drop-in
141
+ * replacement — it requires `isConfigured()`, makes `getVoices` and
142
+ * `maxTextLength` optional, and its `synthesizeStream` may return `undefined`
143
+ * to select the buffered path — so this is a distinct legacy shape, not an
144
+ * alias.
137
145
  */
138
146
  export type TTSProvider = {
139
147
  /**
@@ -155,6 +163,11 @@ export type TTSProvider = {
155
163
  };
156
164
  /**
157
165
  * TTS stream chunk for streaming synthesis
166
+ *
167
+ * @deprecated Use the canonical `TTSChunk` type instead. Kept at its
168
+ * original shape so existing external callers keep compiling: `TTSChunk`
169
+ * narrows `format` to `TTSAudioFormat` and has no `timestampMs`, so it
170
+ * is not a drop-in replacement.
158
171
  */
159
172
  export type TTSStreamChunk = {
160
173
  /** Audio data chunk */
@@ -162,17 +162,142 @@ export declare class TTSProcessor {
162
162
  * ```
163
163
  */
164
164
  static synthesize(text: string, provider: string, options: TTSOptions): Promise<TTSResult>;
165
+ /**
166
+ * Normalize a provider failure into the public `TTSError` shape.
167
+ *
168
+ * Extracted from `synthesize()` so the native streaming path can shape its
169
+ * transport errors identically. A raw provider error reaching the public
170
+ * surface unwrapped loses `retriable` (and the provider-qualified message),
171
+ * which is exactly what a caller keying retry logic off
172
+ * `ttsMetadata.error.retriable` reads.
173
+ */
174
+ private static toSynthesisError;
175
+ /**
176
+ * Open the `tts.synthesize` span that `synthesize()` opens, so a segment
177
+ * served by a handler's native stream emits the same telemetry a buffered
178
+ * segment does.
179
+ */
180
+ private static startSynthesisSpan;
181
+ private static finishSynthesisSpan;
182
+ /**
183
+ * `TTSHandler.synthesizeStream` is declared `unknown`, so a handler may
184
+ * carry anything at all under that name — including a legacy member of an
185
+ * unrelated shape. The native capability is discovered structurally instead:
186
+ * the member must be callable, and what it returns must be async-iterable.
187
+ *
188
+ * Discovery reads consumer-controlled properties, and a property read can
189
+ * itself execute user code (a Proxy trap, a throwing getter) — the member on
190
+ * the handler and the well-known symbol on what it returns alike. Every one
191
+ * of those reads therefore happens inside a try: `resolveNativeStream`
192
+ * guards the whole preflight in one region, and `cancelStream`/
193
+ * `releaseIterator` in `streamCancellation.ts` guard the symbol read they
194
+ * each perform on the unwind path.
195
+ */
196
+ private static isCallableMember;
197
+ private static isNativeStream;
198
+ /**
199
+ * Coerce one fragment yielded by a handler's native stream into the fields
200
+ * this processor forwards, or `undefined` when the fragment is not audio.
201
+ *
202
+ * `TTSHandler.synthesizeStream` is declared `unknown` because ANY narrower
203
+ * type rejects some existing consumer that already carries a member of that
204
+ * name — a Critical Rule 5 break. The type therefore checks nothing at all,
205
+ * and every check happens at runtime instead. Here that means: a
206
+ * fragment must carry a non-empty binary payload, and a reported `format` is
207
+ * honoured only when it names a real audio format, falling back to the
208
+ * requested one. The caller skips an `undefined` result exactly as it skips
209
+ * a zero-length transport read.
210
+ */
211
+ private static normalizeNativeChunk;
212
+ /**
213
+ * Ask a handler for a native stream for one segment, or `undefined` to serve
214
+ * the segment from `synthesize()`.
215
+ *
216
+ * `undefined` is the documented "not incrementally deliverable" signal. A
217
+ * handler that throws instead, or hands back something that is not
218
+ * async-iterable, is buggy — but that is a reason to serve the segment from
219
+ * the buffered path, not to lose it.
220
+ *
221
+ * **Every failure this method anticipates answers `undefined` instead of
222
+ * throwing, and the caller's catch backstops what no code that must
223
+ * describe a hostile thrown value can rule out.** Everything it does is a question ABOUT the
224
+ * handler, asked before any of the segment's work has begun, and every one
225
+ * of those questions can run consumer code: reading `synthesizeStream` or
226
+ * `isConfigured` can hit an accessor or a `Proxy` trap, calling
227
+ * `isConfigured()` and calling the member run handler code outright, and
228
+ * reading `Symbol.asyncIterator` off whatever comes back can hit a getter or
229
+ * a trap of its own. A throw from ANY of them means the same thing — the
230
+ * capability could not be established — so they all sit inside the single
231
+ * guarded region below and every failure answers `undefined`. The segment
232
+ * then goes to `synthesize()`, which re-runs the identical preflight inside
233
+ * its own `tts.synthesize` span and normalizes whatever it raises, exactly
234
+ * as it did before native streaming existed. Reporting a preflight failure
235
+ * from here instead would open a second span for one segment, or drop a
236
+ * segment the buffered path can still serve.
237
+ *
238
+ * Two ordering rules make that equivalence exact:
239
+ *
240
+ * - The member is read FIRST and exactly ONCE per segment. An accessor-
241
+ * defined member runs consumer code on every read, so a second read can
242
+ * answer differently from the one that was tested — and reading it before
243
+ * `isConfigured()` keeps a handler that does not offer the capability at
244
+ * all on precisely the call sequence it had before this method existed.
245
+ * - `isConfigured()` runs only once a callable member has been found, which
246
+ * is the only case where this method is about to invoke the handler.
247
+ *
248
+ * The one measured departure from `origin/release` is a call count, not an
249
+ * outcome: a segment that starts down the native path and falls back calls
250
+ * the handler's `isConfigured()` twice — here, and again inside
251
+ * `synthesize()`. That is inherent to attempting native delivery at all and
252
+ * predates this method's current shape; `isConfigured()` is specified as a
253
+ * configuration predicate, and for any implementation that behaves as one —
254
+ * a pure predicate — every observable of such a segment (chunks, spans,
255
+ * error code and `retriable`) is identical either way. A handler whose
256
+ * answer CHANGES between the two calls changes the outcome with it: the
257
+ * second answer, taken on the path that actually synthesizes, decides.
258
+ */
259
+ private static resolveNativeStream;
165
260
  /**
166
261
  * Incrementally synthesize sentence-buffered text chunks.
167
262
  *
168
263
  * Text is flushed at a sentence boundary after `streamingBufferSize`
169
264
  * characters, or hard-split before the provider's maximum text length.
170
- * Each segment goes through `synthesize()`, preserving the existing handler
171
- * registry, validation, error normalization, and telemetry seam.
172
265
  *
173
- * The most recent successful audio chunk is held until another succeeds or
174
- * the input ends, so exactly one real audio chunk carries `isFinal: true`
175
- * without emitting a separate empty terminator chunk.
266
+ * A segment is served by the handler's `synthesizeStream()` when it offers
267
+ * one and returns a stream, and by `synthesize()` otherwise including
268
+ * when the native stream produces no deliverable audio at all, and including
269
+ * every way capability discovery itself can fail. The preflight reads and
270
+ * calls that decide whether a native stream exists all sit inside one
271
+ * guarded region in `resolveNativeStream`, with the call site's own catch
272
+ * backstopping it, so a handler that misbehaves while being ASKED lands on
273
+ * the buffered path rather than costing the segment. That is what makes the
274
+ * next sentence true of every segment rather than only of the ones that got
275
+ * that far.
276
+ *
277
+ * Either way the segment keeps the same handler registry, validation, error
278
+ * normalization and `tts.synthesize` telemetry seam — exactly one span per
279
+ * segment, opened by whichever path served it — cancellation included: a
280
+ * segment whose stream is still in flight when the consumer stops records
281
+ * its span from the unwind path rather than dropping it. Failures that
282
+ * originate in the native segment's own work, once a stream has been
283
+ * established, are a different case and keep the shaped failed-segment
284
+ * semantics every synthesis failure has.
285
+ *
286
+ * Provider-reported indexes, cumulative sizes and finality are discarded and
287
+ * recomputed globally. A native fragment is dropped unless it carries a
288
+ * non-empty binary payload, so no native read reaches the consumer as an
289
+ * empty chunk; the buffered path is unfiltered and forwards whatever
290
+ * `synthesize()` returns, so a handler that produces a zero-byte buffer
291
+ * still yields an empty chunk and a repeated `cumulativeSize`. The most
292
+ * recent successful audio chunk is held until another succeeds or the input
293
+ * ends, so exactly one real audio chunk carries `isFinal: true` without
294
+ * emitting a separate empty terminator chunk.
295
+ *
296
+ * Segment production and per-segment synthesis are deliberately inline
297
+ * rather than nested async generators: each additional generator layer costs
298
+ * every chunk several microtask turns, which is directly observable at
299
+ * `NeuroLink.stream()` as audio interleaving one text chunk later than it
300
+ * does without native streaming.
176
301
  */
177
302
  static synthesizeStream(textChunks: AsyncIterable<string>, provider: string, options: TTSOptions, shouldStop?: () => boolean): AsyncGenerator<TTSChunk>;
178
303
  }