@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/dist/browser/neurolink.min.js +430 -421
  3. package/dist/cli/commands/proxy.js +455 -259
  4. package/dist/constants/contextWindows.d.ts +2 -0
  5. package/dist/constants/contextWindows.js +4 -0
  6. package/dist/neurolink.js +8 -3
  7. package/dist/providers/openaiChatCompletionsBase.js +60 -51
  8. package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
  9. package/dist/proxy/anthropicBudgetResponse.js +153 -0
  10. package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
  11. package/dist/proxy/bodyCaptureProcessing.js +23 -4
  12. package/dist/proxy/bodyCaptureWorker.js +20 -2
  13. package/dist/proxy/codexFallback.d.ts +10 -0
  14. package/dist/proxy/codexFallback.js +108 -24
  15. package/dist/proxy/codexUsage.js +2 -0
  16. package/dist/proxy/globalInstaller.d.ts +32 -1
  17. package/dist/proxy/globalInstaller.js +386 -3
  18. package/dist/proxy/otelLogSink.d.ts +3 -3
  19. package/dist/proxy/otelLogSink.js +137 -144
  20. package/dist/proxy/otlpLogTransport.d.ts +3 -0
  21. package/dist/proxy/otlpLogTransport.js +334 -0
  22. package/dist/proxy/proxyActivity.d.ts +22 -2
  23. package/dist/proxy/proxyActivity.js +114 -9
  24. package/dist/proxy/proxyAnalysis.js +82 -52
  25. package/dist/proxy/proxyClientCancellation.d.ts +7 -0
  26. package/dist/proxy/proxyClientCancellation.js +23 -0
  27. package/dist/proxy/proxyContextPreflight.d.ts +25 -0
  28. package/dist/proxy/proxyContextPreflight.js +337 -0
  29. package/dist/proxy/proxyFailureDetails.d.ts +19 -0
  30. package/dist/proxy/proxyFailureDetails.js +96 -0
  31. package/dist/proxy/proxyLifecycle.js +7 -0
  32. package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
  33. package/dist/proxy/proxyRouteBodyCapture.js +112 -0
  34. package/dist/proxy/proxyTokenBudget.d.ts +40 -0
  35. package/dist/proxy/proxyTokenBudget.js +523 -0
  36. package/dist/proxy/proxyTracer.d.ts +3 -2
  37. package/dist/proxy/proxyTracer.js +46 -20
  38. package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
  39. package/dist/proxy/proxyTranslationEngine.js +736 -376
  40. package/dist/proxy/requestLogger.d.ts +1 -0
  41. package/dist/proxy/requestLogger.js +107 -5
  42. package/dist/proxy/restartControl.js +9 -4
  43. package/dist/proxy/rollingWorkerProcess.js +65 -0
  44. package/dist/proxy/rollingWorkerSupervisor.js +7 -0
  45. package/dist/proxy/updateCoordinator.d.ts +14 -1
  46. package/dist/proxy/updateCoordinator.js +57 -2
  47. package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
  48. package/dist/server/routes/claudeProxyRoutes.js +809 -577
  49. package/dist/server/routes/codexProxyRoutes.js +191 -9
  50. package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
  51. package/dist/server/routes/openaiProxyRoutes.js +261 -69
  52. package/dist/types/cli.d.ts +3 -0
  53. package/dist/types/codex.d.ts +5 -1
  54. package/dist/types/index.d.ts +2 -0
  55. package/dist/types/index.js +2 -0
  56. package/dist/types/proxy.d.ts +148 -3
  57. package/dist/types/proxy.js +0 -15
  58. package/dist/types/proxyBudget.d.ts +74 -0
  59. package/dist/types/proxyBudget.js +1 -0
  60. package/dist/types/proxyContext.d.ts +35 -0
  61. package/dist/types/proxyContext.js +1 -0
  62. package/dist/utils/fileDetector.js +28 -7
  63. package/dist/utils/messageBuilder.js +51 -26
  64. package/dist/utils/pdfProcessor.js +26 -4
  65. package/docs/assets/dashboards/README.md +37 -0
  66. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
  67. package/docs-site/static/search-index.json +1 -1
  68. package/package.json +7 -2
  69. package/scripts/observability/NATIVE-DURABLE.md +63 -0
  70. package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
  71. package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
  72. package/scripts/observability/proxy-telemetry-check.mjs +396 -61
  73. 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 Error(`Codex fallback stream terminated with ${type}`);
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 = proxyTokenUsage({
372
- inputTokens: parsedUsage.inputTokens,
373
- outputTokens: parsedUsage.outputTokens,
374
- reasoningTokens: parsedUsage.reasoningTokensObserved
375
- ? parsedUsage.reasoningTokens
376
- : undefined,
377
- cacheReadTokens: parsedUsage.cacheReadTokens,
378
- cacheCreationTokens: parsedUsage.cacheCreationTokens,
379
- inputIncludesCachedTokens: true,
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 Error("Codex fallback returned no content or tool calls");
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 Error(`Codex fallback stream terminated with ${type}`);
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 = proxyTokenUsage({
524
- inputTokens: parsedUsage.inputTokens,
525
- outputTokens: parsedUsage.outputTokens,
526
- reasoningTokens: parsedUsage.reasoningTokensObserved
527
- ? parsedUsage.reasoningTokens
528
- : undefined,
529
- cacheReadTokens: parsedUsage.cacheReadTokens,
530
- cacheCreationTokens: parsedUsage.cacheCreationTokens,
531
- inputIncludesCachedTokens: true,
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 Error("Codex fallback returned no content or tool calls");
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, {
@@ -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 { basename, dirname, isAbsolute, join, resolve } from "node:path";
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
- * Own a whole capture within byte/count bounds, then pace its chunks by actual
7
- * export callbacks. SDK forceFlush alone does not await an automatic export
8
- * already in flight. Serial publication prevents bursts from dropping tails.
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. */