@juspay/neurolink 11.29.2 → 12.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +54 -2
  2. package/dist/auth/anthropicOAuth.d.ts +50 -0
  3. package/dist/auth/anthropicOAuth.js +78 -0
  4. package/dist/browser/neurolink.min.js +393 -393
  5. package/dist/cli/commands/proxy.d.ts +2 -0
  6. package/dist/cli/commands/proxy.js +284 -4
  7. package/dist/cli/commands/proxyExpose.d.ts +35 -0
  8. package/dist/cli/commands/proxyExpose.js +252 -0
  9. package/dist/cli/commands/proxyPeer.d.ts +29 -0
  10. package/dist/cli/commands/proxyPeer.js +738 -0
  11. package/dist/cli/commands/proxyShare.d.ts +37 -0
  12. package/dist/cli/commands/proxyShare.js +1080 -0
  13. package/dist/cli/parser.js +7 -1
  14. package/dist/core/baseProvider.js +20 -1
  15. package/dist/neurolink.js +30 -2
  16. package/dist/proxy/peerStore.d.ts +52 -0
  17. package/dist/proxy/peerStore.js +324 -0
  18. package/dist/proxy/peerTransport.d.ts +38 -0
  19. package/dist/proxy/peerTransport.js +242 -0
  20. package/dist/proxy/proxyPaths.d.ts +8 -0
  21. package/dist/proxy/proxyPaths.js +55 -17
  22. package/dist/proxy/requestLogger.js +8 -0
  23. package/dist/proxy/residentGrants.d.ts +57 -0
  24. package/dist/proxy/residentGrants.js +393 -0
  25. package/dist/proxy/shareAudit.d.ts +81 -0
  26. package/dist/proxy/shareAudit.js +280 -0
  27. package/dist/proxy/shareContext.d.ts +38 -0
  28. package/dist/proxy/shareContext.js +92 -0
  29. package/dist/proxy/shareGate.d.ts +64 -0
  30. package/dist/proxy/shareGate.js +216 -0
  31. package/dist/proxy/shareGrants.d.ts +115 -0
  32. package/dist/proxy/shareGrants.js +590 -0
  33. package/dist/proxy/shareLease.d.ts +101 -0
  34. package/dist/proxy/shareLease.js +192 -0
  35. package/dist/proxy/shareLedger.d.ts +105 -0
  36. package/dist/proxy/shareLedger.js +406 -0
  37. package/dist/proxy/shareListener.d.ts +60 -0
  38. package/dist/proxy/shareListener.js +143 -0
  39. package/dist/proxy/shareNotes.d.ts +97 -0
  40. package/dist/proxy/shareNotes.js +234 -0
  41. package/dist/proxy/sharePolicy.d.ts +110 -0
  42. package/dist/proxy/sharePolicy.js +366 -0
  43. package/dist/proxy/shareProvisioning.d.ts +110 -0
  44. package/dist/proxy/shareProvisioning.js +237 -0
  45. package/dist/proxy/shareReceipts.d.ts +99 -0
  46. package/dist/proxy/shareReceipts.js +303 -0
  47. package/dist/proxy/shareSigning.d.ts +40 -0
  48. package/dist/proxy/shareSigning.js +78 -0
  49. package/dist/server/routes/claudeProxyRoutes.js +1066 -3
  50. package/dist/types/cli.d.ts +61 -0
  51. package/dist/types/proxy.d.ts +781 -0
  52. package/dist/utils/streamCancellation.d.ts +46 -0
  53. package/dist/utils/streamCancellation.js +90 -0
  54. package/dist/utils/ttsStream.js +34 -1
  55. package/dist/voice/livekit/realtimeEventBridge.js +27 -6
  56. package/package.json +2 -1
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Out-of-band cancellation for chained async streams.
3
+ *
4
+ * `AsyncGenerator.prototype.return()` is the obvious way to tell a stream its
5
+ * consumer has gone away, and it does not work through a chain of generators.
6
+ * The request is *queued* behind whatever `next()` is already in flight; it
7
+ * cannot interrupt an `await`. A provider stream is wrapped several times
8
+ * (lifecycle in `baseProvider`, MCP and pool wrappers in `neurolink`), and
9
+ * during a pull every one of those layers is parked inside an `await` on the
10
+ * layer below. So none of them can unwind, the innermost source is never
11
+ * closed, and a teardown that waits for that to happen waits forever.
12
+ *
13
+ * This module is the side channel that does work. A wrapper registers a
14
+ * cancel callback on the stream object it returns; the callback closes its own
15
+ * upstream iterator and forwards the request further down. Because these are
16
+ * ordinary function calls rather than generator protocol, they run immediately
17
+ * — no queue, no dependence on any pending `next()` settling.
18
+ *
19
+ * Registration is optional and reading is defensive, so a stream that knows
20
+ * nothing about this behaves exactly as it did before.
21
+ */
22
+ /**
23
+ * Register `cancel` as `stream`'s teardown hook and return the same object.
24
+ *
25
+ * Non-enumerable so the property never shows up in spreads, `JSON.stringify`
26
+ * or logging of a stream handle.
27
+ */
28
+ export declare function attachStreamCancel<S extends object>(stream: S, cancel: () => void): S;
29
+ /**
30
+ * Invoke `stream`'s cancel hook if it has one.
31
+ *
32
+ * Never throws. This runs from `finally` blocks during teardown, where the
33
+ * consumer has already stopped listening — an exception here would replace
34
+ * whatever outcome the caller was actually returning with a cleanup error.
35
+ */
36
+ export declare function cancelStream(stream: unknown): void;
37
+ /**
38
+ * Close `iterator` without waiting for it and without surfacing a rejection.
39
+ *
40
+ * Deliberately not awaited: on a generator that is parked mid-`await` this
41
+ * promise may never settle, which is the whole failure this module exists to
42
+ * avoid. The call still propagates whenever the iterator can act on it.
43
+ */
44
+ export declare function releaseIterator(iterator: {
45
+ return?: (value?: unknown) => unknown;
46
+ }): void;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Out-of-band cancellation for chained async streams.
3
+ *
4
+ * `AsyncGenerator.prototype.return()` is the obvious way to tell a stream its
5
+ * consumer has gone away, and it does not work through a chain of generators.
6
+ * The request is *queued* behind whatever `next()` is already in flight; it
7
+ * cannot interrupt an `await`. A provider stream is wrapped several times
8
+ * (lifecycle in `baseProvider`, MCP and pool wrappers in `neurolink`), and
9
+ * during a pull every one of those layers is parked inside an `await` on the
10
+ * layer below. So none of them can unwind, the innermost source is never
11
+ * closed, and a teardown that waits for that to happen waits forever.
12
+ *
13
+ * This module is the side channel that does work. A wrapper registers a
14
+ * cancel callback on the stream object it returns; the callback closes its own
15
+ * upstream iterator and forwards the request further down. Because these are
16
+ * ordinary function calls rather than generator protocol, they run immediately
17
+ * — no queue, no dependence on any pending `next()` settling.
18
+ *
19
+ * Registration is optional and reading is defensive, so a stream that knows
20
+ * nothing about this behaves exactly as it did before.
21
+ */
22
+ const STREAM_CANCEL = Symbol.for("neurolink.streamCancel");
23
+ /**
24
+ * Register `cancel` as `stream`'s teardown hook and return the same object.
25
+ *
26
+ * Non-enumerable so the property never shows up in spreads, `JSON.stringify`
27
+ * or logging of a stream handle.
28
+ */
29
+ export function attachStreamCancel(stream, cancel) {
30
+ Object.defineProperty(stream, STREAM_CANCEL, {
31
+ value: cancel,
32
+ enumerable: false,
33
+ configurable: true,
34
+ writable: true,
35
+ });
36
+ return stream;
37
+ }
38
+ /**
39
+ * Invoke `stream`'s cancel hook if it has one.
40
+ *
41
+ * Never throws. This runs from `finally` blocks during teardown, where the
42
+ * consumer has already stopped listening — an exception here would replace
43
+ * whatever outcome the caller was actually returning with a cleanup error.
44
+ */
45
+ export function cancelStream(stream) {
46
+ if (stream === null || stream === undefined) {
47
+ return;
48
+ }
49
+ if (typeof stream !== "object" && typeof stream !== "function") {
50
+ return;
51
+ }
52
+ try {
53
+ // The property read is inside the try, not before it. Reading a symbol off
54
+ // an arbitrary object can execute user code — a Proxy trap or a throwing
55
+ // getter — and this function is documented as never throwing because it
56
+ // runs from teardown `finally` blocks, where an exception would replace
57
+ // the outcome the caller was actually returning with a cleanup error.
58
+ const hook = stream[STREAM_CANCEL];
59
+ if (typeof hook !== "function") {
60
+ return;
61
+ }
62
+ hook();
63
+ }
64
+ catch {
65
+ // Teardown is best-effort by definition.
66
+ }
67
+ }
68
+ /**
69
+ * Close `iterator` without waiting for it and without surfacing a rejection.
70
+ *
71
+ * Deliberately not awaited: on a generator that is parked mid-`await` this
72
+ * promise may never settle, which is the whole failure this module exists to
73
+ * avoid. The call still propagates whenever the iterator can act on it.
74
+ */
75
+ export function releaseIterator(iterator) {
76
+ if (typeof iterator.return !== "function") {
77
+ return;
78
+ }
79
+ try {
80
+ const result = iterator.return(undefined);
81
+ if (result &&
82
+ typeof result.catch === "function" &&
83
+ typeof result.then === "function") {
84
+ void result.catch(() => { });
85
+ }
86
+ }
87
+ catch {
88
+ // Best-effort, as above.
89
+ }
90
+ }
@@ -3,6 +3,7 @@ import { sanitizeErrorCause } from "./logSanitize.js";
3
3
  import { logger } from "./logger.js";
4
4
  import { IncrementalTTSSynthesisError, TTS_ERROR_CODES, TTSProcessor, } from "./ttsProcessor.js";
5
5
  import { TimeoutError as AsyncTimeoutError } from "./async/withTimeout.js";
6
+ import { cancelStream } from "./streamCancellation.js";
6
7
  function getStreamingTTSErrorDetails(error) {
7
8
  const incrementalFailure = error instanceof IncrementalTTSSynthesisError ? error : undefined;
8
9
  const cause = incrementalFailure?.firstError ?? error;
@@ -122,6 +123,20 @@ function aggregateTTSChunks(chunks) {
122
123
  sampleRate: last.sampleRate,
123
124
  };
124
125
  }
126
+ /**
127
+ * Upper bound on how long stream teardown may wait for upstream iterators to
128
+ * acknowledge `.return()`. Closing an iterator is bookkeeping, not work, so a
129
+ * responsive upstream finishes far inside this; an unresponsive one is exactly
130
+ * the case that must not block the consumer. The timer is unref'd so a pending
131
+ * grace period never by itself keeps the process alive.
132
+ */
133
+ const RELEASE_GRACE_MS = 5_000;
134
+ function releaseGraceTimer() {
135
+ return new Promise((resolve) => {
136
+ const timer = setTimeout(resolve, RELEASE_GRACE_MS);
137
+ timer.unref?.();
138
+ });
139
+ }
125
140
  /**
126
141
  * Preserve source-stream backpressure while interleaving incremental TTS audio.
127
142
  * Source chunks are yielded before their derived audio, and TTS failures degrade
@@ -208,6 +223,12 @@ export async function* interleaveTTSStream(params) {
208
223
  finally {
209
224
  if (!completed) {
210
225
  cancelled = true;
226
+ // Tell the upstream chain out of band, before asking politely via
227
+ // `.return()` below. When the consumer breaks mid-pull every wrapper
228
+ // beneath is parked inside an `await` and cannot process a queued
229
+ // `return()`, so this side channel is the only thing that actually
230
+ // reaches — and closes — the provider stream at the bottom.
231
+ cancelStream(stream);
211
232
  }
212
233
  textQueue.end();
213
234
  const releases = [];
@@ -217,7 +238,19 @@ export async function* interleaveTTSStream(params) {
217
238
  if (nextAudio && audioIterator.return) {
218
239
  releases.push(Promise.resolve().then(() => audioIterator.return?.(undefined)));
219
240
  }
220
- await Promise.allSettled(releases);
241
+ // Cleanup must not be able to outlive the consumer that abandoned this
242
+ // stream. `.return()` on an async generator parked inside an `await` is
243
+ // queued behind the in-flight `next()` and cannot interrupt it, so when an
244
+ // upstream pull never settles these promises settle *never* — not late.
245
+ // Awaiting them unbounded turned a consumer's `break` into a permanent
246
+ // hang: the caller's `for await` never returned, with no error and no way
247
+ // to defend against it from outside this module.
248
+ //
249
+ // The releases are still issued, and still propagate normally whenever the
250
+ // upstream is responsive — which is every case where awaiting them was
251
+ // doing anything useful. We only stop making the consumer's exit depend on
252
+ // an upstream that may never answer.
253
+ await Promise.race([Promise.allSettled(releases), releaseGraceTimer()]);
221
254
  if (!completed) {
222
255
  onComplete?.(undefined);
223
256
  }
@@ -52,18 +52,39 @@ export async function attachRealtimeEventBridge(params) {
52
52
  via,
53
53
  bytes: bytes.byteLength,
54
54
  });
55
- if (via === "data") {
56
- void participant.publishData(bytes, {
57
- reliable: true,
58
- topic: eventsTopic,
55
+ // Both calls are detached on purpose — the bridge must never make the
56
+ // caller wait on the UI. But `void` alone does NOT make a rejection
57
+ // harmless, and the try/catch below cannot help: a synchronous catch
58
+ // never sees a rejected promise. Measured — a rejecting publish with
59
+ // this exact shape leaves the catch untouched and, with no
60
+ // unhandledRejection handler installed, terminates the process:
61
+ // SYNC_CATCH_FIRED = false · UNHANDLED_COUNT = 1 · exit 1
62
+ // These are network calls to a peer that can disconnect mid-publish, so
63
+ // the rejection is ordinary, not exotic. Attaching a handler is what
64
+ // actually makes the bridge best-effort, which is what the catch below
65
+ // only claimed. Same class as the detached-pump crash in anthropic.
66
+ const publishFailed = (error) => {
67
+ logger.debug("realtime.bridge.publishFailed", {
68
+ seq,
69
+ type,
70
+ via,
71
+ error: error instanceof Error ? error.message : String(error),
59
72
  });
73
+ };
74
+ if (via === "data") {
75
+ void participant
76
+ .publishData(bytes, { reliable: true, topic: eventsTopic })
77
+ .catch(publishFailed);
60
78
  }
61
79
  else {
62
- void participant.sendText(json, { topic: eventsTopic });
80
+ void participant
81
+ .sendText(json, { topic: eventsTopic })
82
+ .catch(publishFailed);
63
83
  }
64
84
  }
65
85
  catch {
66
- /* non-fatal — the UI bridge is best-effort */
86
+ /* non-fatal — covers the synchronous half only (encode/stringify);
87
+ the detached publishes carry their own handler above. */
67
88
  }
68
89
  };
69
90
  // HITL: WRITE-labeled tools pause for user confirmation. We publish a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.29.2",
3
+ "version": "12.0.0",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -83,6 +83,7 @@
83
83
  "test:media": "pnpm exec tsx test/continuous-test-suite-media-gen.ts",
84
84
  "test:media-registry-collisions": "pnpm exec tsx test/continuous-test-suite-media-registry-collisions.ts",
85
85
  "test:memory": "pnpm exec tsx test/continuous-test-suite-memory.ts",
86
+ "test:proxy-sharing": "pnpm run build && pnpm exec tsx test/continuous-test-suite-proxy-sharing.ts",
86
87
  "test:openai-compat-streaming-retry": "pnpm exec tsx test/continuous-test-suite-openai-compat-streaming-retry.ts",
87
88
  "test:anthropic-streaming-retry": "pnpm exec tsx test/continuous-test-suite-anthropic-streaming-retry.ts",
88
89
  "test:adjust-body-after-400": "pnpm exec tsx test/continuous-test-suite-adjust-body-after-400.ts",