@juspay/neurolink 9.93.1 → 9.94.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 (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +306 -306
  3. package/dist/cli/commands/proxy.js +687 -200
  4. package/dist/cli/utils/serverUtils.js +4 -1
  5. package/dist/lib/proxy/globalInstaller.js +1 -1
  6. package/dist/lib/proxy/openaiFormat.js +1 -0
  7. package/dist/lib/proxy/rollingProxyServer.d.ts +3 -0
  8. package/dist/lib/proxy/rollingProxyServer.js +149 -0
  9. package/dist/lib/proxy/rollingWorkerProcess.d.ts +2 -0
  10. package/dist/lib/proxy/rollingWorkerProcess.js +194 -0
  11. package/dist/lib/proxy/rollingWorkerProtocol.d.ts +5 -0
  12. package/dist/lib/proxy/rollingWorkerProtocol.js +43 -0
  13. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +39 -0
  14. package/dist/lib/proxy/rollingWorkerSupervisor.js +402 -0
  15. package/dist/lib/proxy/socketWorkerRuntime.d.ts +14 -0
  16. package/dist/lib/proxy/socketWorkerRuntime.js +294 -0
  17. package/dist/lib/proxy/sseInterceptor.js +1 -1
  18. package/dist/lib/proxy/workerLog.d.ts +6 -0
  19. package/dist/lib/proxy/workerLog.js +42 -0
  20. package/dist/lib/server/routes/openaiProxyRoutes.d.ts +9 -0
  21. package/dist/lib/server/routes/openaiProxyRoutes.js +16 -1
  22. package/dist/lib/types/cli.d.ts +38 -0
  23. package/dist/lib/types/proxy.d.ts +156 -0
  24. package/dist/lib/utils/errorHandling.d.ts +8 -0
  25. package/dist/lib/utils/errorHandling.js +20 -0
  26. package/dist/proxy/globalInstaller.js +1 -1
  27. package/dist/proxy/openaiFormat.js +1 -0
  28. package/dist/proxy/rollingProxyServer.d.ts +3 -0
  29. package/dist/proxy/rollingProxyServer.js +148 -0
  30. package/dist/proxy/rollingWorkerProcess.d.ts +2 -0
  31. package/dist/proxy/rollingWorkerProcess.js +193 -0
  32. package/dist/proxy/rollingWorkerProtocol.d.ts +5 -0
  33. package/dist/proxy/rollingWorkerProtocol.js +42 -0
  34. package/dist/proxy/rollingWorkerSupervisor.d.ts +39 -0
  35. package/dist/proxy/rollingWorkerSupervisor.js +401 -0
  36. package/dist/proxy/socketWorkerRuntime.d.ts +14 -0
  37. package/dist/proxy/socketWorkerRuntime.js +293 -0
  38. package/dist/proxy/sseInterceptor.js +1 -1
  39. package/dist/proxy/workerLog.d.ts +6 -0
  40. package/dist/proxy/workerLog.js +41 -0
  41. package/dist/server/routes/openaiProxyRoutes.d.ts +9 -0
  42. package/dist/server/routes/openaiProxyRoutes.js +16 -1
  43. package/dist/types/cli.d.ts +38 -0
  44. package/dist/types/proxy.d.ts +156 -0
  45. package/dist/utils/errorHandling.d.ts +8 -0
  46. package/dist/utils/errorHandling.js +20 -0
  47. package/package.json +3 -2
@@ -112,7 +112,10 @@ export class StateFileManager {
112
112
  if (!fs.existsSync(dir)) {
113
113
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
114
114
  }
115
- fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2));
115
+ fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), {
116
+ mode: 0o600,
117
+ });
118
+ fs.chmodSync(this.filePath, 0o600);
116
119
  }
117
120
  /**
118
121
  * Load state from the state file
@@ -14,7 +14,7 @@ function writableDirectory(path) {
14
14
  if (!existsSync(path)) {
15
15
  return false;
16
16
  }
17
- accessSync(path, constants.W_OK);
17
+ accessSync(path, constants.W_OK | constants.X_OK);
18
18
  return true;
19
19
  }
20
20
  catch {
@@ -760,6 +760,7 @@ export function createClaudeToOpenAIStreamTransform(requestModel, options = {})
760
760
  finished = true;
761
761
  options.onError?.(message);
762
762
  emit(controller, serializer.emitError(message));
763
+ controller.terminate();
763
764
  return;
764
765
  }
765
766
  default:
@@ -0,0 +1,3 @@
1
+ import type { RollingProxyServer, RollingProxyServerOptions } from "../types/index.js";
2
+ /** Keep one public listener stable while serving workers start and rotate. */
3
+ export declare function startRollingProxyServer(options: RollingProxyServerOptions): Promise<RollingProxyServer>;
@@ -0,0 +1,149 @@
1
+ import { createServer } from "node:net";
2
+ import { ErrorFactory } from "../utils/errorHandling.js";
3
+ import { RollingWorkerSupervisor } from "./rollingWorkerSupervisor.js";
4
+ const DEFAULT_RECOVERY_DELAY_MS = 250;
5
+ const DEFAULT_MAX_RECOVERY_DELAY_MS = 10_000;
6
+ /** Keep one public listener stable while serving workers start and rotate. */
7
+ export async function startRollingProxyServer(options) {
8
+ let desiredVersion = options.initialVersion;
9
+ let closing = false;
10
+ let listening = false;
11
+ let recoveryFailures = 0;
12
+ let recoveryTimer;
13
+ const recoveryDelayMs = Math.max(1, options.recoveryDelayMs ?? DEFAULT_RECOVERY_DELAY_MS);
14
+ const maxRecoveryDelayMs = Math.max(recoveryDelayMs, options.maxRecoveryDelayMs ?? DEFAULT_MAX_RECOVERY_DELAY_MS);
15
+ const scheduleRecovery = () => {
16
+ if (closing || !listening || recoveryTimer) {
17
+ return;
18
+ }
19
+ const delay = Math.min(maxRecoveryDelayMs, recoveryDelayMs * 2 ** Math.min(recoveryFailures, 8));
20
+ options.log?.(`[proxy-supervisor] scheduling worker recovery version=${desiredVersion} delayMs=${delay}`);
21
+ recoveryTimer = setTimeout(() => {
22
+ recoveryTimer = undefined;
23
+ // An explicit replace() may have started (or completed) a generation
24
+ // while this timer was pending. Re-validate before recovering so we never
25
+ // launch a duplicate generation that conflicts with the requested worker.
26
+ const snapshot = supervisor.snapshot();
27
+ if (closing || snapshot.active || snapshot.candidate) {
28
+ return;
29
+ }
30
+ void supervisor.replace(desiredVersion).then(() => {
31
+ recoveryFailures = 0;
32
+ }, (error) => {
33
+ recoveryFailures += 1;
34
+ options.log?.(`[proxy-supervisor] worker recovery failed: ${error instanceof Error ? error.message : String(error)}`);
35
+ scheduleRecovery();
36
+ });
37
+ }, delay);
38
+ recoveryTimer.unref?.();
39
+ };
40
+ const stateChanged = (snapshot) => {
41
+ try {
42
+ options.onStateChange?.(snapshot);
43
+ }
44
+ catch (error) {
45
+ options.log?.(`[proxy-supervisor] failed to publish server state: ${error instanceof Error ? error.message : String(error)}`);
46
+ }
47
+ if (listening && !closing && !snapshot.active && !snapshot.candidate) {
48
+ scheduleRecovery();
49
+ }
50
+ };
51
+ const supervisor = new RollingWorkerSupervisor({
52
+ spawnWorker: options.spawnWorker,
53
+ readyTimeoutMs: options.readyTimeoutMs,
54
+ socketQueueLimit: options.socketQueueLimit,
55
+ socketQueueTimeoutMs: options.socketQueueTimeoutMs,
56
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
57
+ onStateChange: stateChanged,
58
+ log: options.log,
59
+ });
60
+ const listener = createServer({ pauseOnConnect: true }, (socket) => {
61
+ supervisor.acceptSocket(socket);
62
+ });
63
+ await new Promise((resolve, reject) => {
64
+ const onError = (error) => {
65
+ listener.off("listening", onListening);
66
+ reject(error);
67
+ };
68
+ const onListening = () => {
69
+ listener.off("error", onError);
70
+ resolve();
71
+ };
72
+ listener.once("error", onError);
73
+ listener.once("listening", onListening);
74
+ listener.listen(options.port, options.host);
75
+ }).catch(async (error) => {
76
+ await supervisor.close();
77
+ throw error;
78
+ });
79
+ listening = true;
80
+ try {
81
+ await supervisor.start(desiredVersion);
82
+ }
83
+ catch (error) {
84
+ recoveryFailures = 1;
85
+ options.log?.(`[proxy-supervisor] initial worker failed; listener will remain available during recovery: ${error instanceof Error ? error.message : String(error)}`);
86
+ }
87
+ if (!supervisor.snapshot().active) {
88
+ scheduleRecovery();
89
+ }
90
+ const address = listener.address();
91
+ if (!address || typeof address === "string") {
92
+ listener.close();
93
+ void supervisor.close();
94
+ throw ErrorFactory.proxyWorkerLifecycle("rolling proxy listener did not expose a TCP address");
95
+ }
96
+ return {
97
+ address: { host: options.host, port: address.port },
98
+ replace: async (expectedVersion) => {
99
+ // Adopt the requested version as the recovery target up front (once it is
100
+ // a valid version string) so the autonomous recovery loop converges on it
101
+ // even if this explicit call collides with an in-flight recovery
102
+ // replacement — otherwise recovery keeps re-targeting a stale version and
103
+ // starves the caller (e.g. an update rollback) for the whole retry window.
104
+ const isValidVersion = /^\d+\.\d+\.\d+$/.test(expectedVersion);
105
+ if (isValidVersion) {
106
+ desiredVersion = expectedVersion;
107
+ }
108
+ // Cancel any pending recovery so it cannot race this explicit
109
+ // replacement and spawn a second, conflicting generation.
110
+ if (recoveryTimer) {
111
+ clearTimeout(recoveryTimer);
112
+ recoveryTimer = undefined;
113
+ }
114
+ try {
115
+ const snapshot = await supervisor.replace(expectedVersion);
116
+ recoveryFailures = 0;
117
+ return snapshot;
118
+ }
119
+ catch (error) {
120
+ // The explicit replacement failed (invalid version, closed, or a
121
+ // conflicting replacement is already in progress). Since we cancelled
122
+ // the pending recovery timer above, re-schedule recovery when nothing
123
+ // is serving so a crashed worker is not left unreplaced.
124
+ const snapshot = supervisor.snapshot();
125
+ if (!snapshot.active && !snapshot.candidate) {
126
+ scheduleRecovery();
127
+ }
128
+ throw error;
129
+ }
130
+ },
131
+ snapshot: () => supervisor.snapshot(),
132
+ close: async () => {
133
+ if (closing) {
134
+ return;
135
+ }
136
+ closing = true;
137
+ listening = false;
138
+ if (recoveryTimer) {
139
+ clearTimeout(recoveryTimer);
140
+ recoveryTimer = undefined;
141
+ }
142
+ const listenerClosed = new Promise((resolve, reject) => {
143
+ listener.close((error) => (error ? reject(error) : resolve()));
144
+ });
145
+ await Promise.all([listenerClosed, supervisor.close()]);
146
+ },
147
+ };
148
+ }
149
+ //# sourceMappingURL=rollingProxyServer.js.map
@@ -0,0 +1,2 @@
1
+ import type { RollingWorkerHandle, SpawnProxySocketWorkerOptions } from "../types/index.js";
2
+ export declare function spawnProxySocketWorker(options: SpawnProxySocketWorkerOptions): RollingWorkerHandle;
@@ -0,0 +1,194 @@
1
+ import { spawn } from "node:child_process";
2
+ import { ErrorFactory } from "../utils/errorHandling.js";
3
+ import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, } from "./rollingWorkerProtocol.js";
4
+ export function spawnProxySocketWorker(options) {
5
+ const socketAckTimeoutMs = Math.max(1, options.socketAckTimeoutMs ?? 5_000);
6
+ let nextSocketId = 0;
7
+ const pendingSockets = new Map();
8
+ const statusListeners = new Set();
9
+ const pendingStatusMessages = [];
10
+ let spawnError;
11
+ const child = spawn(options.command, options.args, {
12
+ env: {
13
+ ...process.env,
14
+ ...options.env,
15
+ [PROXY_SOCKET_WORKER_ENV]: "1",
16
+ NEUROLINK_PROXY_WORKER_GENERATION: String(options.generation),
17
+ NEUROLINK_PROXY_WORKER_EXPECTED_VERSION: options.expectedVersion,
18
+ },
19
+ stdio: [
20
+ "ignore",
21
+ options.stdout ?? "inherit",
22
+ options.stderr ?? "inherit",
23
+ "ipc",
24
+ ],
25
+ });
26
+ const childPid = child.pid;
27
+ const publishStatus = (message) => {
28
+ if (statusListeners.size === 0) {
29
+ if (pendingStatusMessages.length >= 32) {
30
+ pendingStatusMessages.shift();
31
+ }
32
+ pendingStatusMessages.push(message);
33
+ return;
34
+ }
35
+ for (const listener of statusListeners) {
36
+ listener(message);
37
+ }
38
+ };
39
+ child.once("error", (error) => {
40
+ spawnError = error;
41
+ if (!childPid) {
42
+ return;
43
+ }
44
+ publishStatus({
45
+ type: "proxy-worker:fatal",
46
+ generation: options.generation,
47
+ pid: childPid,
48
+ message: error.message,
49
+ });
50
+ });
51
+ if (!childPid) {
52
+ child.kill("SIGTERM");
53
+ throw ErrorFactory.proxyWorkerLifecycle("proxy worker spawn did not return a pid");
54
+ }
55
+ const settleSocket = (socketId, error) => {
56
+ const pending = pendingSockets.get(socketId);
57
+ if (!pending) {
58
+ return;
59
+ }
60
+ pendingSockets.delete(socketId);
61
+ clearTimeout(pending.timeout);
62
+ if (!error) {
63
+ pending.socket.destroy();
64
+ }
65
+ pending.callback(error);
66
+ };
67
+ const onInternalMessage = (message) => {
68
+ if (isProxyWorkerStatusMessage(message) &&
69
+ message.type === "proxy-worker:socket-accepted" &&
70
+ message.generation === options.generation &&
71
+ message.pid === childPid) {
72
+ const pending = pendingSockets.get(message.socketId);
73
+ if (!pending || pending.accepted) {
74
+ return;
75
+ }
76
+ pending.accepted = true;
77
+ try {
78
+ child.send({
79
+ type: "proxy-worker:socket-commit",
80
+ generation: options.generation,
81
+ socketId: message.socketId,
82
+ }, (error) => settleSocket(message.socketId, error ?? undefined));
83
+ }
84
+ catch (error) {
85
+ settleSocket(message.socketId, error instanceof Error ? error : new Error(String(error)));
86
+ }
87
+ }
88
+ };
89
+ child.on("message", (message) => {
90
+ onInternalMessage(message);
91
+ if (isProxyWorkerStatusMessage(message)) {
92
+ publishStatus(message);
93
+ }
94
+ });
95
+ child.once("exit", () => {
96
+ for (const socketId of [...pendingSockets.keys()]) {
97
+ settleSocket(socketId, new Error(`proxy worker ${childPid} exited before accepting socket`));
98
+ }
99
+ });
100
+ const sendControl = (message) => {
101
+ if (!child.connected) {
102
+ throw new Error(`proxy worker ${childPid} IPC channel is closed`);
103
+ }
104
+ child.send(message, (error) => {
105
+ if (error && child.exitCode === null && child.signalCode === null) {
106
+ child.kill("SIGTERM");
107
+ }
108
+ });
109
+ };
110
+ return {
111
+ pid: childPid,
112
+ sendControl,
113
+ sendSocket: (generation, socket, callback) => {
114
+ if (!child.connected) {
115
+ callback(new Error(`proxy worker ${childPid} IPC channel is closed`));
116
+ return;
117
+ }
118
+ const socketId = `${generation}:${++nextSocketId}`;
119
+ const timeout = setTimeout(() => {
120
+ if (child.connected) {
121
+ try {
122
+ child.send({
123
+ type: "proxy-worker:socket-cancel",
124
+ generation,
125
+ socketId,
126
+ });
127
+ }
128
+ catch {
129
+ // The worker is terminated after the failed transfer is reported.
130
+ }
131
+ }
132
+ settleSocket(socketId, new Error(`proxy worker ${childPid} did not accept socket within ${socketAckTimeoutMs}ms`));
133
+ }, socketAckTimeoutMs);
134
+ timeout.unref?.();
135
+ pendingSockets.set(socketId, {
136
+ socket,
137
+ callback,
138
+ timeout,
139
+ accepted: false,
140
+ });
141
+ try {
142
+ child.send({ type: "proxy-worker:socket", generation, socketId }, socket, { keepOpen: true }, (error) => {
143
+ if (error) {
144
+ settleSocket(socketId, error);
145
+ }
146
+ });
147
+ }
148
+ catch (error) {
149
+ settleSocket(socketId, error instanceof Error ? error : new Error(String(error)));
150
+ }
151
+ },
152
+ terminate: (signal = "SIGTERM") => {
153
+ if (child.exitCode === null && child.signalCode === null) {
154
+ child.kill(signal);
155
+ }
156
+ },
157
+ onMessage: (listener) => {
158
+ statusListeners.add(listener);
159
+ if (pendingStatusMessages.length > 0) {
160
+ const buffered = pendingStatusMessages.splice(0);
161
+ queueMicrotask(() => {
162
+ if (statusListeners.has(listener)) {
163
+ for (const message of buffered) {
164
+ listener(message);
165
+ }
166
+ }
167
+ });
168
+ }
169
+ else if (spawnError) {
170
+ const error = spawnError;
171
+ queueMicrotask(() => {
172
+ if (statusListeners.has(listener)) {
173
+ listener({
174
+ type: "proxy-worker:fatal",
175
+ generation: options.generation,
176
+ pid: childPid,
177
+ message: error.message,
178
+ });
179
+ }
180
+ });
181
+ }
182
+ return () => statusListeners.delete(listener);
183
+ },
184
+ onExit: (listener) => {
185
+ if (child.exitCode !== null || child.signalCode !== null) {
186
+ queueMicrotask(() => listener(child.exitCode, child.signalCode));
187
+ return () => undefined;
188
+ }
189
+ child.on("exit", listener);
190
+ return () => child.off("exit", listener);
191
+ },
192
+ };
193
+ }
194
+ //# sourceMappingURL=rollingWorkerProcess.js.map
@@ -0,0 +1,5 @@
1
+ import type { ProxyWorkerControlMessage, ProxyWorkerStatusMessage } from "../types/index.js";
2
+ export declare const PROXY_SOCKET_WORKER_ENV = "NEUROLINK_PROXY_SOCKET_WORKER";
3
+ export declare const PROXY_ROLLING_SUPERVISOR_ENV = "NEUROLINK_PROXY_ROLLING_SUPERVISOR";
4
+ export declare function isProxyWorkerControlMessage(value: unknown): value is ProxyWorkerControlMessage;
5
+ export declare function isProxyWorkerStatusMessage(value: unknown): value is ProxyWorkerStatusMessage;
@@ -0,0 +1,43 @@
1
+ export const PROXY_SOCKET_WORKER_ENV = "NEUROLINK_PROXY_SOCKET_WORKER";
2
+ export const PROXY_ROLLING_SUPERVISOR_ENV = "NEUROLINK_PROXY_ROLLING_SUPERVISOR";
3
+ export function isProxyWorkerControlMessage(value) {
4
+ if (!value || typeof value !== "object") {
5
+ return false;
6
+ }
7
+ const message = value;
8
+ if (!Number.isSafeInteger(message.generation) ||
9
+ Number(message.generation) <= 0) {
10
+ return false;
11
+ }
12
+ if (message.type === "proxy-worker:socket-commit" ||
13
+ message.type === "proxy-worker:socket-cancel") {
14
+ return typeof message.socketId === "string" && message.socketId.length > 0;
15
+ }
16
+ return (message.type === "proxy-worker:drain" ||
17
+ message.type === "proxy-worker:activate" ||
18
+ message.type === "proxy-worker:shutdown");
19
+ }
20
+ export function isProxyWorkerStatusMessage(value) {
21
+ if (!value || typeof value !== "object") {
22
+ return false;
23
+ }
24
+ const message = value;
25
+ if (!Number.isSafeInteger(message.generation) ||
26
+ Number(message.generation) <= 0 ||
27
+ !Number.isSafeInteger(message.pid) ||
28
+ Number(message.pid) <= 0) {
29
+ return false;
30
+ }
31
+ if (message.type === "proxy-worker:ready") {
32
+ return typeof message.version === "string" && message.version.length > 0;
33
+ }
34
+ if (message.type === "proxy-worker:activated" ||
35
+ message.type === "proxy-worker:drained") {
36
+ return true;
37
+ }
38
+ if (message.type === "proxy-worker:socket-accepted") {
39
+ return typeof message.socketId === "string" && message.socketId.length > 0;
40
+ }
41
+ return (message.type === "proxy-worker:fatal" && typeof message.message === "string");
42
+ }
43
+ //# sourceMappingURL=rollingWorkerProtocol.js.map
@@ -0,0 +1,39 @@
1
+ import type { RollingWorkerSupervisorOptions, RollingWorkerSupervisorSnapshot, TransferableProxySocket } from "../types/index.js";
2
+ /**
3
+ * Owns worker generations while the caller owns the public listening socket.
4
+ * Sockets are transferred once to the active worker, so response bytes never
5
+ * traverse a supervisor-side proxy hop.
6
+ */
7
+ export declare class RollingWorkerSupervisor {
8
+ private readonly options;
9
+ private generation;
10
+ private active;
11
+ private candidate;
12
+ private readonly draining;
13
+ private readonly queuedSockets;
14
+ private replacement;
15
+ private rejectedSockets;
16
+ private failedTransfers;
17
+ private lastFailure;
18
+ private closed;
19
+ private shutdownPromise;
20
+ private readonly shutdownWaiters;
21
+ constructor(options: RollingWorkerSupervisorOptions);
22
+ snapshot(): RollingWorkerSupervisorSnapshot;
23
+ start(expectedVersion: string): Promise<RollingWorkerSupervisorSnapshot>;
24
+ replace(expectedVersion: string): Promise<RollingWorkerSupervisorSnapshot>;
25
+ acceptSocket(socket: TransferableProxySocket): void;
26
+ private queueSocket;
27
+ close(): Promise<void>;
28
+ private closeWorkers;
29
+ private requestWorkerShutdown;
30
+ private forceTerminateWorkers;
31
+ private notifyShutdownWaiters;
32
+ private spawnCandidate;
33
+ private flushQueuedSockets;
34
+ private transferSocket;
35
+ private handleTransferFailure;
36
+ private rejectSocket;
37
+ private recordFailure;
38
+ private publishState;
39
+ }