@juspay/neurolink 12.44.5 → 12.44.6
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 +2 -2
- package/dist/cli/commands/proxy.js +30 -13
- package/dist/cli/commands/proxyPolyfill.d.ts +4 -0
- package/dist/cli/commands/proxyPolyfill.js +55 -0
- package/dist/cli/parser.js +3 -1
- package/dist/cli/proxy-clients/registry.d.ts +2 -0
- package/dist/cli/proxy-clients/registry.js +4 -0
- package/dist/proxy/globalInstaller.d.ts +3 -1
- package/dist/proxy/globalInstaller.js +151 -4
- package/dist/proxy/proxyPackagePolyfills.d.ts +18 -0
- package/dist/proxy/proxyPackagePolyfills.js +334 -0
- package/dist/types/proxy.d.ts +26 -0
- package/docs-site/static/search-index.json +3 -2
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
## [12.44.
|
|
1
|
+
## [12.44.6](https://github.com/juspay/neurolink/compare/v12.44.5...v12.44.6) (2026-10-03)
|
|
2
2
|
|
|
3
3
|
### Bug Fixes
|
|
4
4
|
|
|
5
|
-
- **(
|
|
5
|
+
- **(proxy):** preserve local edits and settings across upgrades ([d239e45](https://github.com/juspay/neurolink/commit/d239e45d5c8fed29283222dfd7ccdfadfc51637b))
|
|
6
6
|
|
|
7
7
|
## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
|
|
8
8
|
|
|
@@ -9,7 +9,7 @@ import chalk from "chalk";
|
|
|
9
9
|
import ora from "ora";
|
|
10
10
|
import { buildProxyHealthResponse, createProxyReadinessState, markProxyDrainingForUpdate, markProxyReady, resumeProxyConnections, waitForProxyReadiness, } from "../../proxy/proxyHealth.js";
|
|
11
11
|
import { logger } from "../../utils/logger.js";
|
|
12
|
-
import { applyAllClients, restoreAllClients, } from "../proxy-clients/registry.js";
|
|
12
|
+
import { applyAllClients, applyClientsOnProxyStart, restoreAllClients, } from "../proxy-clients/registry.js";
|
|
13
13
|
import { resolveProxyConfigPath } from "../../proxy/proxyConfig.js";
|
|
14
14
|
import { redactUrlsInText, sanitizeForLog, } from "../../utils/logSanitize.js";
|
|
15
15
|
import { raceWithAbort, withTimeout, } from "../../utils/async/withTimeout.js";
|
|
@@ -23,7 +23,7 @@ import { anthropicAccountKeysEqual, createAccountAllowlist, isAccountAllowed, LE
|
|
|
23
23
|
import { resolveProxyStatusAccountIdentity } from "../../proxy/codexAccountUsage.js";
|
|
24
24
|
import { beginProxyRequest, consumeInternalProxyRequest, releaseProxyRequestAccounting, withProxyFinalLogOwnership, getProxyActivitySnapshot, observeProxyFinalLog, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
|
|
25
25
|
import { configureProxyLifecycleLogger, flushProxyLifecycleEvents, persistProxyLifecycleAcceptance, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../proxy/proxyLifecycle.js";
|
|
26
|
-
import { describeInstallFailure, assertProxyServiceInstallIdle, probeProxyLaunchdPresence, inspectProxyPackage, isProxyServiceEnvironmentKey, parseProxyServiceInstallSettings, installStagedProxyPackage, readProxyPackageSelection, resolveProxyWorkerPackage, selectProxyPackage, writeProxyPackageLauncher, isTransientInstallFailure, resolveGlobalInstaller, validateInstalledVersion, } from "../../proxy/globalInstaller.js";
|
|
26
|
+
import { describeInstallFailure, assertProxyServiceInstallIdle, probeProxyLaunchdPresence, inspectProxyPackage, isProxyServiceEnvironmentKey, parseProxyServiceInstallSettings, installStagedProxyPackage, prepareProxyPackageUpgrade, readProxyPackageSelection, resolveProxyWorkerPackage, selectProxyPackage, writeProxyPackageLauncher, isTransientInstallFailure, resolveGlobalInstaller, validateInstalledVersion, } from "../../proxy/globalInstaller.js";
|
|
27
27
|
import { startUpdaterWorkerSupervisor } from "../../proxy/updaterSupervisor.js";
|
|
28
28
|
import { openProxyWorkerLog } from "../../proxy/workerLog.js";
|
|
29
29
|
import { startRollingProxyServer } from "../../proxy/rollingProxyServer.js";
|
|
@@ -2933,9 +2933,7 @@ async function startProxyRuntime(params) {
|
|
|
2933
2933
|
logger.always(` ${chalk.bold("Env File:")} ${chalk.cyan(params.loadedEnvFile)}`);
|
|
2934
2934
|
}
|
|
2935
2935
|
if (!isDev) {
|
|
2936
|
-
for (const result of await
|
|
2937
|
-
configPath: params.configPath,
|
|
2938
|
-
})) {
|
|
2936
|
+
for (const result of await applyClientsOnProxyStart(url, { configPath: params.configPath }, managedByLaunchd)) {
|
|
2939
2937
|
if (result.error) {
|
|
2940
2938
|
// Visible, not debug-level. A client whose config could not be written
|
|
2941
2939
|
// will keep talking to its own upstream, which looks like the proxy
|
|
@@ -2956,6 +2954,9 @@ async function startProxyRuntime(params) {
|
|
|
2956
2954
|
logger.always(chalk.dim(` Restart ${result.displayName} to connect through proxy`));
|
|
2957
2955
|
}
|
|
2958
2956
|
}
|
|
2957
|
+
if (managedByLaunchd) {
|
|
2958
|
+
logger.always(" Service startup preserves client settings; use 'neurolink proxy setup' to change client routing.");
|
|
2959
|
+
}
|
|
2959
2960
|
}
|
|
2960
2961
|
else {
|
|
2961
2962
|
logger.always(chalk.dim(" ⊘ Dev mode: skipping client auto-configuration"));
|
|
@@ -4270,20 +4271,36 @@ export const proxyGuardCommand = {
|
|
|
4270
4271
|
return;
|
|
4271
4272
|
}
|
|
4272
4273
|
try {
|
|
4273
|
-
//
|
|
4274
|
-
//
|
|
4275
|
-
previousPackage
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4274
|
+
// Keep the exact serving tree, including a same-version local
|
|
4275
|
+
// build. Restaging by version would discard its edits and rollback.
|
|
4276
|
+
if (previousPackage?.version !== runningVersion) {
|
|
4277
|
+
try {
|
|
4278
|
+
const original = inspectProxyPackage(process.argv[1]);
|
|
4279
|
+
if (original.version === runningVersion) {
|
|
4280
|
+
previousPackage = original;
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
catch {
|
|
4284
|
+
// A mutable global entry may have disappeared since startup.
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
if (previousPackage?.version !== runningVersion) {
|
|
4288
|
+
previousPackage = await installStagedProxyPackage({
|
|
4289
|
+
version: runningVersion,
|
|
4290
|
+
packagesDir: PROXY_PACKAGES_DIR,
|
|
4291
|
+
installer,
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
4280
4294
|
if (!isCurrentUpdateOwner()) {
|
|
4281
4295
|
return;
|
|
4282
4296
|
}
|
|
4283
|
-
candidatePackage = await
|
|
4297
|
+
candidatePackage = await prepareProxyPackageUpgrade({
|
|
4284
4298
|
version: result.latestVersion,
|
|
4285
4299
|
packagesDir: PROXY_PACKAGES_DIR,
|
|
4286
4300
|
installer,
|
|
4301
|
+
activePackage: previousPackage,
|
|
4302
|
+
isCurrentOwner: isCurrentUpdateAttemptOwner,
|
|
4303
|
+
onPolyfills: ({ applied, alreadyIncluded }) => logger.always(`[updater] local polyfills: ${applied.length} files applied, ${alreadyIncluded.length} already included in release`),
|
|
4287
4304
|
onProgress: ({ elapsedMs, outputBytes }) => logger.debug(`[updater] staged install progress elapsedMs=${elapsedMs} outputBytes=${outputBytes}`),
|
|
4288
4305
|
});
|
|
4289
4306
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs";
|
|
2
|
+
import type { ProxyPolyfillArgs } from "../../types/index.js";
|
|
3
|
+
/** Record the base of a locally patched package without changing running services. */
|
|
4
|
+
export declare const proxyPolyfillCommand: CommandModule<object, ProxyPolyfillArgs>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { inspectProxyPackage } from "../../proxy/globalInstaller.js";
|
|
2
|
+
import { captureProxyPackageBaseline } from "../../proxy/proxyPackagePolyfills.js";
|
|
3
|
+
/** Record the base of a locally patched package without changing running services. */
|
|
4
|
+
export const proxyPolyfillCommand = {
|
|
5
|
+
command: "polyfill <action>",
|
|
6
|
+
describe: "Preserve local proxy compatibility edits across staged upgrades",
|
|
7
|
+
builder: (yargs) => yargs
|
|
8
|
+
.positional("action", {
|
|
9
|
+
choices: ["capture"],
|
|
10
|
+
type: "string",
|
|
11
|
+
demandOption: true,
|
|
12
|
+
})
|
|
13
|
+
.option("base", {
|
|
14
|
+
type: "string",
|
|
15
|
+
demandOption: true,
|
|
16
|
+
description: "CLI entry script of the pristine package used for the local edit",
|
|
17
|
+
})
|
|
18
|
+
.option("patched", {
|
|
19
|
+
type: "string",
|
|
20
|
+
demandOption: true,
|
|
21
|
+
description: "CLI entry script of the locally patched package",
|
|
22
|
+
})
|
|
23
|
+
.option("format", {
|
|
24
|
+
choices: ["text", "json"],
|
|
25
|
+
type: "string",
|
|
26
|
+
default: "text",
|
|
27
|
+
})
|
|
28
|
+
.example("neurolink proxy polyfill capture --base /original/dist/cli/index.js --patched /local/dist/cli/index.js", "Record the exact original files so later upgrades can apply only the local diff"),
|
|
29
|
+
handler: async (argv) => {
|
|
30
|
+
try {
|
|
31
|
+
const base = inspectProxyPackage(argv.base);
|
|
32
|
+
const patched = inspectProxyPackage(argv.patched);
|
|
33
|
+
const files = captureProxyPackageBaseline(base, patched);
|
|
34
|
+
const result = {
|
|
35
|
+
captured: true,
|
|
36
|
+
baseVersion: base.version,
|
|
37
|
+
patchedVersion: patched.version,
|
|
38
|
+
files,
|
|
39
|
+
};
|
|
40
|
+
console.info(argv.format === "json"
|
|
41
|
+
? JSON.stringify(result)
|
|
42
|
+
: `Recorded ${files.length} local proxy polyfill file(s). Upgrades will apply remaining diff hunks, skip included fixes, and retain the current worker on conflicts.`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error
|
|
46
|
+
? error.message
|
|
47
|
+
: "Proxy polyfill capture failed";
|
|
48
|
+
console.error(argv.format === "json"
|
|
49
|
+
? JSON.stringify({ captured: false, message })
|
|
50
|
+
: message);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=proxyPolyfill.js.map
|
package/dist/cli/parser.js
CHANGED
|
@@ -24,6 +24,7 @@ import { TelemetryCommandFactory } from "./commands/telemetry.js";
|
|
|
24
24
|
import { proxyStartCommand, proxyStatusCommand, proxyTelemetryCommand, proxySetupCommand, proxyGuardCommand, proxyInstallCommand, proxyUninstallCommand, } from "./commands/proxy.js";
|
|
25
25
|
import { proxyAnalyzeCommand } from "./commands/proxyAnalyze.js";
|
|
26
26
|
import { proxyRestartCommand } from "./commands/proxyRestart.js";
|
|
27
|
+
import { proxyPolyfillCommand } from "./commands/proxyPolyfill.js";
|
|
27
28
|
import { proxyShareCommand } from "./commands/proxyShare.js";
|
|
28
29
|
import { proxyPeerCommand } from "./commands/proxyPeer.js";
|
|
29
30
|
import { proxyExposeCommand } from "./commands/proxyExpose.js";
|
|
@@ -229,6 +230,7 @@ export function initializeCliParser() {
|
|
|
229
230
|
.command(proxyStartCommand)
|
|
230
231
|
.command(proxyStatusCommand)
|
|
231
232
|
.command(proxyRestartCommand)
|
|
233
|
+
.command(proxyPolyfillCommand)
|
|
232
234
|
.command(proxyShareCommand)
|
|
233
235
|
.command(proxyPeerCommand)
|
|
234
236
|
.command(proxyExposeCommand)
|
|
@@ -239,7 +241,7 @@ export function initializeCliParser() {
|
|
|
239
241
|
.command(proxyGuardCommand)
|
|
240
242
|
.command(proxyInstallCommand)
|
|
241
243
|
.command(proxyUninstallCommand)
|
|
242
|
-
.demandCommand(1, "Please specify a proxy subcommand: start, status, restart, share <create|provision|url|list|status|pause|resume|revoke|topup|set|link|rotate|level|note|notes|receipts|delete>, peer <add|request|sync|receipts|net|redeem|list|status|test|remove|pause|resume|set>, expose, analyze, replay <export|compare>, telemetry <setup|start|stop|status|logs|import-dashboard>, setup, guard, install, or uninstall"),
|
|
244
|
+
.demandCommand(1, "Please specify a proxy subcommand: start, status, restart, polyfill capture, share <create|provision|url|list|status|pause|resume|revoke|topup|set|link|rotate|level|note|notes|receipts|delete>, peer <add|request|sync|receipts|net|redeem|list|status|test|remove|pause|resume|set>, expose, analyze, replay <export|compare>, telemetry <setup|start|stop|status|logs|import-dashboard>, setup, guard, install, or uninstall"),
|
|
243
245
|
handler: () => { },
|
|
244
246
|
})
|
|
245
247
|
// Evaluate Command Group - Using EvaluateCommandFactory
|
|
@@ -15,5 +15,7 @@ export declare const PROXY_CLIENT_CONFIGURATORS: readonly CliProxyClientConfigur
|
|
|
15
15
|
* while the setup wizard prints a visible warning.
|
|
16
16
|
*/
|
|
17
17
|
export declare function applyAllClients(proxyBaseUrl: string, options?: CliProxyClientApplyOptions): Promise<CliProxyClientApplyResult[]>;
|
|
18
|
+
/** Service replacements own the server process, not the user's client routing. */
|
|
19
|
+
export declare function applyClientsOnProxyStart(proxyBaseUrl: string, options: CliProxyClientApplyOptions, managedByLaunchd: boolean): Promise<CliProxyClientApplyResult[]>;
|
|
18
20
|
/** Restore every client's previous configuration. See applyAllClients. */
|
|
19
21
|
export declare function restoreAllClients(proxyBaseUrl: string): Promise<CliProxyClientRestoreResult[]>;
|
|
@@ -77,6 +77,10 @@ export async function applyAllClients(proxyBaseUrl, options) {
|
|
|
77
77
|
}
|
|
78
78
|
return results;
|
|
79
79
|
}
|
|
80
|
+
/** Service replacements own the server process, not the user's client routing. */
|
|
81
|
+
export async function applyClientsOnProxyStart(proxyBaseUrl, options, managedByLaunchd) {
|
|
82
|
+
return managedByLaunchd ? [] : applyAllClients(proxyBaseUrl, options);
|
|
83
|
+
}
|
|
80
84
|
/** Restore every client's previous configuration. See applyAllClients. */
|
|
81
85
|
export async function restoreAllClients(proxyBaseUrl) {
|
|
82
86
|
const results = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProxyServiceInstallSettings, ProxyPackageSelection, ProxyStagedInstallOptions, GlobalInstallerExecFile, GlobalInstallerKind, GlobalInstallerResolution, InstalledVersionValidation, ResolveGlobalInstallerOptions, ValidateInstalledVersionOptions } from "../types/index.js";
|
|
1
|
+
import type { ProxyServiceInstallSettings, ProxyPackageSelection, ProxyStagedInstallOptions, ProxyPackageUpgradeOptions, 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[];
|
|
@@ -32,6 +32,8 @@ export declare function selectProxyPackage(packagesDir: string, selection: Proxy
|
|
|
32
32
|
export declare function writeProxyPackageLauncher(path: string, selection: ProxyPackageSelection, isCurrentOwner?: () => boolean): void;
|
|
33
33
|
/** Install in a private tree, then publish the immutable directory after validation. */
|
|
34
34
|
export declare function installStagedProxyPackage(options: ProxyStagedInstallOptions): Promise<ProxyPackageSelection>;
|
|
35
|
+
/** Preserve local runtime diff hunks before any launcher or live selection changes. */
|
|
36
|
+
export declare function prepareProxyPackageUpgrade(options: ProxyPackageUpgradeOptions): Promise<ProxyPackageSelection>;
|
|
35
37
|
/** Only operator runtime settings survive reinstall, never worker IPC identity. */
|
|
36
38
|
export declare function isProxyServiceEnvironmentKey(name: string): boolean;
|
|
37
39
|
/** Parse plutil JSON without exposing credentials or invoking the saved launcher. */
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { execFileSync as nodeExecFileSync, spawn as nodeSpawn, } from "node:child_process";
|
|
2
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
|
+
import { cp, mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { basename, dirname, isAbsolute, join, resolve, relative, } from "node:path";
|
|
7
|
+
import { changedProxyRuntimeFiles, proxyPackageRoot, readProxyPackageBaseline, reconcileProxyPackagePolyfills, snapshotProxyPackage, writeProxyPackageBaseline, writeProxyPolyfilledFile, } from "./proxyPackagePolyfills.js";
|
|
7
8
|
function runText(execFileSync, bin, args) {
|
|
8
9
|
return String(execFileSync(bin, args, {
|
|
9
10
|
encoding: "utf8",
|
|
@@ -373,7 +374,10 @@ export function selectProxyPackage(packagesDir, selection, isCurrentOwner) {
|
|
|
373
374
|
const state = {
|
|
374
375
|
schemaVersion: 1,
|
|
375
376
|
active: selection,
|
|
376
|
-
previous: current.active &&
|
|
377
|
+
previous: current.active &&
|
|
378
|
+
(current.active.version !== selection.version ||
|
|
379
|
+
current.active.entryScript !== selection.entryScript ||
|
|
380
|
+
current.active.nodePath !== selection.nodePath)
|
|
377
381
|
? current.active
|
|
378
382
|
: current.previous,
|
|
379
383
|
};
|
|
@@ -539,7 +543,8 @@ export async function installStagedProxyPackage(options) {
|
|
|
539
543
|
}
|
|
540
544
|
});
|
|
541
545
|
});
|
|
542
|
-
validate(staging);
|
|
546
|
+
const stagedSelection = validate(staging);
|
|
547
|
+
writeProxyPackageBaseline(stagedSelection, snapshotProxyPackage(stagedSelection));
|
|
543
548
|
await rename(staging, destination);
|
|
544
549
|
return validate(destination);
|
|
545
550
|
}
|
|
@@ -547,6 +552,148 @@ export async function installStagedProxyPackage(options) {
|
|
|
547
552
|
await rm(staging, { recursive: true, force: true });
|
|
548
553
|
}
|
|
549
554
|
}
|
|
555
|
+
/** Registry comparison must never inherit an existing directory's dependencies. */
|
|
556
|
+
async function installFreshProxyRegistryPackage(options) {
|
|
557
|
+
if (!/^\d+\.\d+\.\d+$/.test(options.version)) {
|
|
558
|
+
throw new Error("Invalid proxy package version");
|
|
559
|
+
}
|
|
560
|
+
await mkdir(options.packagesDir, { recursive: true, mode: 0o700 });
|
|
561
|
+
const container = await mkdtemp(join(options.packagesDir, `.registry-${options.version}-`));
|
|
562
|
+
try {
|
|
563
|
+
const selection = await installStagedProxyPackage({
|
|
564
|
+
...options,
|
|
565
|
+
packagesDir: container,
|
|
566
|
+
});
|
|
567
|
+
return { selection, container };
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
await rm(container, { recursive: true, force: true });
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/** Preserve local runtime diff hunks before any launcher or live selection changes. */
|
|
575
|
+
export async function prepareProxyPackageUpgrade(options) {
|
|
576
|
+
const assertOwner = () => assertPackageSelectionOwner(options.isCurrentOwner);
|
|
577
|
+
assertOwner();
|
|
578
|
+
const active = options.activePackage;
|
|
579
|
+
let baseline = readProxyPackageBaseline(active);
|
|
580
|
+
if (!baseline) {
|
|
581
|
+
// Legacy packages need a clean registry comparison. Never invent a baseline
|
|
582
|
+
// from edited files: an unknown local build must remain serving until its
|
|
583
|
+
// operator records the actual base with `proxy polyfill capture`.
|
|
584
|
+
const original = await installFreshProxyRegistryPackage({
|
|
585
|
+
...options,
|
|
586
|
+
version: active.version,
|
|
587
|
+
});
|
|
588
|
+
let retainReference = false;
|
|
589
|
+
try {
|
|
590
|
+
assertOwner();
|
|
591
|
+
baseline = readProxyPackageBaseline(original.selection);
|
|
592
|
+
if (!baseline) {
|
|
593
|
+
throw new Error("Pristine registry package baseline is missing");
|
|
594
|
+
}
|
|
595
|
+
if (changedProxyRuntimeFiles(baseline, snapshotProxyPackage(active)).length) {
|
|
596
|
+
retainReference = true;
|
|
597
|
+
throw new Error(`Active proxy contains local changes without a recorded base; upgrade retained the serving package. Record its actual original package with 'proxy polyfill capture --base <original-entry> --patched <local-entry>'. Downloaded registry reference: ${original.selection.entryScript}`);
|
|
598
|
+
}
|
|
599
|
+
writeProxyPackageBaseline(active, baseline);
|
|
600
|
+
}
|
|
601
|
+
finally {
|
|
602
|
+
if (!retainReference) {
|
|
603
|
+
await rm(original.container, { recursive: true, force: true });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const local = snapshotProxyPackage(active);
|
|
608
|
+
// Also rejects unsupported local dependency/entrypoint changes.
|
|
609
|
+
changedProxyRuntimeFiles(baseline, local);
|
|
610
|
+
const fresh = await installFreshProxyRegistryPackage(options);
|
|
611
|
+
try {
|
|
612
|
+
const candidate = fresh.selection;
|
|
613
|
+
assertOwner();
|
|
614
|
+
const candidateBase = readProxyPackageBaseline(candidate);
|
|
615
|
+
if (!candidateBase ||
|
|
616
|
+
changedProxyRuntimeFiles(candidateBase, snapshotProxyPackage(candidate))
|
|
617
|
+
.length) {
|
|
618
|
+
throw new Error("Candidate proxy package has no trustworthy pristine baseline");
|
|
619
|
+
}
|
|
620
|
+
const { files, report } = reconcileProxyPackagePolyfills(baseline, local, candidateBase);
|
|
621
|
+
const assertLocalUnchanged = () => {
|
|
622
|
+
assertOwner();
|
|
623
|
+
if (JSON.stringify(snapshotProxyPackage(active)) !== JSON.stringify(local)) {
|
|
624
|
+
throw new Error("Active proxy files changed while preparing the upgrade; activation refused");
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
assertLocalUnchanged();
|
|
628
|
+
if (!report.applied.length) {
|
|
629
|
+
const destination = join(options.packagesDir, `${options.version}-release-${randomUUID()}`);
|
|
630
|
+
await rename(resolve(proxyPackageRoot(candidate.entryScript), "../../.."), destination);
|
|
631
|
+
const selected = inspectProxyPackage(join(destination, "node_modules", "@juspay", "neurolink", candidateBase.entryRelative));
|
|
632
|
+
assertOwner();
|
|
633
|
+
options.onPolyfills?.(report);
|
|
634
|
+
return selected;
|
|
635
|
+
}
|
|
636
|
+
const digest = createHash("sha256")
|
|
637
|
+
.update(JSON.stringify({ version: options.version, files }))
|
|
638
|
+
.digest("hex")
|
|
639
|
+
.slice(0, 16);
|
|
640
|
+
const destination = join(options.packagesDir, `${options.version}-polyfills-${digest}-${randomUUID()}`);
|
|
641
|
+
const candidateRoot = proxyPackageRoot(candidate.entryScript);
|
|
642
|
+
const expected = { ...candidateBase.files };
|
|
643
|
+
for (const [path, content] of Object.entries(files)) {
|
|
644
|
+
if (content === null) {
|
|
645
|
+
delete expected[path];
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
expected[path] = content;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const validateCopy = (root) => {
|
|
652
|
+
const selection = inspectProxyPackage(join(root, "node_modules", "@juspay", "neurolink", candidateBase.entryRelative));
|
|
653
|
+
const snapshot = snapshotProxyPackage(selection);
|
|
654
|
+
if (selection.version !== options.version ||
|
|
655
|
+
snapshot.manifest !== candidateBase.manifest ||
|
|
656
|
+
Object.keys({ ...snapshot.files, ...expected }).some((path) => snapshot.files[path] !== expected[path])) {
|
|
657
|
+
throw new Error("Polyfilled proxy package does not match its validated plan");
|
|
658
|
+
}
|
|
659
|
+
for (const path of report.applied) {
|
|
660
|
+
if (files[path] !== null && /\.[cm]?js$/.test(path)) {
|
|
661
|
+
(options.execFileSync ?? nodeExecFileSync)(selection.nodePath, ["--check", join(proxyPackageRoot(selection.entryScript), path)], { timeout: 10_000, stdio: "pipe" });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return selection;
|
|
665
|
+
};
|
|
666
|
+
const staging = await mkdtemp(join(options.packagesDir, `.polyfill-${options.version}-`));
|
|
667
|
+
try {
|
|
668
|
+
await cp(resolve(candidateRoot, "../../.."), staging, {
|
|
669
|
+
recursive: true,
|
|
670
|
+
mode: constants.COPYFILE_FICLONE,
|
|
671
|
+
verbatimSymlinks: true,
|
|
672
|
+
});
|
|
673
|
+
const root = join(staging, "node_modules", "@juspay", "neurolink");
|
|
674
|
+
for (const [path, content] of Object.entries(files)) {
|
|
675
|
+
if (content === null) {
|
|
676
|
+
await rm(join(root, path));
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
writeProxyPolyfilledFile(root, path, content);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
validateCopy(staging);
|
|
683
|
+
assertLocalUnchanged();
|
|
684
|
+
await rename(staging, destination);
|
|
685
|
+
const selection = validateCopy(destination);
|
|
686
|
+
options.onPolyfills?.(report);
|
|
687
|
+
return selection;
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
await rm(staging, { recursive: true, force: true });
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
finally {
|
|
694
|
+
await rm(fresh.container, { recursive: true, force: true });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
550
697
|
/** Only operator runtime settings survive reinstall, never worker IPC identity. */
|
|
551
698
|
export function isProxyServiceEnvironmentKey(name) {
|
|
552
699
|
return ((name.startsWith("OTEL_") ||
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ProxyPackageBaseline, ProxyPackagePolyfillReport, ProxyPackageSelection } from "../types/index.js";
|
|
2
|
+
/** Locate the package without importing its executable or provider graph. */
|
|
3
|
+
export declare function proxyPackageRoot(entryScript: string): string;
|
|
4
|
+
/** Bounded baseline of shipped proxy code; operator config and dependencies are excluded. */
|
|
5
|
+
export declare function snapshotProxyPackage(selection: ProxyPackageSelection): ProxyPackageBaseline;
|
|
6
|
+
/** Invalid or missing baseline must never be silently replaced with edited files. */
|
|
7
|
+
export declare function readProxyPackageBaseline(selection: ProxyPackageSelection): ProxyPackageBaseline | null;
|
|
8
|
+
export declare function writeProxyPackageBaseline(selection: ProxyPackageSelection, baseline: ProxyPackageBaseline): void;
|
|
9
|
+
/** Operator supplies the pristine base for a legacy local build; this does not activate it. */
|
|
10
|
+
export declare function captureProxyPackageBaseline(base: ProxyPackageSelection, patched: ProxyPackageSelection): string[];
|
|
11
|
+
export declare function changedProxyRuntimeFiles(base: ProxyPackageBaseline, current: ProxyPackageBaseline): string[];
|
|
12
|
+
/** Apply exact local hunks, leaving independent upstream changes intact. */
|
|
13
|
+
export declare function reconcileProxyPackagePolyfills(base: ProxyPackageBaseline, local: ProxyPackageBaseline, candidate: ProxyPackageBaseline): {
|
|
14
|
+
files: Record<string, string | null>;
|
|
15
|
+
report: ProxyPackagePolyfillReport;
|
|
16
|
+
};
|
|
17
|
+
/** Writes are confined to a new staged copy, never the serving package. */
|
|
18
|
+
export declare function writeProxyPolyfilledFile(root: string, path: string, content: string): void;
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, writeFileSync, renameSync, mkdirSync, } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { applyPatch, structuredPatch } from "diff";
|
|
5
|
+
const BASELINE_NAME = ".neurolink-proxy-baseline.json";
|
|
6
|
+
const MAX_FILES = 4096;
|
|
7
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
8
|
+
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
9
|
+
const MAX_CHANGED_FILES = 64;
|
|
10
|
+
/** Locate the package without importing its executable or provider graph. */
|
|
11
|
+
export function proxyPackageRoot(entryScript) {
|
|
12
|
+
let directory = dirname(realpathSync(entryScript));
|
|
13
|
+
for (;;) {
|
|
14
|
+
const manifest = join(directory, "package.json");
|
|
15
|
+
if (existsSync(manifest)) {
|
|
16
|
+
let value;
|
|
17
|
+
try {
|
|
18
|
+
value = JSON.parse(readFileSync(manifest, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error("Proxy package manifest is unreadable or invalid");
|
|
22
|
+
}
|
|
23
|
+
if (value &&
|
|
24
|
+
typeof value === "object" &&
|
|
25
|
+
"name" in value &&
|
|
26
|
+
value.name === "@juspay/neurolink") {
|
|
27
|
+
return directory;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const parent = dirname(directory);
|
|
31
|
+
if (parent === directory) {
|
|
32
|
+
throw new Error("Proxy package manifest not found");
|
|
33
|
+
}
|
|
34
|
+
directory = parent;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function sorted(value) {
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
return value.map(sorted);
|
|
40
|
+
}
|
|
41
|
+
if (value && typeof value === "object") {
|
|
42
|
+
return Object.fromEntries(Object.entries(value)
|
|
43
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
44
|
+
.map(([key, item]) => [key, sorted(item)]));
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function safePath(path) {
|
|
49
|
+
return (!isAbsolute(path) &&
|
|
50
|
+
!path.includes("\\") &&
|
|
51
|
+
path
|
|
52
|
+
.split("/")
|
|
53
|
+
.every((part) => part !== "" && part !== "." && part !== ".."));
|
|
54
|
+
}
|
|
55
|
+
function runtimePath(path, entryRelative) {
|
|
56
|
+
return (safePath(path) &&
|
|
57
|
+
/\.(?:[cm]?js|json)$/.test(path) &&
|
|
58
|
+
(path === entryRelative ||
|
|
59
|
+
(path.startsWith("dist/") && !path.startsWith("dist/browser/")) ||
|
|
60
|
+
path.startsWith("scripts/observability/")));
|
|
61
|
+
}
|
|
62
|
+
/** Bounded baseline of shipped proxy code; operator config and dependencies are excluded. */
|
|
63
|
+
export function snapshotProxyPackage(selection) {
|
|
64
|
+
const root = proxyPackageRoot(selection.entryScript);
|
|
65
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
66
|
+
// Release numbers and npm bookkeeping are not runtime compatibility edits.
|
|
67
|
+
const comparable = Object.fromEntries(Object.entries(manifest).filter(([key]) => key !== "version" && key !== "gitHead" && !key.startsWith("_")));
|
|
68
|
+
const entryRelative = relative(root, realpathSync(selection.entryScript))
|
|
69
|
+
.split("\\")
|
|
70
|
+
.join("/");
|
|
71
|
+
if (!safePath(entryRelative)) {
|
|
72
|
+
throw new Error("Proxy executable escapes package");
|
|
73
|
+
}
|
|
74
|
+
const files = {};
|
|
75
|
+
let bytes = 0;
|
|
76
|
+
const read = (path) => {
|
|
77
|
+
if (!runtimePath(path, entryRelative) || Object.hasOwn(files, path)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const file = join(root, path);
|
|
81
|
+
const stat = lstatSync(file);
|
|
82
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
83
|
+
throw new Error(`Proxy runtime file is not a regular file: ${path}`);
|
|
84
|
+
}
|
|
85
|
+
if (stat.size > MAX_FILE_BYTES ||
|
|
86
|
+
bytes + stat.size > MAX_TOTAL_BYTES ||
|
|
87
|
+
Object.keys(files).length >= MAX_FILES) {
|
|
88
|
+
throw new Error("Proxy runtime baseline exceeds its size limit");
|
|
89
|
+
}
|
|
90
|
+
const data = readFileSync(file);
|
|
91
|
+
files[path] = new TextDecoder("utf-8", {
|
|
92
|
+
fatal: true,
|
|
93
|
+
ignoreBOM: true,
|
|
94
|
+
}).decode(data);
|
|
95
|
+
bytes += data.length;
|
|
96
|
+
};
|
|
97
|
+
const walk = (path) => {
|
|
98
|
+
if (!existsSync(join(root, path))) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (lstatSync(join(root, path)).isSymbolicLink()) {
|
|
102
|
+
throw new Error(`Proxy runtime directory is a symbolic link: ${path}`);
|
|
103
|
+
}
|
|
104
|
+
for (const item of readdirSync(join(root, path), {
|
|
105
|
+
withFileTypes: true,
|
|
106
|
+
}).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
107
|
+
const next = `${path}/${item.name}`;
|
|
108
|
+
if (next === "dist/browser" || item.name === "node_modules") {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (item.isSymbolicLink()) {
|
|
112
|
+
throw new Error(`Proxy runtime path is a symbolic link: ${next}`);
|
|
113
|
+
}
|
|
114
|
+
if (item.isDirectory()) {
|
|
115
|
+
walk(next);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
read(next);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
walk("dist");
|
|
123
|
+
walk("scripts/observability");
|
|
124
|
+
read(entryRelative);
|
|
125
|
+
return {
|
|
126
|
+
schemaVersion: 1,
|
|
127
|
+
version: selection.version,
|
|
128
|
+
manifest: JSON.stringify(sorted(comparable)),
|
|
129
|
+
entryRelative,
|
|
130
|
+
files,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Invalid or missing baseline must never be silently replaced with edited files. */
|
|
134
|
+
export function readProxyPackageBaseline(selection) {
|
|
135
|
+
const path = join(proxyPackageRoot(selection.entryScript), BASELINE_NAME);
|
|
136
|
+
if (!existsSync(path)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const stat = lstatSync(path);
|
|
140
|
+
if (!stat.isFile() ||
|
|
141
|
+
stat.isSymbolicLink() ||
|
|
142
|
+
stat.size > MAX_TOTAL_BYTES * 2) {
|
|
143
|
+
throw new Error("Proxy package baseline is invalid");
|
|
144
|
+
}
|
|
145
|
+
let value;
|
|
146
|
+
try {
|
|
147
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new Error("Proxy package baseline is unreadable or invalid");
|
|
151
|
+
}
|
|
152
|
+
if (value.schemaVersion !== 1 ||
|
|
153
|
+
typeof value.version !== "string" ||
|
|
154
|
+
typeof value.manifest !== "string" ||
|
|
155
|
+
typeof value.entryRelative !== "string" ||
|
|
156
|
+
!safePath(value.entryRelative) ||
|
|
157
|
+
!value.files ||
|
|
158
|
+
typeof value.files !== "object" ||
|
|
159
|
+
Array.isArray(value.files)) {
|
|
160
|
+
throw new Error("Proxy package baseline is invalid");
|
|
161
|
+
}
|
|
162
|
+
let bytes = 0;
|
|
163
|
+
const entries = Object.entries(value.files);
|
|
164
|
+
if (entries.length > MAX_FILES) {
|
|
165
|
+
throw new Error("Proxy package baseline exceeds its size limit");
|
|
166
|
+
}
|
|
167
|
+
for (const [name, content] of entries) {
|
|
168
|
+
if (!runtimePath(name, value.entryRelative) ||
|
|
169
|
+
typeof content !== "string" ||
|
|
170
|
+
Buffer.byteLength(content) > MAX_FILE_BYTES) {
|
|
171
|
+
throw new Error("Proxy package baseline contains an invalid file");
|
|
172
|
+
}
|
|
173
|
+
bytes += Buffer.byteLength(content);
|
|
174
|
+
}
|
|
175
|
+
if (bytes > MAX_TOTAL_BYTES) {
|
|
176
|
+
throw new Error("Proxy package baseline exceeds its size limit");
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
export function writeProxyPackageBaseline(selection, baseline) {
|
|
181
|
+
const path = join(proxyPackageRoot(selection.entryScript), BASELINE_NAME);
|
|
182
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
183
|
+
writeFileSync(temporary, JSON.stringify(baseline), {
|
|
184
|
+
mode: 0o600,
|
|
185
|
+
flag: "wx",
|
|
186
|
+
});
|
|
187
|
+
renameSync(temporary, path);
|
|
188
|
+
}
|
|
189
|
+
/** Operator supplies the pristine base for a legacy local build; this does not activate it. */
|
|
190
|
+
export function captureProxyPackageBaseline(base, patched) {
|
|
191
|
+
const original = snapshotProxyPackage(base);
|
|
192
|
+
const current = snapshotProxyPackage(patched);
|
|
193
|
+
if (original.manifest !== current.manifest ||
|
|
194
|
+
original.entryRelative !== current.entryRelative) {
|
|
195
|
+
throw new Error("Local package metadata differs from its base; runtime polyfills cannot change dependencies or the executable");
|
|
196
|
+
}
|
|
197
|
+
const existing = readProxyPackageBaseline(patched);
|
|
198
|
+
if (existing &&
|
|
199
|
+
(existing.manifest !== original.manifest ||
|
|
200
|
+
JSON.stringify(sorted(existing.files)) !==
|
|
201
|
+
JSON.stringify(sorted(original.files)))) {
|
|
202
|
+
throw new Error("A different proxy package baseline is already recorded");
|
|
203
|
+
}
|
|
204
|
+
const changed = changedProxyRuntimeFiles(original, current);
|
|
205
|
+
writeProxyPackageBaseline(patched, original);
|
|
206
|
+
return changed;
|
|
207
|
+
}
|
|
208
|
+
export function changedProxyRuntimeFiles(base, current) {
|
|
209
|
+
if (base.manifest !== current.manifest ||
|
|
210
|
+
base.entryRelative !== current.entryRelative) {
|
|
211
|
+
throw new Error("Local package metadata changed; refusing upgrade rather than discarding it");
|
|
212
|
+
}
|
|
213
|
+
return [
|
|
214
|
+
...new Set([...Object.keys(base.files), ...Object.keys(current.files)]),
|
|
215
|
+
]
|
|
216
|
+
.sort()
|
|
217
|
+
.filter((path) => base.files[path] !== current.files[path]);
|
|
218
|
+
}
|
|
219
|
+
function occurrences(content, lines) {
|
|
220
|
+
if (!lines.length) {
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
const haystack = content.split("\n");
|
|
224
|
+
let count = 0;
|
|
225
|
+
for (let start = 0; start + lines.length <= haystack.length; start++) {
|
|
226
|
+
if (lines.every((line, offset) => line === haystack[start + offset])) {
|
|
227
|
+
count++;
|
|
228
|
+
}
|
|
229
|
+
if (count > 1) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return count;
|
|
234
|
+
}
|
|
235
|
+
/** Apply exact local hunks, leaving independent upstream changes intact. */
|
|
236
|
+
export function reconcileProxyPackagePolyfills(base, local, candidate) {
|
|
237
|
+
const changed = changedProxyRuntimeFiles(base, local);
|
|
238
|
+
if (changed.length > MAX_CHANGED_FILES) {
|
|
239
|
+
throw new Error("Proxy polyfill exceeds the 64-file compatibility diff limit");
|
|
240
|
+
}
|
|
241
|
+
const files = {};
|
|
242
|
+
const report = {
|
|
243
|
+
applied: [],
|
|
244
|
+
alreadyIncluded: [],
|
|
245
|
+
};
|
|
246
|
+
for (const path of changed) {
|
|
247
|
+
if (!runtimePath(path, candidate.entryRelative)) {
|
|
248
|
+
throw new Error(`Proxy polyfill path is outside the candidate runtime: ${path}`);
|
|
249
|
+
}
|
|
250
|
+
const before = base.files[path];
|
|
251
|
+
const after = local.files[path];
|
|
252
|
+
const upstream = candidate.files[path];
|
|
253
|
+
if (upstream === after) {
|
|
254
|
+
report.alreadyIncluded.push(path);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (upstream === before) {
|
|
258
|
+
files[path] = after ?? null;
|
|
259
|
+
report.applied.push(path);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (before === undefined ||
|
|
263
|
+
after === undefined ||
|
|
264
|
+
upstream === undefined ||
|
|
265
|
+
before === "" ||
|
|
266
|
+
after === "") {
|
|
267
|
+
throw new Error(`Proxy polyfill conflicts with the release: ${path}`);
|
|
268
|
+
}
|
|
269
|
+
const patch = structuredPatch(path, path, before, after, "", "", {
|
|
270
|
+
context: 3,
|
|
271
|
+
timeout: 1000,
|
|
272
|
+
maxEditLength: 20_000,
|
|
273
|
+
});
|
|
274
|
+
if (!patch || patch.hunks.length > 128) {
|
|
275
|
+
throw new Error(`Proxy polyfill exceeds its diff budget: ${path}`);
|
|
276
|
+
}
|
|
277
|
+
let content = upstream;
|
|
278
|
+
let applied = false;
|
|
279
|
+
for (const hunk of patch.hunks) {
|
|
280
|
+
const oldLines = hunk.lines
|
|
281
|
+
.filter((line) => line[0] === " " || line[0] === "-")
|
|
282
|
+
.map((line) => line.slice(1));
|
|
283
|
+
const newLines = hunk.lines
|
|
284
|
+
.filter((line) => line[0] === " " || line[0] === "+")
|
|
285
|
+
.map((line) => line.slice(1));
|
|
286
|
+
const oldMatches = occurrences(content, oldLines);
|
|
287
|
+
const newMatches = occurrences(content, newLines);
|
|
288
|
+
if (newMatches === 1 && oldMatches === 0) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (oldMatches !== 1 || newMatches !== 0) {
|
|
292
|
+
throw new Error(`Proxy polyfill conflicts with the release: ${path}`);
|
|
293
|
+
}
|
|
294
|
+
const result = applyPatch(content, { ...patch, hunks: [hunk] }, { fuzzFactor: 0 });
|
|
295
|
+
if (result === false) {
|
|
296
|
+
throw new Error(`Proxy polyfill conflicts with the release: ${path}`);
|
|
297
|
+
}
|
|
298
|
+
content = result;
|
|
299
|
+
applied = true;
|
|
300
|
+
}
|
|
301
|
+
if (applied) {
|
|
302
|
+
files[path] = content;
|
|
303
|
+
report.applied.push(path);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
report.alreadyIncluded.push(path);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { files, report };
|
|
310
|
+
}
|
|
311
|
+
/** Writes are confined to a new staged copy, never the serving package. */
|
|
312
|
+
export function writeProxyPolyfilledFile(root, path, content) {
|
|
313
|
+
root = realpathSync(root);
|
|
314
|
+
if (!safePath(path)) {
|
|
315
|
+
throw new Error("Invalid proxy polyfill path");
|
|
316
|
+
}
|
|
317
|
+
const target = resolve(root, path);
|
|
318
|
+
if (!target.startsWith(`${realpathSync(root)}/`)) {
|
|
319
|
+
throw new Error("Proxy polyfill escapes its package");
|
|
320
|
+
}
|
|
321
|
+
// A candidate cannot redirect a patch through a symlinked parent.
|
|
322
|
+
let parent = dirname(target);
|
|
323
|
+
while (parent !== root) {
|
|
324
|
+
if (existsSync(parent) && lstatSync(parent).isSymbolicLink()) {
|
|
325
|
+
throw new Error("Proxy polyfill parent is a symbolic link");
|
|
326
|
+
}
|
|
327
|
+
parent = dirname(parent);
|
|
328
|
+
}
|
|
329
|
+
if (existsSync(target) && lstatSync(target).isSymbolicLink()) {
|
|
330
|
+
throw new Error("Proxy polyfill target is a symbolic link");
|
|
331
|
+
}
|
|
332
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
333
|
+
writeFileSync(target, content);
|
|
334
|
+
}
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -4437,6 +4437,32 @@ export type ProxyStagedInstallOptions = {
|
|
|
4437
4437
|
outputBytes: number;
|
|
4438
4438
|
}) => void;
|
|
4439
4439
|
};
|
|
4440
|
+
/** Original proxy runtime files retained before any local compatibility edits. */
|
|
4441
|
+
export type ProxyPackageBaseline = {
|
|
4442
|
+
schemaVersion: 1;
|
|
4443
|
+
version: string;
|
|
4444
|
+
manifest: string;
|
|
4445
|
+
entryRelative: string;
|
|
4446
|
+
files: Record<string, string>;
|
|
4447
|
+
};
|
|
4448
|
+
/** A candidate carries only local diff hunks that its release still needs. */
|
|
4449
|
+
export type ProxyPackagePolyfillReport = {
|
|
4450
|
+
applied: string[];
|
|
4451
|
+
alreadyIncluded: string[];
|
|
4452
|
+
};
|
|
4453
|
+
/** Staged upgrade that reconciles recorded local edits before publication. */
|
|
4454
|
+
export type ProxyPackageUpgradeOptions = ProxyStagedInstallOptions & {
|
|
4455
|
+
activePackage: ProxyPackageSelection;
|
|
4456
|
+
isCurrentOwner?: () => boolean;
|
|
4457
|
+
onPolyfills?: (report: ProxyPackagePolyfillReport) => void;
|
|
4458
|
+
};
|
|
4459
|
+
/** Capture a trusted baseline for a locally patched package without activation. */
|
|
4460
|
+
export type ProxyPolyfillArgs = {
|
|
4461
|
+
action: "capture";
|
|
4462
|
+
base: string;
|
|
4463
|
+
patched: string;
|
|
4464
|
+
format: "text" | "json";
|
|
4465
|
+
};
|
|
4440
4466
|
/** Saved launchd settings retained when installing the service again. */
|
|
4441
4467
|
export type ProxyServiceInstallSettings = {
|
|
4442
4468
|
envFile?: string;
|
|
@@ -2289,9 +2289,10 @@
|
|
|
2289
2289
|
{"objectID":"9d37e7d66f60fa6af16eaee381757f1d39b03c29d805748d72980fc766a80afc","title":"Acceptance evidence","url":"/docs/development/proxy-incident-verification#acceptance-evidence","content":"Local verification on macOS, Node 24.14.1, September 7, 2026. All network fixtures\nuse ephemeral loopback listeners and isolated storage. No installed proxy,\nprovider account, launchd service, or production request was used.\n\n| Requirement | Evidence |\n| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Delayed acceptance gets its own full commit deadline | Actual child-process IPC fixture; no failed transfer or worker replacement |\n| A failed commit preserves unrelated streams | Actual shipped worker runtime; affected connection closes, other stream delivers every byte, replacement serves requests; commit timeouts and IPC write failures retain distinct reasons |\n| Accepted work remains identifiable after worker death | Confirmed admission append, fixture SIGKILL, independent parent exit journal, built analyzer joins by process instance |\n| Unknown outcomes remain unknown | Missing terminal plus worker exit is reported as unconfirmedAtWorkerExit; conflicting exit evidence is excluded |\n| Storage failures cannot dispatch unrecorded upstream work | HTTP fixture returns classified 503 after admission write failure; upstream invocation count stays zero |\n| Time-window analysis does not f","hierarchy":{"lvl0":"Development","lvl1":"Proxy incident reliability verification","lvl2":"Acceptance evidence","lvl3":""}},
|
|
2290
2290
|
{"objectID":"e445d51bde52184ea947c95c6b1f2105e1c1dd67b2ed01509cbfa320d3a396db","title":"Performance observations","url":"/docs/development/proxy-incident-verification#performance-observations","content":"The existing lifecycle gate wrote 100,000 events for 25,000 requests with zero\ndrops or uncertain writes. Enqueue p95 was 12 microseconds; added response-tracking\np95 was 49 microseconds. The statistics gate reconciled 50,000 requests exactly.\nThe loopback transport gate added 1.69 ms p95 against its 5 ms budget.\n\nThe first rolling gate preserved all traffic but failed the sustained latency\nbudget: 51.28 ms added p95 against 25 ms. We then ran three alternating comparisons\nagainst unchanged release 382217975c39433df058e2b2c2fb91b283230725, using the\nsame fixture, machine and background scheduling priority. No budgets were changed.\n\n| Round | Release added p95 under sustained load | Patched added p95 under sustained load |\n| ----- | -------------------------------------: | -------------------------------------: |\n| 1 | 6.042 ms | 24.840 ms |\n| 2 | 6.826 ms | 5.355 ms |\n| 3 | 9.827 ms | 4.811 ms |\n\nEvery comparison passed the existing budgets, preserved the active stream, and\nreported zero dropped requests. The fixture includes 100 concurrent handoff\nrequests and 64-way sustained traffic. These observations expose variability;\nthey do not certify a latency ceiling under arbitrary host contention.","hierarchy":{"lvl0":"Development","lvl1":"Proxy incident reliability verification","lvl2":"Performance observations","lvl3":""}},
|
|
2291
2291
|
{"objectID":"5cb2fe62f38e8943b94facdd1ddd6a4a968fbbb161744152fe47c3c269d575cf","title":"Interpretation limits","url":"/docs/development/proxy-incident-verification#interpretation-limits","content":"The CLI has one signal owner. Proxy workers and supervisors own their drain and\nexit; the ordinary CLI telemetry cleanup cannot exit alongside them. Repeated\nSIGINT/SIGTERM signals reuse that shutdown. Ordinary command cleanup catches\nflush failures and uses a five-second deadline. Worker and supervisor telemetry\ncleanup also has a deadline after request draining. Supervisor SIGHUP forwards a\nreload to the ready serving worker, while a candidate loads configuration during\nstartup. Isolated subprocess fixtures in test:proxy-restart exercise reload,\nduplicate signals, failed flushes, stalled flushes and drain failures. If startup\nregisters a shutdown owner during ordinary cleanup, the original signal is\ndelegated to that owner after cleanup settles, including rejection or timeout.\nSignal-triggered ordinary cleanup preserves an existing command failure code.\nSupervisor SIGHUP handling is installed before startup awaits, ignores reloads\nuntil a serving worker is ready, and forwards them during control-plane setup.\nSupervisor shutdown ownership is also registered before startup awaits. A stop\nprevents later setup stages, waits for already-started resource creation, and\ncloses the resulting resources before exiting. Cleanup continues after an\nindividual close failure and exits unsuccessfully when a resource cannot close.\nIsolated child-process regressions signal bootstrap, pending worker startup,\npending restart-control setup, and updater startup, including duplicate signals\nand a rejected control close.\nResource cleanup runs concurrently: worker creation and drain have a 35-second\ndeadline, and control cleanup has a 5-second deadline. Telemetry then has its\n5-second budget, leaving headroom inside the launchd service's 45-second stop\ntimeout. A stalled resource is reported and causes an unsuccessful exit; a late\nresource returned while telemetry is flushing is still closed without extending\nthe exit deadline. Fixtures cover permanently pending worker/control setup and a\n","hierarchy":{"lvl0":"Development","lvl1":"Proxy incident reliability verification","lvl2":"Interpretation limits","lvl3":""}},
|
|
2292
|
-
{"objectID":"e8a7f76ea9249969c4e7f7f8767fd2cac18478baca29ee921e7a7ca73825db69","title":"Proxy package updates and token budgets","url":"/docs/development/proxy-updates-and-token-budgets","content":"Proxy package updates and token budgets\n\nAutomatic updates prepare a separate package before asking the supervisor to\nactivate it. They do not run a global package replacement over files used by\nserving workers. These changes take effect after the release is installed and\nits supervisor is activated; merging a PR does not change a running service.\n\nUpdate and restart behavior\n\nThe updater installs the current running version, when needed for rollback, and\nthe candidate under ~/.neurolink/proxy-packages/<version>. Installation uses a\nprivate temporary directory and publishes the version directory only after the\npackage name, version, executable location, and JavaScript syntax validate.\nA candidate with missing dependencies can still fail startup: actual worker\nreadiness remains the activation gate, and the serving generation is retained\nwhen replacement fails.\n\nThe package-manager process runs asynchronously. Output progress renews a\ntwo-minute inactivity deadline; a fifteen-minute maximum still bounds an\ninstaller that emits output forever. A timed-out installer and its process group\nare stopped, its unpublished directory is removed, and transient errors receive\nthe updater's bounded retry schedule. Progress telemetry contains byte counts and\ntiming, not raw package-manager output that may contain authenticated URLs.\n\nThe stable launcher selects one validated package. Its --version check reads\npackage metadata in a minimal Node process; it does not initialize provider,\ntelemetry, or proxy code. Each worker is spawned from its expected package, so a\nnew candidate cannot silently change the recovery executable of an older worker.\nRollback restores the previous selection rather than reinstalling over live\nfiles. Proxy auto-update does not change the global CLI package; invoking an older\nglobal CLI to reinstall the service retains the selected proxy package instead\nof downgrading it or rejecting its version. Version directories are intentionally retained: they can still be needed\nby active/draining generations or rollback. This increases package disk usage;\nthere is currently no automatic garbage collection of retained versions.\n\nBefore refreshing a supervisor, all of the following must be settled:\nActive requests in the current worker.\nEvery draining worker generation.\nQueued sockets and pending socket transfers.\nAny candidate worker.\n\nIncomplete rolling status is not evidence of idleness. A drain attempt lasts at\nmost thirty seconds before deferral and admission recovery; it never kills an\nolder stream to make the update proceed. The worker also has a ninety-second\nadmission recovery lease if the updater disappears. Package installation and\nvalidation finish before a legacy service closes admission.\n\nUse neurolink proxy restart --check for a read-only restart preflight and\nneurolink proxy restart for a supervisor-owned rolling replacement. A failed\ncheck does not fall back to a forced process restart. The result distinguishes a\nverified replacement from an activated worker whose final verification failed.\nA worker restart does not, by itself, upgrade the supervisor.\n\nStatus separates diskPackageVersion (metadata at the executable used by the\nreporting process), selectedPackageVersion (next launcher selection),\nvalidatedVersion (last updater-validated package), candidateVersion, the\nserving worker version, and the supervisor version. A changed disk manifest or a\nvalidated package does not prove live adoption.\n\nReinstall first refuses any running worker, draining generation, or loaded launchd\njob before writing files or changing processes. Unknown/permission-denied process\nor launchd state also refuses. Use the rolling restart command for a live service;\nexplicitly stop and unload it before an intended reinstall.\n\nFor a stopped service, reinstall reads the saved definition before writing it. Existing\nhost/port, environment-file path, routing-config path, and operator OTel/proxy\nvariables become defaults; explicit arguments and current environment values\nwin. An unreadable service definition aborts reinstall instead of silently\nremoving its settings. Worker IPC identity is not persisted. This includes\nNEUROLINK_PROXY_OTLP_BODIES_ENDPOINT, NEUROLINK_PROXY_CONTEXT_POLICY, and\nNEUROLINK_PROXY_TOKEN_BUDGET.\n\
|
|
2292
|
+
{"objectID":"e8a7f76ea9249969c4e7f7f8767fd2cac18478baca29ee921e7a7ca73825db69","title":"Proxy package updates and token budgets","url":"/docs/development/proxy-updates-and-token-budgets","content":"Proxy package updates and token budgets\n\nAutomatic updates prepare a separate package before asking the supervisor to\nactivate it. They do not run a global package replacement over files used by\nserving workers. These changes take effect after the release is installed and\nits supervisor is activated; merging a PR does not change a running service.\n\nUpdate and restart behavior\n\nThe updater installs the current running version, when needed for rollback, and\nthe candidate under ~/.neurolink/proxy-packages/<version>. Installation uses a\nprivate temporary directory and publishes the version directory only after the\npackage name, version, executable location, and JavaScript syntax validate.\nA candidate with missing dependencies can still fail startup: actual worker\nreadiness remains the activation gate, and the serving generation is retained\nwhen replacement fails.\n\nThe package-manager process runs asynchronously. Output progress renews a\ntwo-minute inactivity deadline; a fifteen-minute maximum still bounds an\ninstaller that emits output forever. A timed-out installer and its process group\nare stopped, its unpublished directory is removed, and transient errors receive\nthe updater's bounded retry schedule. Progress telemetry contains byte counts and\ntiming, not raw package-manager output that may contain authenticated URLs.\n\nThe stable launcher selects one validated package. Its --version check reads\npackage metadata in a minimal Node process; it does not initialize provider,\ntelemetry, or proxy code. Each worker is spawned from its expected package, so a\nnew candidate cannot silently change the recovery executable of an older worker.\nRollback restores the previous selection rather than reinstalling over live\nfiles. Proxy auto-update does not change the global CLI package; invoking an older\nglobal CLI to reinstall the service retains the selected proxy package instead\nof downgrading it or rejecting its version. Version directories are intentionally retained: they can still be needed\nby active/draining generations or rollback. This increases package disk usage;\nthere is currently no automatic garbage collection of retained versions.\n\nBefore refreshing a supervisor, all of the following must be settled:\nActive requests in the current worker.\nEvery draining worker generation.\nQueued sockets and pending socket transfers.\nAny candidate worker.\n\nIncomplete rolling status is not evidence of idleness. A drain attempt lasts at\nmost thirty seconds before deferral and admission recovery; it never kills an\nolder stream to make the update proceed. The worker also has a ninety-second\nadmission recovery lease if the updater disappears. Package installation and\nvalidation finish before a legacy service closes admission.\n\nUse neurolink proxy restart --check for a read-only restart preflight and\nneurolink proxy restart for a supervisor-owned rolling replacement. A failed\ncheck does not fall back to a forced process restart. The result distinguishes a\nverified replacement from an activated worker whose final verification failed.\nA worker restart does not, by itself, upgrade the supervisor.\n\nStatus separates diskPackageVersion (metadata at the executable used by the\nreporting process), selectedPackageVersion (next launcher selection),\nvalidatedVersion (last updater-validated package), candidateVersion, the\nserving worker version, and the supervisor version. A changed disk manifest or a\nvalidated package does not prove live adoption.\n\nReinstall first refuses any running worker, draining generation, or loaded launchd\njob before writing files or changing processes. Unknown/permission-denied process\nor launchd state also refuses. Use the rolling restart command for a live service;\nexplicitly stop and unload it before an intended reinstall.\n\nFor a stopped service, reinstall reads the saved definition before writing it. Existing\nhost/port, environment-file path, routing-config path, and operator OTel/proxy\nvariables become defaults; explicit arguments and current environment values\nwin. An unreadable service definition aborts reinstall instead of silently\nremoving its settings. Worker IPC identity is not persisted. This includes\nNEUROLINK_PROXY_OTLP_BODIES_ENDPOINT, NEUROLINK_PROXY_CONTEXT_POLICY, and\nNEUROLINK_PROXY_TOKEN_BUDGET.\n\nLocal compatibility polyfills\n\nAn automatic package upgrade leaves the routing config, environment file, and\nlaunchd definition intact. It also preserves the exact previous package path,\nincluding when a local build and an official build share a version number.\nManaged service starts and worker replacements do not reapply CLI client\nconfiguration. User changes to Codex, Claude Code, and other client settings\nremain in place. Use explicit proxy setup or foreground proxy start to\nconfigure client routing.\n\nEach freshly staged package records its original proxy runtime files. When a\nlater upgrade is available, the updater compares the serving package with that\nbaseline and applies only the remaining local diff hunk","hierarchy":{"lvl0":"Development","lvl1":"Proxy package updates and token budgets","lvl2":"","lvl3":""}},
|
|
2293
2293
|
{"objectID":"d8b758e463be06d23598c7f1722e82d46f3d5f2df3c3c98058f400923f83ac8f","title":"Proxy package updates and token budgets","url":"/docs/development/proxy-updates-and-token-budgets#proxy-package-updates-and-token-budgets","content":"Automatic updates prepare a separate package before asking the supervisor to\nactivate it. They do not run a global package replacement over files used by\nserving workers. These changes take effect after the release is installed and\nits supervisor is activated; merging a PR does not change a running service.","hierarchy":{"lvl0":"Development","lvl1":"Proxy package updates and token budgets","lvl2":"Proxy package updates and token budgets","lvl3":""}},
|
|
2294
2294
|
{"objectID":"b31737c456bf0dd066337629bccccd40c4572b96ad3b51fdf48de2c44d45e82a","title":"Update and restart behavior","url":"/docs/development/proxy-updates-and-token-budgets#update-and-restart-behavior","content":"The updater installs the current running version, when needed for rollback, and\nthe candidate under ~/.neurolink/proxy-packages/<version>. Installation uses a\nprivate temporary directory and publishes the version directory only after the\npackage name, version, executable location, and JavaScript syntax validate.\nA candidate with missing dependencies can still fail startup: actual worker\nreadiness remains the activation gate, and the serving generation is retained\nwhen replacement fails.\n\nThe package-manager process runs asynchronously. Output progress renews a\ntwo-minute inactivity deadline; a fifteen-minute maximum still bounds an\ninstaller that emits output forever. A timed-out installer and its process group\nare stopped, its unpublished directory is removed, and transient errors receive\nthe updater's bounded retry schedule. Progress telemetry contains byte counts and\ntiming, not raw package-manager output that may contain authenticated URLs.\n\nThe stable launcher selects one validated package. Its --version check reads\npackage metadata in a minimal Node process; it does not initialize provider,\ntelemetry, or proxy code. Each worker is spawned from its expected package, so a\nnew candidate cannot silently change the recovery executable of an older worker.\nRollback restores the previous selection rather than reinstalling over live\nfiles. Proxy auto-update does not change the global CLI package; invoking an older\nglobal CLI to reinstall the service retains the selected proxy package instead\nof downgrading it or rejecting its version. Version directories are intentionally retained: they can still be needed\nby active/draining generations or rollback. This increases package disk usage;\nthere is currently no automatic garbage collection of retained versions.\n\nBefore refreshing a supervisor, all of the following must be settled:\nActive requests in the current worker.\nEvery draining worker generation.\nQueued sockets and pending socket transfers.\nAny candidate worker.\n\nIncom","hierarchy":{"lvl0":"Development","lvl1":"Proxy package updates and token budgets","lvl2":"Update and restart behavior","lvl3":""}},
|
|
2295
|
+
{"objectID":"69e1f9556ad4141f30fa78e44edc0297f70c1dcc1e775c5955e44c912f79a0b6","title":"Local compatibility polyfills","url":"/docs/development/proxy-updates-and-token-budgets#local-compatibility-polyfills","content":"An automatic package upgrade leaves the routing config, environment file, and\nlaunchd definition intact. It also preserves the exact previous package path,\nincluding when a local build and an official build share a version number.\nManaged service starts and worker replacements do not reapply CLI client\nconfiguration. User changes to Codex, Claude Code, and other client settings\nremain in place. Use explicit proxy setup or foreground proxy start to\nconfigure client routing.\n\nEach freshly staged package records its original proxy runtime files. When a\nlater upgrade is available, the updater compares the serving package with that\nbaseline and applies only the remaining local diff hunks to a new candidate.\nIndependent release changes remain intact. A fix already present in the release\nis skipped and disappears from the local diff on subsequent upgrades. The\nserving package is never edited during this reconciliation.\n\nAn overlapping change, ambiguous patch context, unsupported dependency change,\nor invalid JavaScript aborts candidate preparation. The current worker,\nlauncher, config, and rollback selection remain in place. Automatic staging\nlogs the number of files applied or already included without logging their\ncontents. Runtime JavaScript and JSON are supported; dependency trees and\nbrowser bundles are not local proxy polyfills.\n\nClean legacy installations bootstrap their baseline by comparing with a\nseparately staged registry package of the same version. An edited legacy build\ncannot be safely compared without its actual original base, so its upgrade is\nretained until that base is recorded explicitly:\n\nThis command records the original files; it does not apply edits, select a\npackage, or restart a service. Supply the pristine package from which the local\nedit was made. A different existing baseline is refused rather than overwritten.","hierarchy":{"lvl0":"Development","lvl1":"Proxy package updates and token budgets","lvl2":"Local compatibility polyfills","lvl3":""}},
|
|
2295
2296
|
{"objectID":"82a142fcb5a4a408bcc1ef4a742c15da227f7efe0ca8039f32786bd317776acc","title":"Optional token reservations","url":"/docs/development/proxy-updates-and-token-budgets#optional-token-reservations","content":"NEUROLINK_PROXY_TOKEN_BUDGET accepts a JSON object. No token cap is enabled by\ndefault. For example, an operator could configure:\n\nThese numbers are illustrative, not recommended account allowances. Supported\nfields must be positive safe integers; an invalid configuration fails closed.\nmaxInFlightTokens is per provider/account. accountWindowTokens caps charged\ntokens in that account's fixed window. sessionWindowTokens applies across\naccounts and providers for the same client session. The default window is one\nhour when a cap is set without windowMs.\n\nBefore each native upstream attempt or translated SDK invocation, the proxy\nreserves estimated input plus the output allowance. An SDK invocation can contain\nprovider-internal retries that the proxy cannot observe individually; its single\nreservation does not prove or cap the combined tokens of those hidden retries. Tool definitions and instructions contribute to the input\nestimate. This is an estimate with recorded provenance, not an exact provider\ntokenization or a guarantee of monetary cost. A configured estimated input maximum\nis not a hard maximum on actual provider input tokens, especially for images,\naudio, and files. Model/context policy is configured\nseparately with NEUROLINK_PROXY_CONTEXT_POLICY; it must use the actual serving\nmodel. Provider-reported total input plus output replaces the estimate once when\ncomplete usage is known. Reasoning is already part of output and is not added a\nsecond time. Missing or uncertain usage retains the estimate. Cancellation\nbefore dispatch refunds the unused reservation; cancellation after dispatch does\nnot presume that the provider billed zero.\n\nActive and draining workers reserve atomically against one in-memory supervisor\ncoordinator. Outstanding reservations remain charged across a window boundary;\na reset cannot create new capacity for requests that are still running. Worker\nexit retains estimated spending for its unresolved calls while releasing its\nin-flight occupancy.","hierarchy":{"lvl0":"Development","lvl1":"Proxy package updates and token budgets","lvl2":"Optional token reservations","lvl3":""}},
|
|
2296
2297
|
{"objectID":"183568ccdb77340136a3026e2df12e9e82c5722ba3ddbf3b5749ba6c62de231b","title":"OTLP acknowledgement boundaries","url":"/docs/development/proxy-updates-and-token-budgets#otlp-acknowledgement-boundaries","content":"Both metadata and body queues serialize through OpenTelemetry's official JSON\nserializer. A response-aware HTTP sender validates the complete, bounded response\nbefore reporting transport acknowledgement. HTTP 200 with {} or a zero-rejection\npartialSuccess warning is acknowledged. A known partial rejection leaves the\nwhole batch unconfirmed because the response does not identify the rejected\nrecords; that batch is not replayed. Empty, malformed, interrupted, oversized,\nunexpected-shape or ambiguous-alias success responses are also unconfirmed and\nare not retried. Empty JSON is not {}; the zero-byte protobuf convention does\nnot apply to this JSON sender. Canonical and proto snake_case response names are\naccepted, but duplicate aliases are conservatively rejected.\n\nTransient HTTP 429/502/503/504 and transport failures before a response may retry\nwithin the existing 30-second total export deadline, with at most five attempts\nand exponential backoff with jitter even when Retry-After is zero. Retries retain the same\nserialized event IDs and bytes, enabling query-side deduplication. Retry-After\ncannot extend the deadline. Response reads are capped at 64 KiB. The exporter\npreserves merged generic/log-specific OTLP headers, gzip request compression,\nand configured CA/client certificate/key files; invalid configuration fails\nexplicitly. It requests uncompressed JSON responses and treats unsupported\nresponse encodings as unconfirmed. Diagnostics omit collector response bodies\nand credentials. Transport acknowledgement remains distinct from backend\npersistence; query reconciliation is still needed to confirm ingestion.\n\nA staged update may finish after its original supervisor/updater has been\nreplaced. Before publishing, validation rollback, or activation, the updater\nrechecks that its recorded supervisor and updater PIDs still own the service\nand that the original parent is running. Unknown ownership defers mutation.\nStale update jobs cannot overwrite or roll back a replacemen","hierarchy":{"lvl0":"Development","lvl1":"Proxy package updates and token budgets","lvl2":"OTLP acknowledgement boundaries","lvl3":""}},
|
|
2297
2298
|
{"objectID":"3d5db69fb501ec2b11df1700773d0e7648ef30c85f643c3b54975e4f45a3a05b","title":"🧪 COMPREHENSIVE TESTING & VERIFICATION PLAN","url":"/docs/development/testing-plan","content":"🧪 COMPREHENSIVE TESTING & VERIFICATION PLAN\nTest Results Documentation:\nUpdated Documentation:\n\nLighthouse Integration Testing Strategy\nDate: 2025-07-06 02:55 AM\nEstimated Duration: 3 hours total\n\n📋 TESTING OVERVIEW\n\nWhat We're Testing:\n✅ Real-time WebSocket Infrastructure - New streaming services, WebSocket server, enhanced chat\n✅ Advanced Telemetry Integration - OpenTelemetry stack (15+ dependencies, optional by default)\n✅ Voice AI Removal - Complete cleanup of voice dependencies and code\n✅ Backward Compatibility - All existing functionality preserved\n✅ New API Surface - Factory methods, exports, TypeScript interfaces\n\nCritical Success Criteria:\n✅ Zero Breaking Changes: All existing code works unchanged\n✅ Build Success: TypeScript compilation with 0 errors\n✅ Performance: \\<5% overhead when new features disabled\n✅ Optional Features: Telemetry disabled by default, WebSocket services optional\n✅ Complete Integration: New features work with existing AI providers and MCP tools\n\n🔄 PHASE A: IMMEDIATE VERIFICATION (30 minutes)\n\nPriority: CRITICAL | Blocking: Must pass before proceeding\n\nA.1 File System Verification (10 minutes)\n\nSuccess Criteria:\n✅ WebSocket infrastructure files exist\n✅ Streaming services files exist\n✅ Telemetry files exist\n✅ NO voice-related files remain\n✅ Enhanced chat files exist\n\nA.2 Build Validation (15 minutes)\n\nSuccess Criteria:\n✅ TypeScript compilation: 0 errors\n✅ Vite build: successful\n✅ CLI build: successful\n✅ publint: \"All good!\"\n✅ Package integrity: pnpm pack succeeds\n\nA.3 Dependency Verification (5 minutes)\n\nSuccess Criteria:\n✅ Voice AI dependencies: 0 found\n✅ OpenTelemetry dependencies: 15+ installed\n✅ No dependency conflicts\n✅ Package.json reflects changes\n\n🔧 PHASE B: CORE TESTING (1 hour)\n\nPriority: HIGH | Focus: New feature functionality\n\nB.1 WebSocket Infrastructure Testing (20 minutes)\n\nTests to Create:\ntest/websocket-server.test.ts\ntest/streaming-manager.test.ts\ntest/websocket-chat-handler.test.ts\n\nSuccess Criteria:\n✅ WebSocket server starts on specified port\n✅ Connection management works\n✅ Room creation/joining functional\n✅ Streaming channels operational\n✅ Error handling graceful\n\nB.2 Telemetry Integration Testing (20 minutes)\n\nTests to Create:\ntest/telemetryService.test.ts\ntest/ai-instrumentation.test.ts\ntest/mcp-instrumentation.test.ts\n\nSuccess Criteria:\n✅ Telemetry disabled by default\n✅ Telemetry enables when configured\n✅ AI operation tracking works\n✅ MCP tool instrumentation functional\n✅ Zero overhead when disabled\n\nB.3 Enhanced Chat Testing (20 minutes)\n\nTests to Create:\ntest/enhanced-chat.test.ts\ntest/chat-integration.test.ts\n\nSuccess Criteria:\n✅ Enhanced chat service creates successfully\n✅ SSE mode works\n✅ WebSocket mode works\n✅ Dual mode integration functional\n✅ Backward compatibility with existing chat\n\n🚀 PHASE C: COMPREHENSIVE VALIDATION (1 hour)\n\nPriority: HIGH | Focus: Integration and performance\n\nC.1 Existing Functionality Regression Testing (20 minutes)\n\nSuccess Criteria:\n✅ All existing tests pass\n✅ CLI commands work unchanged\n✅ SDK methods work unchanged\n✅ AI providers function correctly\n✅ MCP tools continue working\n\nC.2 Performance Impact Testing (20 minutes)\n\nSuccess Criteria:\n✅ Default performance unchanged\n✅ Performance overhead \\<5% when features enabled\n✅ Memory usage remains stable\n✅ No performance regressions\n\nC.3 Real-World Scenario Testing (20 minutes)\n\nSuccess Criteria:\n✅ WebSocket chat works end-to-end\n✅ Telemetry collects accurate data\n✅ Multi-provider scenarios work\n✅ Streaming integrations functional\n\n✅ PHASE D: FINAL VALIDATION (30 minutes)\n\nPriority: CRITICAL | Focus: Production readiness\n\nD.1 API Surface Validation (10 minutes)\n\nSuccess Criteria:\n✅ All new exports importable\n✅ TypeScript types correct\n✅ No missing dependencies\n✅ API surface consistent\n\nD.2 Documentation Synchronization (10 minutes)\n\nSuccess Criteria:\n✅ Documentation reflects actual implementation\n✅ Voice references removed/minimal\n✅ New features documented\n✅ Examples are accurate\n\nD.3 Production Deployment Readiness (10 minutes)\n\nSuccess Criteria:\n✅ Package builds correctly\n✅ Installation works\n✅ Imports work after installation\n✅ No missing files\n✅ Ready for npm publish\n\n📊 SUCCESS CRITERIA SUMMARY\n\nCritical (Must Pass):\n✅ Build Success: 0 TypeScript errors, successful compilation\n✅ Backward Compatibility: All existing functionality works unchanged\n✅ Performance: \\<5% overhead when new features disabled\n✅ Voice AI Removal: No voice dependencies or code remaining\n\nImportant (Should Pass):\n✅ WebSocket Infrastructure: Real-time services operational\n✅ Telemetry Integration: Optional monitoring works when enabled\n✅ Enhanced Chat: Dual-mode chat capabilities functional\n✅ API Consistency: New exports and types work correctly\n\nNice to Have (Can Be Fixed):\n✅ Documentation Completeness: All features documented\n✅ Example Applications: Working demos available\n✅ Performance Optimization: Further optimization opportunities\n\n🎯 EXECUTION ORDER\n\nSequential Execution Required:\nPhase A → Mu","hierarchy":{"lvl0":"Development","lvl1":"🧪 COMPREHENSIVE TESTING & VERIFICATION PLAN","lvl2":"","lvl3":""}},
|
|
@@ -2907,7 +2908,7 @@
|
|
|
2907
2908
|
{"objectID":"8ea0d60b8e33eeebfb825eb12ad9fcd0cdad610ee262ac5486db5d7fa3b8406b","title":"5xx / Transient Server Error","url":"/docs/features/claude-proxy#5xx-transient-server-error","content":"Transient errors (408, 500, 502, 503, 504, and Cloudflare 520-526/529).\nAlso matches 400 responses with api_error or overloaded_error types that wrap transient HTML content (e.g., Cloudflare error pages).\nApply bounded same-account retries, then rotate to the next account.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"5xx / Transient Server Error","lvl3":""}},
|
|
2908
2909
|
{"objectID":"195dae3c0ea28fdcfeb0fbf39c456fd580652b07fa5f2ce81b341d4279621cf8","title":"All Accounts Exhausted","url":"/docs/features/claude-proxy#all-accounts-exhausted","content":"When every account is in a cooling state:\nWalk the fallback chain (if configured).\nEach fallback uses NeuroLink's stream() pipeline with the specified provider/model.\nIf all fallbacks also fail, return a 429 with Retry-After set to the earliest account recovery time.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"All Accounts Exhausted","lvl3":""}},
|
|
2909
2910
|
{"objectID":"a9d69b153d2a12e50877017c5bfbcdb2ba932f1223183c8c311614898ae4a910","title":"Bootstrap Retry (Streaming)","url":"/docs/features/claude-proxy#bootstrap-retry-streaming","content":"For streaming requests, the proxy reads the first chunk from the upstream response before forwarding it to the client. If the first chunk is empty (indicating a failed stream), the proxy retries with the next account. This prevents Claude Code from receiving an empty SSE stream.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Bootstrap Retry (Streaming)","lvl3":""}},
|
|
2910
|
-
{"objectID":"98c795c3e6eec8f50fcb68adae0d6c62387d0ce83d06775eb4c32ff0698c7e7e","title":"Claude Code integration","url":"/docs/features/claude-proxy#claude-code-integration","content":"
|
|
2911
|
+
{"objectID":"98c795c3e6eec8f50fcb68adae0d6c62387d0ce83d06775eb4c32ff0698c7e7e","title":"Claude Code integration","url":"/docs/features/claude-proxy#claude-code-integration","content":"Foreground proxy start and explicit proxy setup configure ~/.claude/settings.json:\n\nWhen the proxy stops (Ctrl+C or SIGTERM), it removes these entries from the settings file. This means Claude Code automatically routes through the proxy when it is running and goes direct when it is not.\n\nNote: You must restart Claude Code after starting or stopping the proxy for the settings change to take effect.\n\nManaged service restarts and rolling upgrades preserve existing client settings.\nThey do not reapply routing or replace user edits. Run neurolink proxy setup\nwhen you want to configure clients explicitly, including after a standalone\nservice installation.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Claude Code integration","lvl3":""}},
|
|
2911
2912
|
{"objectID":"228facfde8c5dd950496365bfc28e6470b20eee6543317f0c3cc9b5e239a972e","title":"Proxy state file","url":"/docs/features/claude-proxy#proxy-state-file","content":"The proxy persists its running state to ~/.neurolink/proxy-state.json so that neurolink proxy status can report on it and neurolink proxy start can detect an already-running instance. The state includes PID, port, host, strategy, start time, fallback chain, enforced account allowlist, and the optional foreground fail-open guard PID.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Proxy state file","lvl3":""}},
|
|
2912
2913
|
{"objectID":"08047056092173a6d64c0fa57508b677157b3da499411f5a0783681010239ce9","title":"Fail-open guard","url":"/docs/features/claude-proxy#fail-open-guard","content":"A foreground proxy spawns one detached neurolink proxy guard that removes stale Claude Code settings after confirming its parent process has died. A launchd-managed proxy keeps restart ownership in launchd and starts a separate updater-only worker. Automatic package updates are enabled by default and can be disabled with NEUROLINK_PROXY_AUTO_UPDATE=off (also accepts 0 or false). The worker validates the global package root and executable directory and requires the package manager that owns the running installation. Rolling installations activate and health-check the new serving worker first. If the parent supervisor still reports the older version, the updater waits for a two-minute idle window, applies a bounded admission drain, and asks launchd to refresh the supervisor. Success requires both worker and supervisor to report the new version from a new parent process. The drain automatically expires if the updater disappears, and refresh failures retain the installed version for retry instead of repeatedly reinstalling it. Legacy services wait for every request and stream before their direct launchd restart. Post-install executable validation uses bounded retries, and the proxy supervises and replaces an updater worker that exits unexpectedly. Installed-but-not-running versions and stage-specific update failures remain persisted in ~/.neurolink/update-state.json. Updater diagnostics are sent through the configured proxy log sink and exposed in neurolink proxy status.","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Fail-open guard","lvl3":""}},
|
|
2913
2914
|
{"objectID":"778246a1bbbc0afc1258a50c9117e61d795099421fbd7f634d960f73aa6d3fc4","title":"Endpoints","url":"/docs/features/claude-proxy#endpoints","content":"| Method | Path | Description |\n| ------ | --------------------------- | --------------------------------------- |\n| POST | /v1/messages | Claude Messages API (main endpoint) |\n| GET | /v1/models | List available Claude models |\n| POST | /v1/messages/count_tokens | Token counting |\n| GET | /health | Health check (status, strategy, uptime) |\n| GET | /status | Detailed proxy status |","hierarchy":{"lvl0":"Features","lvl1":"Claude Proxy","lvl2":"Endpoints","lvl3":""}},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.44.
|
|
3
|
+
"version": "12.44.6",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
|
|
6
6
|
"author": {
|
|
@@ -286,7 +286,8 @@
|
|
|
286
286
|
"test:native-failure-events": "pnpm exec tsx test/continuous-test-suite-native-failure-events.ts",
|
|
287
287
|
"test:native-audio-model-aware": "pnpm exec tsx test/continuous-test-suite-native-audio-model-aware.ts",
|
|
288
288
|
"test:cli-json-output": "pnpm exec tsx test/continuous-test-suite-cli-json-output.ts",
|
|
289
|
-
"test:logging-guidelines": "pnpm exec tsx test/continuous-test-suite-logging-guidelines.ts"
|
|
289
|
+
"test:logging-guidelines": "pnpm exec tsx test/continuous-test-suite-logging-guidelines.ts",
|
|
290
|
+
"test:proxy-polyfills": "pnpm exec tsx test/continuous-test-suite-proxy-polyfills.ts"
|
|
290
291
|
},
|
|
291
292
|
"files": [
|
|
292
293
|
"dist",
|
|
@@ -444,6 +445,7 @@
|
|
|
444
445
|
"chardet": "2.1.1",
|
|
445
446
|
"croner": "^9.1.0",
|
|
446
447
|
"csv-parser": "^3.2.0",
|
|
448
|
+
"diff": "^9.0.0",
|
|
447
449
|
"dotenv": "^17.3.1",
|
|
448
450
|
"eventsource-parser": "^3.0.8",
|
|
449
451
|
"fast-xml-parser": "^5.7.0",
|
|
@@ -565,7 +567,6 @@
|
|
|
565
567
|
"@vercel/ncc": "^0.38.4",
|
|
566
568
|
"concurrently": "^9.2.1",
|
|
567
569
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
568
|
-
"diff": "^9.0.0",
|
|
569
570
|
"esbuild": "^0.28.1",
|
|
570
571
|
"eslint": "^10.0.2",
|
|
571
572
|
"glob": "^13.0.6",
|