@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.
- package/CHANGELOG.md +54 -2
- package/dist/auth/anthropicOAuth.d.ts +50 -0
- package/dist/auth/anthropicOAuth.js +78 -0
- package/dist/browser/neurolink.min.js +393 -393
- package/dist/cli/commands/proxy.d.ts +2 -0
- package/dist/cli/commands/proxy.js +284 -4
- package/dist/cli/commands/proxyExpose.d.ts +35 -0
- package/dist/cli/commands/proxyExpose.js +252 -0
- package/dist/cli/commands/proxyPeer.d.ts +29 -0
- package/dist/cli/commands/proxyPeer.js +738 -0
- package/dist/cli/commands/proxyShare.d.ts +37 -0
- package/dist/cli/commands/proxyShare.js +1080 -0
- package/dist/cli/parser.js +7 -1
- package/dist/core/baseProvider.js +20 -1
- package/dist/neurolink.js +30 -2
- package/dist/proxy/peerStore.d.ts +52 -0
- package/dist/proxy/peerStore.js +324 -0
- package/dist/proxy/peerTransport.d.ts +38 -0
- package/dist/proxy/peerTransport.js +242 -0
- package/dist/proxy/proxyPaths.d.ts +8 -0
- package/dist/proxy/proxyPaths.js +55 -17
- package/dist/proxy/requestLogger.js +8 -0
- package/dist/proxy/residentGrants.d.ts +57 -0
- package/dist/proxy/residentGrants.js +393 -0
- package/dist/proxy/shareAudit.d.ts +81 -0
- package/dist/proxy/shareAudit.js +280 -0
- package/dist/proxy/shareContext.d.ts +38 -0
- package/dist/proxy/shareContext.js +92 -0
- package/dist/proxy/shareGate.d.ts +64 -0
- package/dist/proxy/shareGate.js +216 -0
- package/dist/proxy/shareGrants.d.ts +115 -0
- package/dist/proxy/shareGrants.js +590 -0
- package/dist/proxy/shareLease.d.ts +101 -0
- package/dist/proxy/shareLease.js +192 -0
- package/dist/proxy/shareLedger.d.ts +105 -0
- package/dist/proxy/shareLedger.js +406 -0
- package/dist/proxy/shareListener.d.ts +60 -0
- package/dist/proxy/shareListener.js +143 -0
- package/dist/proxy/shareNotes.d.ts +97 -0
- package/dist/proxy/shareNotes.js +234 -0
- package/dist/proxy/sharePolicy.d.ts +110 -0
- package/dist/proxy/sharePolicy.js +366 -0
- package/dist/proxy/shareProvisioning.d.ts +110 -0
- package/dist/proxy/shareProvisioning.js +237 -0
- package/dist/proxy/shareReceipts.d.ts +99 -0
- package/dist/proxy/shareReceipts.js +303 -0
- package/dist/proxy/shareSigning.d.ts +40 -0
- package/dist/proxy/shareSigning.js +78 -0
- package/dist/server/routes/claudeProxyRoutes.js +1066 -3
- package/dist/types/cli.d.ts +61 -0
- package/dist/types/proxy.d.ts +781 -0
- package/dist/utils/streamCancellation.d.ts +46 -0
- package/dist/utils/streamCancellation.js +90 -0
- package/dist/utils/ttsStream.js +34 -1
- package/dist/voice/livekit/realtimeEventBridge.js +27 -6
- 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
|
+
}
|
package/dist/utils/ttsStream.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
80
|
+
void participant
|
|
81
|
+
.sendText(json, { topic: eventsTopic })
|
|
82
|
+
.catch(publishFailed);
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
85
|
catch {
|
|
66
|
-
/* non-fatal — the
|
|
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": "
|
|
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",
|