@juspay/neurolink 12.17.1 → 12.17.3
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 +6 -2
- package/dist/browser/neurolink.min.js +430 -421
- package/dist/cli/commands/proxy.js +455 -259
- package/dist/constants/contextWindows.d.ts +2 -0
- package/dist/constants/contextWindows.js +4 -0
- package/dist/neurolink.js +8 -3
- package/dist/providers/openaiChatCompletionsBase.js +60 -51
- package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
- package/dist/proxy/anthropicBudgetResponse.js +153 -0
- package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
- package/dist/proxy/bodyCaptureProcessing.js +23 -4
- package/dist/proxy/bodyCaptureWorker.js +20 -2
- package/dist/proxy/codexFallback.d.ts +10 -0
- package/dist/proxy/codexFallback.js +108 -24
- package/dist/proxy/codexUsage.js +2 -0
- package/dist/proxy/globalInstaller.d.ts +32 -1
- package/dist/proxy/globalInstaller.js +386 -3
- package/dist/proxy/otelLogSink.d.ts +3 -3
- package/dist/proxy/otelLogSink.js +137 -144
- package/dist/proxy/otlpLogTransport.d.ts +3 -0
- package/dist/proxy/otlpLogTransport.js +334 -0
- package/dist/proxy/proxyActivity.d.ts +22 -2
- package/dist/proxy/proxyActivity.js +114 -9
- package/dist/proxy/proxyAnalysis.js +82 -52
- package/dist/proxy/proxyClientCancellation.d.ts +7 -0
- package/dist/proxy/proxyClientCancellation.js +23 -0
- package/dist/proxy/proxyContextPreflight.d.ts +25 -0
- package/dist/proxy/proxyContextPreflight.js +337 -0
- package/dist/proxy/proxyFailureDetails.d.ts +19 -0
- package/dist/proxy/proxyFailureDetails.js +96 -0
- package/dist/proxy/proxyLifecycle.js +7 -0
- package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
- package/dist/proxy/proxyRouteBodyCapture.js +112 -0
- package/dist/proxy/proxyTokenBudget.d.ts +40 -0
- package/dist/proxy/proxyTokenBudget.js +523 -0
- package/dist/proxy/proxyTracer.d.ts +3 -2
- package/dist/proxy/proxyTracer.js +46 -20
- package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
- package/dist/proxy/proxyTranslationEngine.js +736 -376
- package/dist/proxy/requestLogger.d.ts +1 -0
- package/dist/proxy/requestLogger.js +107 -5
- package/dist/proxy/restartControl.js +9 -4
- package/dist/proxy/rollingWorkerProcess.js +65 -0
- package/dist/proxy/rollingWorkerSupervisor.js +7 -0
- package/dist/proxy/updateCoordinator.d.ts +14 -1
- package/dist/proxy/updateCoordinator.js +57 -2
- package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
- package/dist/server/routes/claudeProxyRoutes.js +809 -577
- package/dist/server/routes/codexProxyRoutes.js +191 -9
- package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
- package/dist/server/routes/openaiProxyRoutes.js +261 -69
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/codex.d.ts +5 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/proxy.d.ts +148 -3
- package/dist/types/proxy.js +0 -15
- package/dist/types/proxyBudget.d.ts +74 -0
- package/dist/types/proxyBudget.js +1 -0
- package/dist/types/proxyContext.d.ts +35 -0
- package/dist/types/proxyContext.js +1 -0
- package/dist/utils/fileDetector.js +28 -7
- package/dist/utils/messageBuilder.js +51 -26
- package/dist/utils/pdfProcessor.js +26 -4
- package/docs/assets/dashboards/README.md +37 -0
- package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
- package/docs-site/static/search-index.json +1 -1
- package/package.json +7 -2
- package/scripts/observability/NATIVE-DURABLE.md +63 -0
- package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
- package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
- package/scripts/observability/proxy-telemetry-check.mjs +396 -61
- package/scripts/observability/query-proxy-history.mjs +34 -5
|
@@ -7,18 +7,88 @@
|
|
|
7
7
|
* rules as a native Codex request.
|
|
8
8
|
*/
|
|
9
9
|
import { ClaudeStreamSerializer, generateToolUseId } from "./claudeFormat.js";
|
|
10
|
+
import { classifyProxyFailureCode } from "./proxyFailureDetails.js";
|
|
10
11
|
import { extractCodexUsage } from "./codexUsage.js";
|
|
11
12
|
import { proxyTokenUsage } from "./proxyTokenUsage.js";
|
|
13
|
+
import { sanitizeForLog } from "../utils/logSanitize.js";
|
|
12
14
|
export class CodexFallbackResponseError extends Error {
|
|
13
15
|
status;
|
|
14
16
|
responseBody;
|
|
17
|
+
code;
|
|
18
|
+
retryable;
|
|
15
19
|
constructor(status, responseBody) {
|
|
16
20
|
super(`Codex fallback request returned HTTP ${status}`);
|
|
17
21
|
this.name = "CodexFallbackResponseError";
|
|
18
22
|
this.status = status;
|
|
19
23
|
this.responseBody = responseBody;
|
|
24
|
+
try {
|
|
25
|
+
const payload = JSON.parse(responseBody);
|
|
26
|
+
if (isRecord(payload)) {
|
|
27
|
+
const detail = streamFailureDetails(payload, "http_error");
|
|
28
|
+
this.code = detail.code;
|
|
29
|
+
this.retryable = detail.retryable;
|
|
30
|
+
if (detail.message) {
|
|
31
|
+
this.message = detail.message;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Non-JSON upstream responses retain the HTTP status without exposing HTML.
|
|
37
|
+
}
|
|
20
38
|
}
|
|
21
39
|
}
|
|
40
|
+
/** A provider terminal event, preserved through the format adapter. */
|
|
41
|
+
export class CodexFallbackStreamError extends Error {
|
|
42
|
+
status;
|
|
43
|
+
code;
|
|
44
|
+
retryable;
|
|
45
|
+
usage;
|
|
46
|
+
constructor(payload, eventType, usage) {
|
|
47
|
+
const detail = streamFailureDetails(payload, eventType);
|
|
48
|
+
super(detail.message ?? `Codex fallback stream terminated with ${eventType}`);
|
|
49
|
+
this.name = "CodexFallbackStreamError";
|
|
50
|
+
this.code = detail.code;
|
|
51
|
+
this.status = detail.status;
|
|
52
|
+
this.retryable = detail.retryable;
|
|
53
|
+
const observed = extractCodexUsage(payload);
|
|
54
|
+
this.usage = observed
|
|
55
|
+
? {
|
|
56
|
+
...proxyTokenUsage({
|
|
57
|
+
inputTokens: observed.inputTokens,
|
|
58
|
+
outputTokens: observed.outputTokens,
|
|
59
|
+
reasoningTokens: observed.reasoningTokensObserved
|
|
60
|
+
? observed.reasoningTokens
|
|
61
|
+
: undefined,
|
|
62
|
+
cacheReadTokens: observed.cacheReadTokens,
|
|
63
|
+
cacheCreationTokens: observed.cacheCreationTokens,
|
|
64
|
+
inputIncludesCachedTokens: true,
|
|
65
|
+
}),
|
|
66
|
+
inputTokensObserved: observed.inputTokensObserved,
|
|
67
|
+
outputTokensObserved: observed.outputTokensObserved,
|
|
68
|
+
}
|
|
69
|
+
: usage;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function streamFailureDetails(payload, eventType) {
|
|
73
|
+
const response = isRecord(payload.response) ? payload.response : payload;
|
|
74
|
+
const error = isRecord(response.error) ? response.error : response;
|
|
75
|
+
const incomplete = isRecord(response.incomplete_details)
|
|
76
|
+
? response.incomplete_details
|
|
77
|
+
: {};
|
|
78
|
+
const code = sanitizeForLog(asNonEmptyString(error.code) ??
|
|
79
|
+
asNonEmptyString(incomplete.reason) ??
|
|
80
|
+
eventType, 200);
|
|
81
|
+
const known = classifyProxyFailureCode(code);
|
|
82
|
+
return {
|
|
83
|
+
code,
|
|
84
|
+
status: known.status ?? 502,
|
|
85
|
+
retryable: known.retryable ??
|
|
86
|
+
(typeof error.retryable === "boolean" ? error.retryable : undefined),
|
|
87
|
+
message: typeof error.message === "string"
|
|
88
|
+
? sanitizeForLog(error.message, 500)
|
|
89
|
+
: undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
22
92
|
function isRecord(value) {
|
|
23
93
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
24
94
|
}
|
|
@@ -338,7 +408,7 @@ export function parseCodexFallbackSSE(sse) {
|
|
|
338
408
|
if (type === "error" ||
|
|
339
409
|
type === "response.failed" ||
|
|
340
410
|
type === "response.incomplete") {
|
|
341
|
-
throw new
|
|
411
|
+
throw new CodexFallbackStreamError(payload, type, usage);
|
|
342
412
|
}
|
|
343
413
|
if (type === "response.output_text.delta") {
|
|
344
414
|
if (typeof payload.delta !== "string") {
|
|
@@ -368,16 +438,20 @@ export function parseCodexFallbackSSE(sse) {
|
|
|
368
438
|
}
|
|
369
439
|
const parsedUsage = extractCodexUsage(payload);
|
|
370
440
|
if (parsedUsage) {
|
|
371
|
-
usage =
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
441
|
+
usage = {
|
|
442
|
+
...proxyTokenUsage({
|
|
443
|
+
inputTokens: parsedUsage.inputTokens,
|
|
444
|
+
outputTokens: parsedUsage.outputTokens,
|
|
445
|
+
reasoningTokens: parsedUsage.reasoningTokensObserved
|
|
446
|
+
? parsedUsage.reasoningTokens
|
|
447
|
+
: undefined,
|
|
448
|
+
cacheReadTokens: parsedUsage.cacheReadTokens,
|
|
449
|
+
cacheCreationTokens: parsedUsage.cacheCreationTokens,
|
|
450
|
+
inputIncludesCachedTokens: true,
|
|
451
|
+
}),
|
|
452
|
+
inputTokensObserved: parsedUsage.inputTokensObserved,
|
|
453
|
+
outputTokensObserved: parsedUsage.outputTokensObserved,
|
|
454
|
+
};
|
|
381
455
|
}
|
|
382
456
|
textFromResponse = outputTextFromResponse(payload);
|
|
383
457
|
}
|
|
@@ -387,7 +461,10 @@ export function parseCodexFallbackSSE(sse) {
|
|
|
387
461
|
const text = textFromDeltas || textFromCompletedItems || textFromResponse;
|
|
388
462
|
const resolvedToolCalls = [...toolCalls.values()];
|
|
389
463
|
if (!text && resolvedToolCalls.length === 0) {
|
|
390
|
-
throw new
|
|
464
|
+
throw new CodexFallbackStreamError({
|
|
465
|
+
code: "empty_response",
|
|
466
|
+
message: "Codex fallback returned no content or tool calls",
|
|
467
|
+
}, "empty_response", usage);
|
|
391
468
|
}
|
|
392
469
|
return {
|
|
393
470
|
text,
|
|
@@ -498,7 +575,7 @@ export async function createCodexFallbackStream(response, model) {
|
|
|
498
575
|
if (type === "error" ||
|
|
499
576
|
type === "response.failed" ||
|
|
500
577
|
type === "response.incomplete") {
|
|
501
|
-
throw new
|
|
578
|
+
throw new CodexFallbackStreamError(payload, type, usage);
|
|
502
579
|
}
|
|
503
580
|
const index = typeof payload.output_index === "number" ? payload.output_index : 0;
|
|
504
581
|
if (type === "response.output_text.delta") {
|
|
@@ -520,16 +597,20 @@ export async function createCodexFallbackStream(response, model) {
|
|
|
520
597
|
completed = true;
|
|
521
598
|
const parsedUsage = extractCodexUsage(payload);
|
|
522
599
|
if (parsedUsage) {
|
|
523
|
-
usage =
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
600
|
+
usage = {
|
|
601
|
+
...proxyTokenUsage({
|
|
602
|
+
inputTokens: parsedUsage.inputTokens,
|
|
603
|
+
outputTokens: parsedUsage.outputTokens,
|
|
604
|
+
reasoningTokens: parsedUsage.reasoningTokensObserved
|
|
605
|
+
? parsedUsage.reasoningTokens
|
|
606
|
+
: undefined,
|
|
607
|
+
cacheReadTokens: parsedUsage.cacheReadTokens,
|
|
608
|
+
cacheCreationTokens: parsedUsage.cacheCreationTokens,
|
|
609
|
+
inputIncludesCachedTokens: true,
|
|
610
|
+
}),
|
|
611
|
+
inputTokensObserved: parsedUsage.inputTokensObserved,
|
|
612
|
+
outputTokensObserved: parsedUsage.outputTokensObserved,
|
|
613
|
+
};
|
|
533
614
|
}
|
|
534
615
|
const responseBody = payload.response;
|
|
535
616
|
if (isRecord(responseBody) && Array.isArray(responseBody.output)) {
|
|
@@ -573,7 +654,10 @@ export async function createCodexFallbackStream(response, model) {
|
|
|
573
654
|
throw new Error("Codex fallback stream ended before response.completed");
|
|
574
655
|
}
|
|
575
656
|
if (textLength === 0 && toolCalls.size === 0) {
|
|
576
|
-
throw new
|
|
657
|
+
throw new CodexFallbackStreamError({
|
|
658
|
+
code: "empty_response",
|
|
659
|
+
message: "Codex fallback returned no content or tool calls",
|
|
660
|
+
}, "empty_response", usage);
|
|
577
661
|
}
|
|
578
662
|
const finishReason = toolCalls.size > 0 ? "tool_use" : "end_turn";
|
|
579
663
|
yield* serializer.finish(usage?.output, finishReason, {
|
package/dist/proxy/codexUsage.js
CHANGED
|
@@ -104,6 +104,8 @@ export function extractCodexUsage(payload) {
|
|
|
104
104
|
return {
|
|
105
105
|
inputTokens: nonNegativeInt(input),
|
|
106
106
|
outputTokens: nonNegativeInt(output),
|
|
107
|
+
inputTokensObserved: typeof input === "number" && Number.isFinite(input) && input >= 0,
|
|
108
|
+
outputTokensObserved: typeof output === "number" && Number.isFinite(output) && output >= 0,
|
|
107
109
|
cacheReadTokens: nonNegativeInt(inputDetails?.cached_tokens),
|
|
108
110
|
cacheCreationTokens: nonNegativeInt(inputDetails?.cache_write_tokens),
|
|
109
111
|
reasoningTokens: nonNegativeInt(outputDetails?.reasoning_tokens),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GlobalInstallerKind, GlobalInstallerResolution, InstalledVersionValidation, ResolveGlobalInstallerOptions, ValidateInstalledVersionOptions } from "../types/index.js";
|
|
1
|
+
import type { ProxyServiceInstallSettings, ProxyPackageSelection, ProxyStagedInstallOptions, GlobalInstallerExecFile, GlobalInstallerKind, GlobalInstallerResolution, InstalledVersionValidation, ResolveGlobalInstallerOptions, ValidateInstalledVersionOptions } from "../types/index.js";
|
|
2
2
|
/** Resolve a package manager that can update the installation currently running. */
|
|
3
3
|
export declare function resolveGlobalInstaller(options?: ResolveGlobalInstallerOptions): GlobalInstallerResolution;
|
|
4
4
|
export declare function getGlobalInstallArgs(kind: GlobalInstallerKind, packageSpec: string): string[];
|
|
@@ -17,3 +17,34 @@ export declare function isTransientInstallFailure(error: unknown): boolean;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function validateInstalledVersion(options: ValidateInstalledVersionOptions): Promise<InstalledVersionValidation>;
|
|
19
19
|
export declare function describeInstallFailure(error: unknown): string;
|
|
20
|
+
/** Read package metadata without importing the CLI/provider/telemetry graph. */
|
|
21
|
+
export declare function inspectProxyPackage(entryScript: string, nodePath?: string): ProxyPackageSelection;
|
|
22
|
+
export declare function readProxyPackageSelection(packagesDir: string, previous?: boolean): ProxyPackageSelection | null;
|
|
23
|
+
/** Resolve immutable selections before consulting an older mutable global entry. */
|
|
24
|
+
export declare function resolveProxyWorkerPackage(options: {
|
|
25
|
+
packagesDir: string;
|
|
26
|
+
entryScript: string;
|
|
27
|
+
expectedVersion: string;
|
|
28
|
+
}): ProxyPackageSelection;
|
|
29
|
+
/** Persist only package selection; runtime env, config and launchd settings are untouched. */
|
|
30
|
+
export declare function selectProxyPackage(packagesDir: string, selection: ProxyPackageSelection, isCurrentOwner?: () => boolean): void;
|
|
31
|
+
/** No startup probes: IPC descriptors and OTel initialization belong to the real process. */
|
|
32
|
+
export declare function writeProxyPackageLauncher(path: string, selection: ProxyPackageSelection, isCurrentOwner?: () => boolean): void;
|
|
33
|
+
/** Install in a private tree, then publish the immutable directory after validation. */
|
|
34
|
+
export declare function installStagedProxyPackage(options: ProxyStagedInstallOptions): Promise<ProxyPackageSelection>;
|
|
35
|
+
/** Only operator runtime settings survive reinstall, never worker IPC identity. */
|
|
36
|
+
export declare function isProxyServiceEnvironmentKey(name: string): boolean;
|
|
37
|
+
/** Parse plutil JSON without exposing credentials or invoking the saved launcher. */
|
|
38
|
+
export declare function parseProxyServiceInstallSettings(value: unknown): ProxyServiceInstallSettings;
|
|
39
|
+
/** Distinguish an absent launchd job from permission failures without logging plist/env data. */
|
|
40
|
+
export declare function probeProxyLaunchdPresence(options: {
|
|
41
|
+
label: string;
|
|
42
|
+
uid: number;
|
|
43
|
+
execFileSync?: GlobalInstallerExecFile;
|
|
44
|
+
}): "loaded" | "not_loaded" | "unknown";
|
|
45
|
+
/** Read-only guard: callers must run this before every reinstall mutation. */
|
|
46
|
+
export declare function assertProxyServiceInstallIdle(options: {
|
|
47
|
+
pids: number[];
|
|
48
|
+
getProcessStatus: (pid: number) => "running" | "not_running" | "unknown";
|
|
49
|
+
launchdPresence: "loaded" | "not_loaded" | "unknown";
|
|
50
|
+
}): void;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { execFileSync as nodeExecFileSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, existsSync, realpathSync } from "node:fs";
|
|
1
|
+
import { execFileSync as nodeExecFileSync, spawn as nodeSpawn, } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, existsSync, realpathSync, readFileSync, mkdirSync, writeFileSync, renameSync, chmodSync, rmSync, } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, resolve, relative, } from "node:path";
|
|
5
7
|
function runText(execFileSync, bin, args) {
|
|
6
8
|
return String(execFileSync(bin, args, {
|
|
7
9
|
encoding: "utf8",
|
|
@@ -246,3 +248,384 @@ export function describeInstallFailure(error) {
|
|
|
246
248
|
.filter(Boolean)
|
|
247
249
|
.join("\n");
|
|
248
250
|
}
|
|
251
|
+
/** Read package metadata without importing the CLI/provider/telemetry graph. */
|
|
252
|
+
export function inspectProxyPackage(entryScript, nodePath = process.execPath) {
|
|
253
|
+
const entry = realpathSync(entryScript);
|
|
254
|
+
let directory = dirname(entry);
|
|
255
|
+
while (true) {
|
|
256
|
+
const manifestPath = join(directory, "package.json");
|
|
257
|
+
if (existsSync(manifestPath)) {
|
|
258
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
259
|
+
if (manifest.name === "@juspay/neurolink") {
|
|
260
|
+
if (typeof manifest.version !== "string" ||
|
|
261
|
+
!/^\d+\.\d+\.\d+$/.test(manifest.version)) {
|
|
262
|
+
throw new Error("Invalid proxy package version");
|
|
263
|
+
}
|
|
264
|
+
return { version: manifest.version, entryScript: entry, nodePath };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const parent = dirname(directory);
|
|
268
|
+
if (parent === directory) {
|
|
269
|
+
throw new Error("Proxy entrypoint has no package manifest");
|
|
270
|
+
}
|
|
271
|
+
directory = parent;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function validateProxyPackageSelection(value) {
|
|
275
|
+
try {
|
|
276
|
+
if (!value || typeof value !== "object") {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const selection = value;
|
|
280
|
+
if (typeof selection.entryScript !== "string" ||
|
|
281
|
+
typeof selection.nodePath !== "string" ||
|
|
282
|
+
!isAbsolute(selection.entryScript) ||
|
|
283
|
+
!isAbsolute(selection.nodePath)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const actual = inspectProxyPackage(selection.entryScript, selection.nodePath);
|
|
287
|
+
return actual.version === selection.version ? actual : null;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/** Read active and rollback selections from one committed generation. */
|
|
294
|
+
function readProxyPackageSelections(packagesDir) {
|
|
295
|
+
let value;
|
|
296
|
+
try {
|
|
297
|
+
value = JSON.parse(readFileSync(join(packagesDir, "selections.json"), "utf8"));
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
// Legacy migration is allowed only when no atomic record has ever been
|
|
301
|
+
// published. Invalid/unreadable committed state must not revive stale files.
|
|
302
|
+
if (error.code !== "ENOENT") {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const readLegacy = (file) => {
|
|
306
|
+
try {
|
|
307
|
+
return validateProxyPackageSelection(JSON.parse(readFileSync(join(packagesDir, file), "utf8")));
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
schemaVersion: 1,
|
|
315
|
+
active: readLegacy("active.json"),
|
|
316
|
+
previous: readLegacy("previous.json"),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (!value || typeof value !== "object") {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const state = value;
|
|
323
|
+
if (state.schemaVersion !== 1 ||
|
|
324
|
+
!("active" in state) ||
|
|
325
|
+
!("previous" in state)) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
schemaVersion: 1,
|
|
330
|
+
active: validateProxyPackageSelection(state.active),
|
|
331
|
+
previous: validateProxyPackageSelection(state.previous),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
export function readProxyPackageSelection(packagesDir, previous = false) {
|
|
335
|
+
const state = readProxyPackageSelections(packagesDir);
|
|
336
|
+
return (previous ? state?.previous : state?.active) ?? null;
|
|
337
|
+
}
|
|
338
|
+
/** Resolve immutable selections before consulting an older mutable global entry. */
|
|
339
|
+
export function resolveProxyWorkerPackage(options) {
|
|
340
|
+
const state = readProxyPackageSelections(options.packagesDir);
|
|
341
|
+
for (const selection of [state?.active, state?.previous]) {
|
|
342
|
+
if (selection?.version === options.expectedVersion) {
|
|
343
|
+
return selection;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const original = inspectProxyPackage(options.entryScript);
|
|
348
|
+
if (original.version === options.expectedVersion) {
|
|
349
|
+
return original;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// A global upgrade/removal may invalidate the original entry while an
|
|
354
|
+
// already selected private package remains the valid recovery target.
|
|
355
|
+
}
|
|
356
|
+
throw new Error(`No validated package selection for worker v${options.expectedVersion}`);
|
|
357
|
+
}
|
|
358
|
+
function assertPackageSelectionOwner(isCurrentOwner) {
|
|
359
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
360
|
+
throw new Error("Proxy update owner changed; package selection was not modified");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/** Persist only package selection; runtime env, config and launchd settings are untouched. */
|
|
364
|
+
export function selectProxyPackage(packagesDir, selection, isCurrentOwner) {
|
|
365
|
+
assertPackageSelectionOwner(isCurrentOwner);
|
|
366
|
+
mkdirSync(packagesDir, { recursive: true, mode: 0o700 });
|
|
367
|
+
const current = readProxyPackageSelections(packagesDir);
|
|
368
|
+
if (!current) {
|
|
369
|
+
throw new Error("Proxy package selection state is unreadable or invalid");
|
|
370
|
+
}
|
|
371
|
+
// Manifest reads can outlive a service owner, even before private staging.
|
|
372
|
+
assertPackageSelectionOwner(isCurrentOwner);
|
|
373
|
+
const state = {
|
|
374
|
+
schemaVersion: 1,
|
|
375
|
+
active: selection,
|
|
376
|
+
previous: current.active && current.active.version !== selection.version
|
|
377
|
+
? current.active
|
|
378
|
+
: current.previous,
|
|
379
|
+
};
|
|
380
|
+
const temporary = join(packagesDir, `selections-${process.pid}-${randomUUID()}.tmp`);
|
|
381
|
+
try {
|
|
382
|
+
writeFileSync(temporary, JSON.stringify(state), {
|
|
383
|
+
mode: 0o600,
|
|
384
|
+
flag: "wx",
|
|
385
|
+
});
|
|
386
|
+
assertPackageSelectionOwner(isCurrentOwner);
|
|
387
|
+
// One rename publishes both versions. A stale owner never mutates rollback
|
|
388
|
+
// state independently, and must never attempt to restore a newer owner's state.
|
|
389
|
+
renameSync(temporary, join(packagesDir, "selections.json"));
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
rmSync(temporary, { force: true });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** No startup probes: IPC descriptors and OTel initialization belong to the real process. */
|
|
396
|
+
export function writeProxyPackageLauncher(path, selection, isCurrentOwner) {
|
|
397
|
+
assertPackageSelectionOwner(isCurrentOwner);
|
|
398
|
+
const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
399
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
400
|
+
// --version reads the manifest in a tiny isolated Node process. This proves
|
|
401
|
+
// the selected files still exist without cold-loading every provider twice.
|
|
402
|
+
const probe = `const fs=require('node:fs');let p=${JSON.stringify(dirname(selection.entryScript))};for(;;){const f=p+'/package.json';if(fs.existsSync(f)){const m=JSON.parse(fs.readFileSync(f,'utf8'));if(m.name==='@juspay/neurolink'){if(m.version!==${JSON.stringify(selection.version)})process.exit(1);console.log(m.version);break;}}const q=require('node:path').dirname(p);if(q===p)process.exit(1);p=q;}`;
|
|
403
|
+
const script = `#!/bin/sh\n# Generated proxy package selection. Service environment is inherited.\n[ -f ${quote(selection.entryScript)} ] || exit 127\nif [ "$#" = "1" ] && [ "$1" = "--version" ]; then\n exec ${quote(selection.nodePath)} -e ${quote(probe)}\nfi\nexec ${quote(selection.nodePath)} ${quote(selection.entryScript)} "$@"\n`;
|
|
404
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
405
|
+
writeFileSync(temporary, script, { mode: 0o755 });
|
|
406
|
+
chmodSync(temporary, 0o755);
|
|
407
|
+
assertPackageSelectionOwner(isCurrentOwner);
|
|
408
|
+
renameSync(temporary, path);
|
|
409
|
+
}
|
|
410
|
+
/** Install in a private tree, then publish the immutable directory after validation. */
|
|
411
|
+
export async function installStagedProxyPackage(options) {
|
|
412
|
+
if (!/^\d+\.\d+\.\d+$/.test(options.version)) {
|
|
413
|
+
throw new Error("Invalid proxy package version");
|
|
414
|
+
}
|
|
415
|
+
await mkdir(options.packagesDir, { recursive: true, mode: 0o700 });
|
|
416
|
+
const destination = join(options.packagesDir, options.version);
|
|
417
|
+
const validate = (root) => {
|
|
418
|
+
const packageRoot = join(root, "node_modules", "@juspay", "neurolink");
|
|
419
|
+
const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
420
|
+
const bin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.neurolink;
|
|
421
|
+
if (!bin) {
|
|
422
|
+
throw new Error("Candidate package is missing its neurolink executable");
|
|
423
|
+
}
|
|
424
|
+
const entry = realpathSync(resolve(packageRoot, bin));
|
|
425
|
+
const rel = relative(realpathSync(packageRoot), entry);
|
|
426
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
427
|
+
throw new Error("Candidate executable escapes its package");
|
|
428
|
+
}
|
|
429
|
+
const selection = inspectProxyPackage(entry);
|
|
430
|
+
if (selection.version !== options.version) {
|
|
431
|
+
throw new Error(`Candidate version ${selection.version} does not match ${options.version}`);
|
|
432
|
+
}
|
|
433
|
+
(options.execFileSync ?? nodeExecFileSync)(selection.nodePath, ["--check", selection.entryScript], { timeout: 10_000, stdio: "pipe" });
|
|
434
|
+
return selection;
|
|
435
|
+
};
|
|
436
|
+
if (existsSync(destination)) {
|
|
437
|
+
return validate(destination);
|
|
438
|
+
}
|
|
439
|
+
const staging = await mkdtemp(join(options.packagesDir, `.install-${options.version}-`));
|
|
440
|
+
try {
|
|
441
|
+
await writeFile(join(staging, "package.json"), JSON.stringify({ private: true }), { mode: 0o600 });
|
|
442
|
+
const spec = `@juspay/neurolink@${options.version}`;
|
|
443
|
+
const args = options.installer.kind === "npm"
|
|
444
|
+
? [
|
|
445
|
+
"install",
|
|
446
|
+
"--prefix",
|
|
447
|
+
staging,
|
|
448
|
+
"--no-save",
|
|
449
|
+
"--package-lock=false",
|
|
450
|
+
"--no-audit",
|
|
451
|
+
"--no-fund",
|
|
452
|
+
"--omit=dev",
|
|
453
|
+
"--loglevel=http",
|
|
454
|
+
spec,
|
|
455
|
+
]
|
|
456
|
+
: [
|
|
457
|
+
"add",
|
|
458
|
+
"--dir",
|
|
459
|
+
staging,
|
|
460
|
+
"--no-lockfile",
|
|
461
|
+
"--prod",
|
|
462
|
+
"--reporter=ndjson",
|
|
463
|
+
spec,
|
|
464
|
+
];
|
|
465
|
+
await new Promise((resolveInstall, rejectInstall) => {
|
|
466
|
+
const started = Date.now();
|
|
467
|
+
let lastProgress = started;
|
|
468
|
+
let bytes = 0;
|
|
469
|
+
let failureCode;
|
|
470
|
+
let failure;
|
|
471
|
+
let killTimer;
|
|
472
|
+
const child = (options.spawn ?? nodeSpawn)(options.installer.bin, args, {
|
|
473
|
+
cwd: staging,
|
|
474
|
+
env: { ...process.env, npm_config_update_notifier: "false" },
|
|
475
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
476
|
+
detached: process.platform !== "win32",
|
|
477
|
+
});
|
|
478
|
+
const progress = (chunk) => {
|
|
479
|
+
bytes += Buffer.byteLength(chunk);
|
|
480
|
+
lastProgress = Date.now();
|
|
481
|
+
// Retain a machine code only: npm output can include authenticated URLs.
|
|
482
|
+
const text = String(chunk);
|
|
483
|
+
const code = text.match(/npm (?:ERR!|error) code ([A-Z][A-Z0-9_]+)/)?.[1] ??
|
|
484
|
+
text.match(/\b(?:ECONNRESET|ECONNREFUSED|EAI_AGAIN|EHOSTUNREACH|ENETUNREACH|ENOTFOUND|EPIPE|ETIMEDOUT)\b/)?.[0];
|
|
485
|
+
if (code) {
|
|
486
|
+
failureCode = code;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
child.stdout?.on("data", progress);
|
|
490
|
+
child.stderr?.on("data", progress);
|
|
491
|
+
const signal = (value) => {
|
|
492
|
+
try {
|
|
493
|
+
if (process.platform !== "win32" && child.pid) {
|
|
494
|
+
process.kill(-child.pid, value);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
child.kill(value);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
/* child already exited */
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
const interval = setInterval(() => {
|
|
505
|
+
const now = Date.now();
|
|
506
|
+
options.onProgress?.({
|
|
507
|
+
elapsedMs: now - started,
|
|
508
|
+
outputBytes: bytes,
|
|
509
|
+
});
|
|
510
|
+
if (!failure &&
|
|
511
|
+
(now - started >= (options.maxDurationMs ?? 15 * 60_000) ||
|
|
512
|
+
now - lastProgress >= (options.idleTimeoutMs ?? 120_000))) {
|
|
513
|
+
failure = Object.assign(new Error(`Staged install timed out after ${now - started}ms (${bytes} output bytes, idle ${now - lastProgress}ms)`), { code: "ETIMEDOUT" });
|
|
514
|
+
signal("SIGTERM");
|
|
515
|
+
killTimer = setTimeout(() => signal("SIGKILL"), options.killGraceMs ?? 5_000);
|
|
516
|
+
}
|
|
517
|
+
}, Math.min(5_000, Math.max(10, (options.idleTimeoutMs ?? 120_000) / 4)));
|
|
518
|
+
const cleanup = () => {
|
|
519
|
+
clearInterval(interval);
|
|
520
|
+
clearTimeout(killTimer);
|
|
521
|
+
};
|
|
522
|
+
child.once("error", (error) => {
|
|
523
|
+
cleanup();
|
|
524
|
+
rejectInstall(error);
|
|
525
|
+
});
|
|
526
|
+
child.once("close", (code, signalName) => {
|
|
527
|
+
cleanup();
|
|
528
|
+
if (failure) {
|
|
529
|
+
rejectInstall(failure);
|
|
530
|
+
}
|
|
531
|
+
else if (code !== 0) {
|
|
532
|
+
rejectInstall(Object.assign(new Error(`Staged install exited code=${code} signal=${signalName ?? "none"} errorCode=${failureCode ?? "unknown"}`), {
|
|
533
|
+
code: failureCode ??
|
|
534
|
+
(code === null ? "ECONNRESET" : "INSTALL_FAILED"),
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
resolveInstall();
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
validate(staging);
|
|
543
|
+
await rename(staging, destination);
|
|
544
|
+
return validate(destination);
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
await rm(staging, { recursive: true, force: true });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/** Only operator runtime settings survive reinstall, never worker IPC identity. */
|
|
551
|
+
export function isProxyServiceEnvironmentKey(name) {
|
|
552
|
+
return ((name.startsWith("OTEL_") ||
|
|
553
|
+
name.startsWith("NEUROLINK_PROXY_") ||
|
|
554
|
+
[
|
|
555
|
+
"NEUROLINK_PACKAGE_MANAGER",
|
|
556
|
+
"NEUROLINK_PACKAGE_MANAGER_PATH",
|
|
557
|
+
"NEUROLINK_PNPM_PATH",
|
|
558
|
+
"PNPM_HOME",
|
|
559
|
+
].includes(name)) &&
|
|
560
|
+
![
|
|
561
|
+
"NEUROLINK_PROXY_SOCKET_WORKER",
|
|
562
|
+
"NEUROLINK_PROXY_WORKER_GENERATION",
|
|
563
|
+
"NEUROLINK_PROXY_WORKER_EXPECTED_VERSION",
|
|
564
|
+
"NEUROLINK_PROXY_UPDATE_CONTROL_TOKEN",
|
|
565
|
+
"NEUROLINK_PROXY_ROLLING_SUPERVISOR",
|
|
566
|
+
"NEUROLINK_PROXY_TRAMPOLINE_EXEC_ONLY",
|
|
567
|
+
"NEUROLINK_PROXY_TEST_ISOLATED",
|
|
568
|
+
].includes(name));
|
|
569
|
+
}
|
|
570
|
+
/** Parse plutil JSON without exposing credentials or invoking the saved launcher. */
|
|
571
|
+
export function parseProxyServiceInstallSettings(value) {
|
|
572
|
+
const settings = { environment: {} };
|
|
573
|
+
if (!value || typeof value !== "object") {
|
|
574
|
+
return settings;
|
|
575
|
+
}
|
|
576
|
+
const plist = value;
|
|
577
|
+
const args = Array.isArray(plist.ProgramArguments)
|
|
578
|
+
? plist.ProgramArguments
|
|
579
|
+
: [];
|
|
580
|
+
const option = (name) => {
|
|
581
|
+
const index = args.indexOf(name);
|
|
582
|
+
return index >= 0 && typeof args[index + 1] === "string"
|
|
583
|
+
? args[index + 1]
|
|
584
|
+
: undefined;
|
|
585
|
+
};
|
|
586
|
+
settings.envFile = option("--env-file");
|
|
587
|
+
settings.configFile = option("--config");
|
|
588
|
+
settings.host = option("--host");
|
|
589
|
+
const port = Number(option("--port"));
|
|
590
|
+
if (Number.isInteger(port) && port > 0 && port <= 65535) {
|
|
591
|
+
settings.port = port;
|
|
592
|
+
}
|
|
593
|
+
if (plist.EnvironmentVariables &&
|
|
594
|
+
typeof plist.EnvironmentVariables === "object") {
|
|
595
|
+
for (const [name, entry] of Object.entries(plist.EnvironmentVariables)) {
|
|
596
|
+
if (isProxyServiceEnvironmentKey(name) && typeof entry === "string") {
|
|
597
|
+
settings.environment[name] = entry;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return settings;
|
|
602
|
+
}
|
|
603
|
+
/** Distinguish an absent launchd job from permission failures without logging plist/env data. */
|
|
604
|
+
export function probeProxyLaunchdPresence(options) {
|
|
605
|
+
try {
|
|
606
|
+
(options.execFileSync ?? nodeExecFileSync)("launchctl", ["print", `gui/${options.uid}/${options.label}`], { encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"] });
|
|
607
|
+
// A loaded but currently idle KeepAlive job can start immediately. It must
|
|
608
|
+
// be explicitly unloaded before regenerating its launcher and environment.
|
|
609
|
+
return "loaded";
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
if (error &&
|
|
613
|
+
typeof error === "object" &&
|
|
614
|
+
"status" in error &&
|
|
615
|
+
error.status === 113 &&
|
|
616
|
+
"stderr" in error &&
|
|
617
|
+
/could not find service/i.test(String(error.stderr))) {
|
|
618
|
+
return "not_loaded";
|
|
619
|
+
}
|
|
620
|
+
return "unknown";
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/** Read-only guard: callers must run this before every reinstall mutation. */
|
|
624
|
+
export function assertProxyServiceInstallIdle(options) {
|
|
625
|
+
if (options.launchdPresence !== "not_loaded" ||
|
|
626
|
+
options.pids.some((pid) => Number.isSafeInteger(pid) &&
|
|
627
|
+
pid > 0 &&
|
|
628
|
+
options.getProcessStatus(pid) !== "not_running")) {
|
|
629
|
+
throw new Error("Proxy service is running or could still run. Reinstall was refused before changing files or processes. Use 'neurolink proxy restart' for a safe rolling replacement, or explicitly stop and unload the service before an intended reinstall.");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
@@ -3,9 +3,9 @@ import type { ProxyBodyChunkEmitter, ProxyBodyDeliveryResult } from "../types/in
|
|
|
3
3
|
/** Explicit opt-in; configuration never silently falls back to file logging. */
|
|
4
4
|
export declare function isProxyOtelOnly(): boolean;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Publish bounded captures concurrently into a shared batch queue. Transport
|
|
7
|
+
* callbacks settle each chunk once; a flush is a process fence, not a per-body
|
|
8
|
+
* network round trip. Metadata has a separate queue and transport.
|
|
9
9
|
*/
|
|
10
10
|
export declare function publishProxyOtelBody(captureId: string, body: string, emit: ProxyBodyChunkEmitter): Promise<ProxyBodyDeliveryResult>;
|
|
11
11
|
/** Initialize a log-only provider in every proxy process, including the supervisor. */
|