@juspay/neurolink 10.4.2 → 10.4.4

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 (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/auth/tokenStore.d.ts +7 -0
  3. package/dist/auth/tokenStore.js +54 -0
  4. package/dist/browser/neurolink.min.js +377 -380
  5. package/dist/cli/commands/proxy.d.ts +2 -1
  6. package/dist/cli/commands/proxy.js +290 -63
  7. package/dist/cli/commands/proxyAnalyze.js +10 -1
  8. package/dist/lib/auth/tokenStore.d.ts +7 -0
  9. package/dist/lib/auth/tokenStore.js +54 -0
  10. package/dist/lib/proxy/proxyAnalysis.js +136 -12
  11. package/dist/lib/proxy/proxyTranslationEngine.d.ts +1 -0
  12. package/dist/lib/proxy/proxyTranslationEngine.js +56 -12
  13. package/dist/lib/proxy/rawStreamCapture.js +47 -1
  14. package/dist/lib/proxy/requestLogger.d.ts +2 -0
  15. package/dist/lib/proxy/requestLogger.js +72 -13
  16. package/dist/lib/proxy/rollingProxyServer.js +79 -8
  17. package/dist/lib/proxy/rollingWorkerProcess.js +20 -15
  18. package/dist/lib/proxy/rollingWorkerProtocol.js +3 -0
  19. package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
  20. package/dist/lib/proxy/rollingWorkerSupervisor.js +31 -11
  21. package/dist/lib/proxy/runtimeConfig.d.ts +1 -0
  22. package/dist/lib/proxy/runtimeConfig.js +20 -5
  23. package/dist/lib/proxy/socketWorkerRuntime.js +41 -13
  24. package/dist/lib/proxy/sseInterceptor.js +47 -1
  25. package/dist/lib/proxy/tokenRefresh.d.ts +6 -0
  26. package/dist/lib/proxy/tokenRefresh.js +70 -15
  27. package/dist/lib/proxy/usageStats.d.ts +25 -3
  28. package/dist/lib/proxy/usageStats.js +546 -55
  29. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -0
  30. package/dist/lib/server/routes/claudeProxyRoutes.js +261 -297
  31. package/dist/lib/types/proxy.d.ts +90 -1
  32. package/dist/proxy/proxyAnalysis.js +136 -12
  33. package/dist/proxy/proxyTranslationEngine.d.ts +1 -0
  34. package/dist/proxy/proxyTranslationEngine.js +56 -12
  35. package/dist/proxy/rawStreamCapture.js +47 -1
  36. package/dist/proxy/requestLogger.d.ts +2 -0
  37. package/dist/proxy/requestLogger.js +72 -13
  38. package/dist/proxy/rollingProxyServer.js +79 -8
  39. package/dist/proxy/rollingWorkerProcess.js +20 -15
  40. package/dist/proxy/rollingWorkerProtocol.js +3 -0
  41. package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
  42. package/dist/proxy/rollingWorkerSupervisor.js +31 -11
  43. package/dist/proxy/runtimeConfig.d.ts +1 -0
  44. package/dist/proxy/runtimeConfig.js +20 -5
  45. package/dist/proxy/socketWorkerRuntime.js +41 -13
  46. package/dist/proxy/sseInterceptor.js +47 -1
  47. package/dist/proxy/tokenRefresh.d.ts +6 -0
  48. package/dist/proxy/tokenRefresh.js +70 -15
  49. package/dist/proxy/usageStats.d.ts +25 -3
  50. package/dist/proxy/usageStats.js +546 -55
  51. package/dist/server/routes/claudeProxyRoutes.d.ts +2 -0
  52. package/dist/server/routes/claudeProxyRoutes.js +261 -297
  53. package/dist/types/proxy.d.ts +90 -1
  54. package/package.json +1 -1
@@ -10,8 +10,28 @@ export async function startRollingProxyServer(options) {
10
10
  let listening = false;
11
11
  let recoveryFailures = 0;
12
12
  let recoveryTimer;
13
+ let requestedReplacementTimer;
14
+ let requestedReplacementSchedule = 0;
15
+ let requestedReplacementPending = false;
16
+ let replacementQueueTail = null;
13
17
  const recoveryDelayMs = Math.max(1, options.recoveryDelayMs ?? DEFAULT_RECOVERY_DELAY_MS);
14
18
  const maxRecoveryDelayMs = Math.max(recoveryDelayMs, options.maxRecoveryDelayMs ?? DEFAULT_MAX_RECOVERY_DELAY_MS);
19
+ const queueReplacement = (operation) => {
20
+ const predecessor = replacementQueueTail;
21
+ const result = (predecessor ?? Promise.resolve()).then(operation);
22
+ const completion = result.then(() => undefined, () => undefined);
23
+ replacementQueueTail = completion;
24
+ void completion.finally(() => {
25
+ if (replacementQueueTail !== completion) {
26
+ return;
27
+ }
28
+ replacementQueueTail = null;
29
+ if (requestedReplacementPending) {
30
+ scheduleRequestedReplacement();
31
+ }
32
+ });
33
+ return result;
34
+ };
15
35
  const scheduleRecovery = () => {
16
36
  if (closing || !listening || recoveryTimer) {
17
37
  return;
@@ -23,11 +43,13 @@ export async function startRollingProxyServer(options) {
23
43
  // An explicit replace() may have started (or completed) a generation
24
44
  // while this timer was pending. Re-validate before recovering so we never
25
45
  // 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(() => {
46
+ void queueReplacement(async () => {
47
+ const snapshot = supervisor.snapshot();
48
+ if (closing || snapshot.active || snapshot.candidate) {
49
+ return;
50
+ }
51
+ await supervisor.replace(desiredVersion);
52
+ }).then(() => {
31
53
  recoveryFailures = 0;
32
54
  }, (error) => {
33
55
  recoveryFailures += 1;
@@ -48,6 +70,38 @@ export async function startRollingProxyServer(options) {
48
70
  scheduleRecovery();
49
71
  }
50
72
  };
73
+ function scheduleRequestedReplacement() {
74
+ if (closing) {
75
+ return;
76
+ }
77
+ requestedReplacementPending = true;
78
+ if (requestedReplacementTimer || replacementQueueTail) {
79
+ return;
80
+ }
81
+ const schedule = ++requestedReplacementSchedule;
82
+ requestedReplacementTimer = setTimeout(() => {
83
+ requestedReplacementTimer = undefined;
84
+ if (schedule !== requestedReplacementSchedule) {
85
+ return;
86
+ }
87
+ if (closing || !supervisor.snapshot().active) {
88
+ return;
89
+ }
90
+ requestedReplacementPending = false;
91
+ const replacementVersion = desiredVersion;
92
+ void queueReplacement(async () => {
93
+ if (closing || !supervisor.snapshot().active) {
94
+ return;
95
+ }
96
+ options.log?.(`[proxy-supervisor] preparing same-version worker replacement version=${replacementVersion} reason=environment`);
97
+ await supervisor.replace(replacementVersion);
98
+ options.log?.(`[proxy-supervisor] same-version worker replacement complete version=${replacementVersion} reason=environment`);
99
+ }).catch((error) => {
100
+ options.log?.(`[proxy-supervisor] same-version worker replacement failed version=${replacementVersion} reason=environment: ${error instanceof Error ? error.message : String(error)}`);
101
+ });
102
+ }, 50);
103
+ requestedReplacementTimer.unref?.();
104
+ }
51
105
  const supervisor = new RollingWorkerSupervisor({
52
106
  spawnWorker: options.spawnWorker,
53
107
  readyTimeoutMs: options.readyTimeoutMs,
@@ -55,9 +109,14 @@ export async function startRollingProxyServer(options) {
55
109
  socketQueueTimeoutMs: options.socketQueueTimeoutMs,
56
110
  shutdownTimeoutMs: options.shutdownTimeoutMs,
57
111
  onStateChange: stateChanged,
112
+ onReplacementRequested: scheduleRequestedReplacement,
58
113
  log: options.log,
59
114
  });
60
115
  const listener = createServer({ pauseOnConnect: true }, (socket) => {
116
+ // The parent keeps its descriptor until the worker commits the IPC
117
+ // transfer. Consume client resets during that interval so they cannot
118
+ // terminate the long-lived supervisor process.
119
+ socket.once("error", () => socket.destroy());
61
120
  supervisor.acceptSocket(socket);
62
121
  });
63
122
  await new Promise((resolve, reject) => {
@@ -89,8 +148,8 @@ export async function startRollingProxyServer(options) {
89
148
  }
90
149
  const address = listener.address();
91
150
  if (!address || typeof address === "string") {
92
- listener.close();
93
- void supervisor.close();
151
+ await new Promise((resolve) => listener.close(() => resolve()));
152
+ await supervisor.close().catch(() => undefined);
94
153
  throw ErrorFactory.proxyWorkerLifecycle("rolling proxy listener did not expose a TCP address");
95
154
  }
96
155
  return {
@@ -111,8 +170,14 @@ export async function startRollingProxyServer(options) {
111
170
  clearTimeout(recoveryTimer);
112
171
  recoveryTimer = undefined;
113
172
  }
173
+ if (requestedReplacementTimer) {
174
+ clearTimeout(requestedReplacementTimer);
175
+ requestedReplacementTimer = undefined;
176
+ }
177
+ requestedReplacementSchedule += 1;
178
+ requestedReplacementPending = false;
114
179
  try {
115
- const snapshot = await supervisor.replace(expectedVersion);
180
+ const snapshot = await queueReplacement(() => supervisor.replace(expectedVersion));
116
181
  recoveryFailures = 0;
117
182
  return snapshot;
118
183
  }
@@ -139,6 +204,12 @@ export async function startRollingProxyServer(options) {
139
204
  clearTimeout(recoveryTimer);
140
205
  recoveryTimer = undefined;
141
206
  }
207
+ if (requestedReplacementTimer) {
208
+ clearTimeout(requestedReplacementTimer);
209
+ requestedReplacementTimer = undefined;
210
+ }
211
+ requestedReplacementSchedule += 1;
212
+ requestedReplacementPending = false;
142
213
  const listenerClosed = new Promise((resolve, reject) => {
143
214
  listener.close((error) => (error ? reject(error) : resolve()));
144
215
  });
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { ErrorFactory } from "../utils/errorHandling.js";
3
3
  import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, } from "./rollingWorkerProtocol.js";
4
4
  export function spawnProxySocketWorker(options) {
5
- const socketAckTimeoutMs = Math.max(1, options.socketAckTimeoutMs ?? 5_000);
5
+ const socketAckTimeoutMs = Math.max(1, options.socketAckTimeoutMs ?? 30_000);
6
6
  let nextSocketId = 0;
7
7
  const pendingSockets = new Map();
8
8
  const statusListeners = new Set();
@@ -59,6 +59,18 @@ export function spawnProxySocketWorker(options) {
59
59
  }
60
60
  pendingSockets.delete(socketId);
61
61
  clearTimeout(pending.timeout);
62
+ if (error && child.connected && !pending.accepted) {
63
+ try {
64
+ child.send({
65
+ type: "proxy-worker:socket-cancel",
66
+ generation: options.generation,
67
+ socketId,
68
+ }, () => undefined);
69
+ }
70
+ catch {
71
+ // The supervisor will quarantine the worker after the failed transfer.
72
+ }
73
+ }
62
74
  if (!error) {
63
75
  pending.socket.destroy();
64
76
  }
@@ -92,9 +104,14 @@ export function spawnProxySocketWorker(options) {
92
104
  publishStatus(message);
93
105
  }
94
106
  });
95
- child.once("exit", () => {
107
+ child.once("exit", (code, signal) => {
96
108
  for (const socketId of [...pendingSockets.keys()]) {
97
- settleSocket(socketId, new Error(`proxy worker ${childPid} exited before accepting socket`));
109
+ settleSocket(socketId, ErrorFactory.proxyWorkerLifecycle(`proxy worker ${childPid} exited before socket transfer committed (code=${code ?? "none"}, signal=${signal ?? "none"})`, {
110
+ workerPid: childPid,
111
+ generation: options.generation,
112
+ exitCode: code,
113
+ signal,
114
+ }));
98
115
  }
99
116
  });
100
117
  const sendControl = (message) => {
@@ -117,18 +134,6 @@ export function spawnProxySocketWorker(options) {
117
134
  }
118
135
  const socketId = `${generation}:${++nextSocketId}`;
119
136
  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
137
  settleSocket(socketId, new Error(`proxy worker ${childPid} did not accept socket within ${socketAckTimeoutMs}ms`));
133
138
  }, socketAckTimeoutMs);
134
139
  timeout.unref?.();
@@ -38,6 +38,9 @@ export function isProxyWorkerStatusMessage(value) {
38
38
  if (message.type === "proxy-worker:socket-accepted") {
39
39
  return typeof message.socketId === "string" && message.socketId.length > 0;
40
40
  }
41
+ if (message.type === "proxy-worker:replacement-requested") {
42
+ return message.reason === "environment";
43
+ }
41
44
  return (message.type === "proxy-worker:fatal" && typeof message.message === "string");
42
45
  }
43
46
  //# sourceMappingURL=rollingWorkerProtocol.js.map
@@ -34,6 +34,7 @@ export declare class RollingWorkerSupervisor {
34
34
  private transferSocket;
35
35
  private handleTransferFailure;
36
36
  private rejectSocket;
37
+ private describeTransferError;
37
38
  private recordFailure;
38
39
  private publishState;
39
40
  }
@@ -79,7 +79,6 @@ export class RollingWorkerSupervisor {
79
79
  ? this.replacement
80
80
  : Promise.reject(ErrorFactory.proxyWorkerLifecycle(`worker replacement for v${this.candidate?.expectedVersion ?? "unknown"} is already in progress`, { requestedVersion: expectedVersion }));
81
81
  }
82
- this.lastFailure = null;
83
82
  this.replacement = this.spawnCandidate(expectedVersion).finally(() => {
84
83
  this.replacement = null;
85
84
  });
@@ -242,6 +241,16 @@ export class RollingWorkerSupervisor {
242
241
  }
243
242
  return;
244
243
  }
244
+ if (message.type === "proxy-worker:replacement-requested") {
245
+ if (this.active?.generation === generation) {
246
+ this.options.onReplacementRequested?.({
247
+ generation,
248
+ pid: handle.pid,
249
+ reason: message.reason,
250
+ });
251
+ }
252
+ return;
253
+ }
245
254
  if (message.type === "proxy-worker:ready") {
246
255
  if (message.version !== expectedVersion) {
247
256
  finish(new Error(`worker ${handle.pid} reported v${message.version}; expected v${expectedVersion}`));
@@ -309,7 +318,7 @@ export class RollingWorkerSupervisor {
309
318
  if (!this.closed) {
310
319
  this.recordFailure(generation, expectedVersion, "runtime", `worker exited (code=${code ?? "none"}, signal=${signal ?? "none"})`);
311
320
  }
312
- this.options.log?.(`[proxy-supervisor] active worker exited generation=${generation} pid=${handle.pid}`);
321
+ this.options.log?.(`[proxy-supervisor] active worker exited generation=${generation} pid=${handle.pid} code=${code ?? "none"} signal=${signal ?? "none"}`);
313
322
  }
314
323
  const drained = this.draining.get(generation);
315
324
  if (drained) {
@@ -356,23 +365,27 @@ export class RollingWorkerSupervisor {
356
365
  try {
357
366
  worker.handle.sendSocket(worker.generation, socket, (error) => {
358
367
  if (error) {
359
- this.handleTransferFailure(worker, socket);
368
+ this.handleTransferFailure(worker, socket, error);
360
369
  }
361
370
  });
362
371
  }
363
- catch {
364
- this.handleTransferFailure(worker, socket);
372
+ catch (error) {
373
+ this.handleTransferFailure(worker, socket, error);
365
374
  }
366
375
  }
367
- handleTransferFailure(worker, socket) {
376
+ handleTransferFailure(worker, socket, error) {
368
377
  this.failedTransfers += 1;
369
- this.recordFailure(worker.generation, worker.version, "transfer", `worker ${worker.handle.pid} failed to accept a transferred socket`);
378
+ const detail = this.describeTransferError(error);
379
+ this.recordFailure(worker.generation, worker.version, "transfer", `worker ${worker.handle.pid} failed to accept a transferred socket: ${detail}`);
380
+ this.options.log?.(`[proxy-supervisor] socket transfer failed generation=${worker.generation} pid=${worker.handle.pid}: ${detail}`);
370
381
  if (this.active?.generation === worker.generation && !this.closed) {
371
- // Do not let worker-side graceful shutdown call shutdown(2) on an
372
- // offered-but-uncommitted duplicate descriptor.
373
- worker.dispose();
374
- worker.handle.terminate("SIGKILL");
375
382
  this.active = null;
383
+ this.draining.set(worker.generation, worker);
384
+ // The child may own a duplicate of an incompletely transferred socket.
385
+ // SIGKILL closes its descriptor without worker-side shutdown(2), after
386
+ // which the parent rejects its copy rather than attempting unsafe replay.
387
+ worker.handle.terminate("SIGKILL");
388
+ this.publishState();
376
389
  }
377
390
  this.rejectSocket(socket);
378
391
  }
@@ -381,6 +394,13 @@ export class RollingWorkerSupervisor {
381
394
  socket.destroy();
382
395
  this.publishState();
383
396
  }
397
+ describeTransferError(error) {
398
+ if (!(error instanceof Error)) {
399
+ return String(error ?? "unknown transfer failure");
400
+ }
401
+ const code = error.code;
402
+ return code ? `${code}: ${error.message}` : error.message;
403
+ }
384
404
  recordFailure(generation, version, phase, message) {
385
405
  this.lastFailure = {
386
406
  at: new Date().toISOString(),
@@ -10,6 +10,7 @@ export declare class ProxyRuntimeConfigStore {
10
10
  private reloadTimer;
11
11
  private configFileObserved;
12
12
  private envFileObserved;
13
+ private currentEnvFileHash;
13
14
  private readonly watchListener;
14
15
  private constructor();
15
16
  static create(options: ProxyRuntimeConfigStoreOptions): Promise<ProxyRuntimeConfigStore>;
@@ -162,11 +162,13 @@ function assertResolvedRoutingValues(value, path = "routing") {
162
162
  async function buildCandidate(options, generation, allowMissingConfig, allowMissingEnvFile, rejectInvalidHotEnv) {
163
163
  const effectiveEnv = { ...options.baseEnv };
164
164
  let envFilePresent = false;
165
+ let envFileValues = {};
165
166
  if (options.envFilePath) {
166
167
  try {
167
- const envContent = await readFile(options.envFilePath, "utf8");
168
+ const envFileContent = await readFile(options.envFilePath, "utf8");
168
169
  const { parse } = await import("dotenv");
169
- Object.assign(effectiveEnv, parse(envContent));
170
+ envFileValues = parse(envFileContent);
171
+ Object.assign(effectiveEnv, envFileValues);
170
172
  envFilePresent = true;
171
173
  }
172
174
  catch (error) {
@@ -218,6 +220,12 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
218
220
  accountAllowlist: routing.accountAllowlist,
219
221
  })
220
222
  : undefined;
223
+ const envFileHash = createHash("sha256")
224
+ .update(envFilePresent
225
+ ? JSON.stringify(Object.entries(envFileValues).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0))
226
+ : "[missing]")
227
+ .digest("hex")
228
+ .slice(0, 16);
221
229
  const fingerprintSource = JSON.stringify({
222
230
  strategy,
223
231
  passthrough: options.passthrough,
@@ -249,6 +257,7 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
249
257
  }),
250
258
  configFilePresent,
251
259
  envFilePresent,
260
+ envFileHash,
252
261
  };
253
262
  }
254
263
  /** Atomic last-known-good runtime configuration with file-triggered reloads. */
@@ -262,6 +271,7 @@ export class ProxyRuntimeConfigStore {
262
271
  reloadTimer;
263
272
  configFileObserved;
264
273
  envFileObserved;
274
+ currentEnvFileHash;
265
275
  watchListener = (current, previous) => {
266
276
  if (current.mtimeMs === previous.mtimeMs &&
267
277
  current.size === previous.size &&
@@ -270,11 +280,12 @@ export class ProxyRuntimeConfigStore {
270
280
  }
271
281
  this.scheduleWatchReload();
272
282
  };
273
- constructor(options, snapshot, configFileObserved, envFileObserved) {
283
+ constructor(options, snapshot, configFileObserved, envFileObserved, envFileHash) {
274
284
  this.options = { ...options, baseEnv: { ...options.baseEnv } };
275
285
  this.currentSnapshot = snapshot;
276
286
  this.configFileObserved = configFileObserved;
277
287
  this.envFileObserved = envFileObserved;
288
+ this.currentEnvFileHash = envFileHash;
278
289
  this.status = {
279
290
  configPath: options.configPath,
280
291
  ...(options.envFilePath ? { envFilePath: options.envFilePath } : {}),
@@ -287,7 +298,7 @@ export class ProxyRuntimeConfigStore {
287
298
  }
288
299
  static async create(options) {
289
300
  const candidate = await buildCandidate(options, 1, true, true, false);
290
- return new ProxyRuntimeConfigStore(options, candidate.snapshot, candidate.configFilePresent, candidate.envFilePresent);
301
+ return new ProxyRuntimeConfigStore(options, candidate.snapshot, candidate.configFilePresent, candidate.envFilePresent, candidate.envFileHash);
291
302
  }
292
303
  getSnapshot() {
293
304
  return this.currentSnapshot;
@@ -354,7 +365,8 @@ export class ProxyRuntimeConfigStore {
354
365
  const candidate = await buildCandidate(this.options, this.currentSnapshot.generation + 1, !this.configFileObserved, !this.envFileObserved, true);
355
366
  this.configFileObserved ||= candidate.configFilePresent;
356
367
  this.envFileObserved ||= candidate.envFilePresent;
357
- if (candidate.snapshot.configHash === this.currentSnapshot.configHash) {
368
+ if (candidate.snapshot.configHash === this.currentSnapshot.configHash &&
369
+ candidate.envFileHash === this.currentEnvFileHash) {
358
370
  this.status = {
359
371
  ...this.status,
360
372
  lastReloadAt: attemptedAt,
@@ -369,7 +381,9 @@ export class ProxyRuntimeConfigStore {
369
381
  this.notifyReloadListeners(result);
370
382
  return result;
371
383
  }
384
+ const environmentChanged = candidate.envFileHash !== this.currentEnvFileHash;
372
385
  this.currentSnapshot = candidate.snapshot;
386
+ this.currentEnvFileHash = candidate.envFileHash;
373
387
  this.status = {
374
388
  ...this.status,
375
389
  generation: candidate.snapshot.generation,
@@ -392,6 +406,7 @@ export class ProxyRuntimeConfigStore {
392
406
  applied: true,
393
407
  changed: true,
394
408
  generation: candidate.snapshot.generation,
409
+ ...(environmentChanged ? { environmentChanged: true } : {}),
395
410
  };
396
411
  this.notifyReloadListeners(result);
397
412
  return result;
@@ -8,6 +8,7 @@ function isTransferableProxySocket(handle) {
8
8
  typeof candidate.resume === "function" &&
9
9
  typeof candidate.destroy === "function" &&
10
10
  typeof candidate.end === "function" &&
11
+ typeof candidate.off === "function" &&
11
12
  typeof candidate.once === "function");
12
13
  }
13
14
  function destroyTransferredHandle(handle) {
@@ -153,6 +154,16 @@ export function attachSocketWorkerProcess(server, input) {
153
154
  // synchronously here would truncate that in-flight request.
154
155
  setImmediate(() => runtime.drain());
155
156
  };
157
+ const takePendingSocket = (socketId) => {
158
+ const pending = pendingSockets.get(socketId);
159
+ if (!pending) {
160
+ return undefined;
161
+ }
162
+ pendingSockets.delete(socketId);
163
+ pending.socket.off("error", pending.onError);
164
+ pending.socket.off("close", pending.onClose);
165
+ return pending.socket;
166
+ };
156
167
  const onMessage = (message, handle) => {
157
168
  if (message &&
158
169
  typeof message === "object" &&
@@ -176,7 +187,27 @@ export function attachSocketWorkerProcess(server, input) {
176
187
  socket.destroy();
177
188
  return;
178
189
  }
179
- pendingSockets.set(socketId, socket);
190
+ // A client can reset while the transferred handle is paused between
191
+ // acceptance and commit. The HTTP server has not seen the socket yet,
192
+ // so it cannot install its normal transport-error handler for us.
193
+ const onError = () => {
194
+ if (pendingSockets.get(socketId)?.socket !== socket) {
195
+ return;
196
+ }
197
+ takePendingSocket(socketId);
198
+ socket.destroy();
199
+ drainWhenPendingSettled();
200
+ };
201
+ const onClose = () => {
202
+ if (pendingSockets.get(socketId)?.socket !== socket) {
203
+ return;
204
+ }
205
+ takePendingSocket(socketId);
206
+ drainWhenPendingSettled();
207
+ };
208
+ pendingSockets.set(socketId, { socket, onError, onClose });
209
+ socket.once("error", onError);
210
+ socket.once("close", onClose);
180
211
  try {
181
212
  process.send({
182
213
  type: "proxy-worker:socket-accepted",
@@ -185,14 +216,14 @@ export function attachSocketWorkerProcess(server, input) {
185
216
  socketId,
186
217
  }, (error) => {
187
218
  if (error) {
188
- pendingSockets.delete(socketId);
189
- socket.destroy();
219
+ takePendingSocket(socketId)?.destroy();
220
+ drainWhenPendingSettled();
190
221
  }
191
222
  });
192
223
  }
193
224
  catch {
194
- pendingSockets.delete(socketId);
195
- socket.destroy();
225
+ takePendingSocket(socketId)?.destroy();
226
+ drainWhenPendingSettled();
196
227
  }
197
228
  }
198
229
  else {
@@ -229,11 +260,10 @@ export function attachSocketWorkerProcess(server, input) {
229
260
  }
230
261
  else if (message.type === "proxy-worker:socket-commit" ||
231
262
  message.type === "proxy-worker:socket-cancel") {
232
- const socket = pendingSockets.get(message.socketId);
263
+ const socket = takePendingSocket(message.socketId);
233
264
  if (!socket) {
234
265
  return;
235
266
  }
236
- pendingSockets.delete(message.socketId);
237
267
  if (message.type === "proxy-worker:socket-commit") {
238
268
  runtime.acceptSocket(socket);
239
269
  }
@@ -259,10 +289,9 @@ export function attachSocketWorkerProcess(server, input) {
259
289
  // is exiting. The zero-downtime guarantee applies to the rolling handoff
260
290
  // (control-message) path, not to process termination.
261
291
  const drain = () => {
262
- for (const socket of pendingSockets.values()) {
263
- socket.destroy();
292
+ for (const socketId of [...pendingSockets.keys()]) {
293
+ takePendingSocket(socketId)?.destroy();
264
294
  }
265
- pendingSockets.clear();
266
295
  runtime.drain();
267
296
  };
268
297
  const onTerminationSignal = () => drain();
@@ -283,10 +312,9 @@ export function attachSocketWorkerProcess(server, input) {
283
312
  process.off("disconnect", drain);
284
313
  process.off("SIGTERM", onTerminationSignal);
285
314
  process.off("SIGINT", onTerminationSignal);
286
- for (const socket of pendingSockets.values()) {
287
- socket.destroy();
315
+ for (const socketId of [...pendingSockets.keys()]) {
316
+ takePendingSocket(socketId)?.destroy();
288
317
  }
289
- pendingSockets.clear();
290
318
  runtime.close();
291
319
  },
292
320
  };
@@ -429,7 +429,11 @@ export function createSSEInterceptor(options = {}) {
429
429
  // Wrap the writable side so we can intercept abort() — which does NOT
430
430
  // trigger the TransformStream's flush() or cancel() callbacks.
431
431
  const innerWriter = transform.writable.getWriter();
432
+ let writableController;
432
433
  const writable = new WritableStream({
434
+ start(controller) {
435
+ writableController = controller;
436
+ },
433
437
  write(chunk) {
434
438
  return innerWriter.write(chunk);
435
439
  },
@@ -441,8 +445,50 @@ export function createSSEInterceptor(options = {}) {
441
445
  return innerWriter.abort(reason);
442
446
  },
443
447
  });
448
+ // Preserve downstream cancellation across the wrapped writable. Without
449
+ // this bridge, client disconnects leave the upstream source and telemetry
450
+ // promise open indefinitely after the readable side is cancelled.
451
+ void innerWriter.closed.catch((reason) => {
452
+ settle();
453
+ try {
454
+ writableController.error(reason);
455
+ }
456
+ catch {
457
+ // The wrapper may already be closing or aborted.
458
+ }
459
+ });
460
+ const innerReader = transform.readable.getReader();
461
+ const readable = new ReadableStream({
462
+ async pull(controller) {
463
+ try {
464
+ const { done, value } = await innerReader.read();
465
+ if (done) {
466
+ controller.close();
467
+ }
468
+ else {
469
+ controller.enqueue(value);
470
+ }
471
+ }
472
+ catch (error) {
473
+ controller.error(error);
474
+ }
475
+ },
476
+ async cancel(reason) {
477
+ settle();
478
+ try {
479
+ writableController.error(reason);
480
+ }
481
+ catch {
482
+ // The wrapper may already be closing or aborted.
483
+ }
484
+ await Promise.allSettled([
485
+ innerReader.cancel(reason),
486
+ innerWriter.abort(reason),
487
+ ]);
488
+ },
489
+ });
444
490
  const stream = {
445
- readable: transform.readable,
491
+ readable,
446
492
  writable,
447
493
  };
448
494
  return { stream, telemetry: telemetryPromise };
@@ -8,5 +8,11 @@ export declare function isPermanentRefreshFailure(result: RefreshResult): boolea
8
8
  * loaded either the old token or the newly rotated token around persistence.
9
9
  */
10
10
  export declare function refreshToken(account: RefreshableAccount): Promise<RefreshResult>;
11
+ /**
12
+ * Refreshes against the latest persisted credential generation. Queued
13
+ * requests can otherwise reject a rotated refresh token and disable a newer
14
+ * login that was saved while they were waiting.
15
+ */
16
+ export declare function refreshTokenFromLatest(account: RefreshableAccount, target?: TokenPersistTarget): Promise<RefreshResult>;
11
17
  export declare function clearRefreshStateForTests(): void;
12
18
  export declare function persistTokens(target: TokenPersistTarget, account: RefreshableAccount): Promise<void>;