@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.
- package/CHANGELOG.md +12 -0
- package/dist/auth/tokenStore.d.ts +7 -0
- package/dist/auth/tokenStore.js +54 -0
- package/dist/browser/neurolink.min.js +377 -380
- package/dist/cli/commands/proxy.d.ts +2 -1
- package/dist/cli/commands/proxy.js +290 -63
- package/dist/cli/commands/proxyAnalyze.js +10 -1
- package/dist/lib/auth/tokenStore.d.ts +7 -0
- package/dist/lib/auth/tokenStore.js +54 -0
- package/dist/lib/proxy/proxyAnalysis.js +136 -12
- package/dist/lib/proxy/proxyTranslationEngine.d.ts +1 -0
- package/dist/lib/proxy/proxyTranslationEngine.js +56 -12
- package/dist/lib/proxy/rawStreamCapture.js +47 -1
- package/dist/lib/proxy/requestLogger.d.ts +2 -0
- package/dist/lib/proxy/requestLogger.js +72 -13
- package/dist/lib/proxy/rollingProxyServer.js +79 -8
- package/dist/lib/proxy/rollingWorkerProcess.js +20 -15
- package/dist/lib/proxy/rollingWorkerProtocol.js +3 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/lib/proxy/rollingWorkerSupervisor.js +31 -11
- package/dist/lib/proxy/runtimeConfig.d.ts +1 -0
- package/dist/lib/proxy/runtimeConfig.js +20 -5
- package/dist/lib/proxy/socketWorkerRuntime.js +41 -13
- package/dist/lib/proxy/sseInterceptor.js +47 -1
- package/dist/lib/proxy/tokenRefresh.d.ts +6 -0
- package/dist/lib/proxy/tokenRefresh.js +70 -15
- package/dist/lib/proxy/usageStats.d.ts +25 -3
- package/dist/lib/proxy/usageStats.js +546 -55
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/lib/server/routes/claudeProxyRoutes.js +261 -297
- package/dist/lib/types/proxy.d.ts +90 -1
- package/dist/proxy/proxyAnalysis.js +136 -12
- package/dist/proxy/proxyTranslationEngine.d.ts +1 -0
- package/dist/proxy/proxyTranslationEngine.js +56 -12
- package/dist/proxy/rawStreamCapture.js +47 -1
- package/dist/proxy/requestLogger.d.ts +2 -0
- package/dist/proxy/requestLogger.js +72 -13
- package/dist/proxy/rollingProxyServer.js +79 -8
- package/dist/proxy/rollingWorkerProcess.js +20 -15
- package/dist/proxy/rollingWorkerProtocol.js +3 -0
- package/dist/proxy/rollingWorkerSupervisor.d.ts +1 -0
- package/dist/proxy/rollingWorkerSupervisor.js +31 -11
- package/dist/proxy/runtimeConfig.d.ts +1 -0
- package/dist/proxy/runtimeConfig.js +20 -5
- package/dist/proxy/socketWorkerRuntime.js +41 -13
- package/dist/proxy/sseInterceptor.js +47 -1
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +70 -15
- package/dist/proxy/usageStats.d.ts +25 -3
- package/dist/proxy/usageStats.js +546 -55
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/server/routes/claudeProxyRoutes.js +261 -297
- package/dist/types/proxy.d.ts +90 -1
- 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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
+
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,
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
168
|
+
const envFileContent = await readFile(options.envFilePath, "utf8");
|
|
168
169
|
const { parse } = await import("dotenv");
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
219
|
+
takePendingSocket(socketId)?.destroy();
|
|
220
|
+
drainWhenPendingSettled();
|
|
190
221
|
}
|
|
191
222
|
});
|
|
192
223
|
}
|
|
193
224
|
catch {
|
|
194
|
-
|
|
195
|
-
|
|
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 =
|
|
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
|
|
263
|
-
|
|
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
|
|
287
|
-
|
|
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
|
|
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>;
|