@juspay/neurolink 9.86.4 → 9.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/adapters/video/replicateVideoHandler.js +27 -17
- package/dist/auth/tokenStore.d.ts +6 -0
- package/dist/auth/tokenStore.js +36 -4
- package/dist/browser/neurolink.min.js +417 -417
- package/dist/cli/commands/auth.js +3 -0
- package/dist/cli/commands/proxy.js +310 -107
- package/dist/lib/adapters/video/replicateVideoHandler.js +27 -17
- package/dist/lib/auth/tokenStore.d.ts +6 -0
- package/dist/lib/auth/tokenStore.js +36 -4
- package/dist/lib/proxy/accountCooldown.d.ts +5 -0
- package/dist/lib/proxy/accountCooldown.js +93 -0
- package/dist/lib/proxy/accountQuota.d.ts +3 -4
- package/dist/lib/proxy/accountQuota.js +75 -44
- package/dist/lib/proxy/accountSelection.d.ts +8 -0
- package/dist/lib/proxy/accountSelection.js +26 -0
- package/dist/lib/proxy/proxyConfig.js +24 -0
- package/dist/lib/proxy/proxyPaths.js +2 -0
- package/dist/lib/proxy/requestLogger.js +2 -0
- package/dist/lib/proxy/snapshotPersistence.js +1 -1
- package/dist/lib/proxy/sseInterceptor.js +16 -0
- package/dist/lib/proxy/streamOutcome.d.ts +3 -0
- package/dist/lib/proxy/streamOutcome.js +27 -0
- package/dist/lib/proxy/tokenRefresh.d.ts +8 -0
- package/dist/lib/proxy/tokenRefresh.js +92 -15
- package/dist/lib/proxy/updateState.js +17 -4
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +18 -3
- package/dist/lib/server/routes/claudeProxyRoutes.js +624 -223
- package/dist/lib/types/auth.d.ts +1 -1
- package/dist/lib/types/cli.d.ts +2 -0
- package/dist/lib/types/multimodal.d.ts +12 -0
- package/dist/lib/types/proxy.d.ts +49 -8
- package/dist/lib/types/subscription.d.ts +5 -0
- package/dist/proxy/accountCooldown.d.ts +5 -0
- package/dist/proxy/accountCooldown.js +92 -0
- package/dist/proxy/accountQuota.d.ts +3 -4
- package/dist/proxy/accountQuota.js +75 -44
- package/dist/proxy/accountSelection.d.ts +8 -0
- package/dist/proxy/accountSelection.js +25 -0
- package/dist/proxy/proxyConfig.js +24 -0
- package/dist/proxy/proxyPaths.js +2 -0
- package/dist/proxy/requestLogger.js +2 -0
- package/dist/proxy/snapshotPersistence.js +1 -1
- package/dist/proxy/sseInterceptor.js +16 -0
- package/dist/proxy/streamOutcome.d.ts +3 -0
- package/dist/proxy/streamOutcome.js +26 -0
- package/dist/proxy/tokenRefresh.d.ts +8 -0
- package/dist/proxy/tokenRefresh.js +92 -15
- package/dist/proxy/updateState.js +17 -4
- package/dist/server/routes/claudeProxyRoutes.d.ts +18 -3
- package/dist/server/routes/claudeProxyRoutes.js +624 -223
- package/dist/types/auth.d.ts +1 -1
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/multimodal.d.ts +12 -0
- package/dist/types/proxy.d.ts +49 -8
- package/dist/types/subscription.d.ts +5 -0
- package/package.json +1 -1
|
@@ -18,11 +18,13 @@ import { buildProxyHealthResponse, createProxyReadinessState, markProxyReady, wa
|
|
|
18
18
|
import { logger } from "../../lib/utils/logger.js";
|
|
19
19
|
import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
|
|
20
20
|
import { configureProxyKeepAliveDispatcher } from "../../lib/proxy/proxyDispatcher.js";
|
|
21
|
+
import { anthropicAccountKeysEqual, createAccountAllowlist, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../lib/proxy/accountSelection.js";
|
|
21
22
|
import { loadProxyEnvFile, resolveProxyEnvFile, } from "../../lib/proxy/proxyEnv.js";
|
|
22
23
|
import { createRequire } from "node:module";
|
|
23
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import packageJson from "../../../package.json" with { type: "json" };
|
|
24
26
|
const _require = createRequire(import.meta.url);
|
|
25
|
-
const
|
|
27
|
+
const PROXY_VERSION = packageJson.version;
|
|
26
28
|
const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/observability/manage-local-openobserve.sh", import.meta.url));
|
|
27
29
|
// =============================================================================
|
|
28
30
|
// STATE MANAGEMENT
|
|
@@ -73,6 +75,7 @@ function getProcessStatus(pid) {
|
|
|
73
75
|
* when the configured account isn't currently usable. */
|
|
74
76
|
async function resolveStatusPrimaryAccount(proxyConfig) {
|
|
75
77
|
const configured = proxyConfig?.routing?.primaryAccount?.trim() || null;
|
|
78
|
+
const accountAllowlist = createAccountAllowlist(proxyConfig?.routing?.accountAllowlist);
|
|
76
79
|
let enabledAnthropicKeys = [];
|
|
77
80
|
try {
|
|
78
81
|
const { tokenStore } = await import("../../lib/auth/tokenStore.js");
|
|
@@ -80,7 +83,7 @@ async function resolveStatusPrimaryAccount(proxyConfig) {
|
|
|
80
83
|
const filtered = [];
|
|
81
84
|
for (const key of all) {
|
|
82
85
|
const disabled = await tokenStore.isDisabled(key);
|
|
83
|
-
if (!disabled) {
|
|
86
|
+
if (!disabled && isAccountAllowed(key, accountAllowlist)) {
|
|
84
87
|
filtered.push(key);
|
|
85
88
|
}
|
|
86
89
|
}
|
|
@@ -90,11 +93,12 @@ async function resolveStatusPrimaryAccount(proxyConfig) {
|
|
|
90
93
|
logger.debug(`[proxy] /status: failed to enumerate anthropic accounts: ${err instanceof Error ? err.message : String(err)}`);
|
|
91
94
|
}
|
|
92
95
|
if (configured) {
|
|
93
|
-
const configuredKey =
|
|
94
|
-
|
|
96
|
+
const configuredKey = normalizeAnthropicAccountKey(configured);
|
|
97
|
+
const matchedKey = enabledAnthropicKeys.find((key) => anthropicAccountKeysEqual(key, configuredKey));
|
|
98
|
+
if (matchedKey) {
|
|
95
99
|
return {
|
|
96
100
|
configured,
|
|
97
|
-
key:
|
|
101
|
+
key: matchedKey,
|
|
98
102
|
label: configured,
|
|
99
103
|
source: "configured",
|
|
100
104
|
};
|
|
@@ -129,39 +133,11 @@ async function isLaunchdManaging() {
|
|
|
129
133
|
return false;
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (process.platform !== "darwin") {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
const { existsSync } = await import("fs");
|
|
142
|
-
if (!existsSync(PLIST_PATH)) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
try {
|
|
150
|
-
const { execFileSync } = await import("node:child_process");
|
|
151
|
-
const uid = process.getuid?.() ?? 501;
|
|
152
|
-
execFileSync("launchctl", ["kickstart", "-k", `gui/${uid}/${PLIST_LABEL}`], { stdio: "ignore", timeout: 5_000 });
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
const deadline = Date.now() + timeoutMs;
|
|
158
|
-
while (Date.now() < deadline) {
|
|
159
|
-
await sleep(1_000);
|
|
160
|
-
if (await isProxyHealthy(host, port, 2_000)) {
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return false;
|
|
136
|
+
function isLaunchdManagedProcess() {
|
|
137
|
+
return process.platform === "darwin" && process.ppid === 1;
|
|
138
|
+
}
|
|
139
|
+
function isProxyAutoUpdateEnabled(value = process.env.NEUROLINK_PROXY_AUTO_UPDATE) {
|
|
140
|
+
return ["1", "on", "true"].includes((value ?? "").trim().toLowerCase());
|
|
165
141
|
}
|
|
166
142
|
/** Keys we manage in Claude Code's settings.env */
|
|
167
143
|
const PROXY_MANAGED_KEYS = ["ANTHROPIC_BASE_URL", "ENABLE_TOOL_SEARCH"];
|
|
@@ -760,17 +736,14 @@ async function loadProxyStartConfiguration(argv, spinner) {
|
|
|
760
736
|
}
|
|
761
737
|
}
|
|
762
738
|
catch (configError) {
|
|
763
|
-
if (argv.config) {
|
|
764
|
-
if (spinner) {
|
|
765
|
-
spinner.fail(chalk.red(`Failed to load proxy config: ${configPath}`));
|
|
766
|
-
}
|
|
767
|
-
process.exit(1);
|
|
768
|
-
}
|
|
769
739
|
const isNotFound = configError instanceof Error &&
|
|
770
740
|
"code" in configError &&
|
|
771
741
|
configError.code === "ENOENT";
|
|
772
|
-
if (!isNotFound) {
|
|
773
|
-
|
|
742
|
+
if (argv.config || !isNotFound) {
|
|
743
|
+
if (spinner) {
|
|
744
|
+
spinner.fail(chalk.red(`Failed to load proxy config: ${configPath}`));
|
|
745
|
+
}
|
|
746
|
+
throw configError;
|
|
774
747
|
}
|
|
775
748
|
}
|
|
776
749
|
const strategy = (argv.strategy ??
|
|
@@ -787,6 +760,11 @@ async function loadProxyStartConfiguration(argv, spinner) {
|
|
|
787
760
|
});
|
|
788
761
|
}
|
|
789
762
|
const primaryAccountKey = await resolveBootPrimaryAccountKey(proxyConfig?.routing?.primaryAccount);
|
|
763
|
+
const accountAllowlist = await resolveBootAccountAllowlist(proxyConfig?.routing?.accountAllowlist);
|
|
764
|
+
if (primaryAccountKey &&
|
|
765
|
+
!isAccountAllowed(primaryAccountKey, accountAllowlist)) {
|
|
766
|
+
throw new Error(`Configured routing.primaryAccount=${proxyConfig?.routing?.primaryAccount} is excluded by routing.accountAllowlist`);
|
|
767
|
+
}
|
|
790
768
|
return {
|
|
791
769
|
configPath,
|
|
792
770
|
proxyConfig,
|
|
@@ -794,8 +772,34 @@ async function loadProxyStartConfiguration(argv, spinner) {
|
|
|
794
772
|
modelRouter,
|
|
795
773
|
passthrough: argv.passthrough ?? false,
|
|
796
774
|
primaryAccountKey,
|
|
775
|
+
accountAllowlist,
|
|
797
776
|
};
|
|
798
777
|
}
|
|
778
|
+
async function resolveBootAccountAllowlist(configuredAccounts) {
|
|
779
|
+
const allowlist = createAccountAllowlist(configuredAccounts);
|
|
780
|
+
if (allowlist === undefined) {
|
|
781
|
+
return undefined;
|
|
782
|
+
}
|
|
783
|
+
if (allowlist.size === 0) {
|
|
784
|
+
logger.warn("[proxy] routing.accountAllowlist is empty; all stored Anthropic credentials are denied");
|
|
785
|
+
return allowlist;
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
const { tokenStore } = await import("../../lib/auth/tokenStore.js");
|
|
789
|
+
const known = new Set((await tokenStore.listByPrefix("anthropic:")).map(normalizeAnthropicAccountKey));
|
|
790
|
+
known.add(LEGACY_ANTHROPIC_ACCOUNT_KEY);
|
|
791
|
+
known.add(ENV_ANTHROPIC_ACCOUNT_KEY);
|
|
792
|
+
for (const key of allowlist) {
|
|
793
|
+
if (!known.has(key)) {
|
|
794
|
+
logger.warn(`[proxy] WARN: routing.accountAllowlist entry ${key} is allowed but not currently authenticated`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
catch (err) {
|
|
799
|
+
logger.debug(`[proxy] could not validate account allowlist against token store: ${err instanceof Error ? err.message : String(err)}`);
|
|
800
|
+
}
|
|
801
|
+
return allowlist;
|
|
802
|
+
}
|
|
799
803
|
/** Resolve the operator's configured primary email to a stable token-store
|
|
800
804
|
* key (anthropic:<email>). Cross-checks the token store and emits a one-time
|
|
801
805
|
* startup warning if the configured account isn't authenticated — but still
|
|
@@ -806,11 +810,11 @@ async function resolveBootPrimaryAccountKey(primaryEmail) {
|
|
|
806
810
|
if (!trimmed) {
|
|
807
811
|
return undefined;
|
|
808
812
|
}
|
|
809
|
-
const key =
|
|
813
|
+
const key = normalizeAnthropicAccountKey(trimmed);
|
|
810
814
|
try {
|
|
811
815
|
const { tokenStore } = await import("../../lib/auth/tokenStore.js");
|
|
812
816
|
const known = await tokenStore.listByPrefix("anthropic:");
|
|
813
|
-
if (!known.
|
|
817
|
+
if (!known.some((knownKey) => anthropicAccountKeysEqual(knownKey, key))) {
|
|
814
818
|
logger.warn(`[proxy] WARN: configured routing.primaryAccount=${trimmed} not ` +
|
|
815
819
|
`found in token store; falling back to first enabled account. ` +
|
|
816
820
|
`Run \`neurolink auth login --add\` to authenticate it, or ` +
|
|
@@ -842,7 +846,7 @@ async function createProxyStartApp(params) {
|
|
|
842
846
|
},
|
|
843
847
|
}, 502);
|
|
844
848
|
});
|
|
845
|
-
const routeGroup = createClaudeProxyRoutes(params.modelRouter, "", params.strategy, params.passthrough, params.primaryAccountKey);
|
|
849
|
+
const routeGroup = createClaudeProxyRoutes(params.modelRouter, "", params.strategy, params.passthrough, params.primaryAccountKey, params.accountAllowlist);
|
|
846
850
|
const openaiRouteGroup = createOpenAIProxyRoutes(params.modelRouter, "", params.port);
|
|
847
851
|
const allProxyRoutes = [...routeGroup.routes, ...openaiRouteGroup.routes];
|
|
848
852
|
for (const route of allProxyRoutes) {
|
|
@@ -967,7 +971,20 @@ async function createProxyStartApp(params) {
|
|
|
967
971
|
})));
|
|
968
972
|
app.get("/status", async (c) => {
|
|
969
973
|
const { getStats } = await import("../../lib/proxy/usageStats.js");
|
|
974
|
+
const { loadAccountCooldowns } = await import("../../lib/proxy/accountCooldown.js");
|
|
970
975
|
const stats = getStats();
|
|
976
|
+
const cooldowns = await loadAccountCooldowns();
|
|
977
|
+
const storedAccountKeys = new Set();
|
|
978
|
+
try {
|
|
979
|
+
const { tokenStore } = await import("../../lib/auth/tokenStore.js");
|
|
980
|
+
for (const key of await tokenStore.listByPrefix("anthropic:")) {
|
|
981
|
+
storedAccountKeys.add(normalizeAnthropicAccountKey(key));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
catch (err) {
|
|
985
|
+
logger.debug(`[proxy] /status: failed to resolve account cooldown labels: ${err instanceof Error ? err.message : String(err)}`);
|
|
986
|
+
}
|
|
987
|
+
const now = Date.now();
|
|
971
988
|
const health = buildProxyHealthResponse(readiness, {
|
|
972
989
|
strategy: params.strategy,
|
|
973
990
|
passthrough: params.passthrough,
|
|
@@ -992,20 +1009,35 @@ async function createProxyStartApp(params) {
|
|
|
992
1009
|
totalSuccess: stats.totalSuccess,
|
|
993
1010
|
totalErrors: stats.totalErrors,
|
|
994
1011
|
totalRateLimits: stats.totalRateLimits,
|
|
995
|
-
accounts: Object.values(stats.accounts).map((account) =>
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1012
|
+
accounts: Object.values(stats.accounts).map((account) => {
|
|
1013
|
+
const normalizedKey = normalizeAnthropicAccountKey(account.label);
|
|
1014
|
+
const accountKey = storedAccountKeys.has(normalizedKey)
|
|
1015
|
+
? normalizedKey
|
|
1016
|
+
: account.label === "env"
|
|
1017
|
+
? ENV_ANTHROPIC_ACCOUNT_KEY
|
|
1018
|
+
: account.type === "oauth"
|
|
1019
|
+
? LEGACY_ANTHROPIC_ACCOUNT_KEY
|
|
1020
|
+
: normalizedKey;
|
|
1021
|
+
return {
|
|
1022
|
+
label: account.label,
|
|
1023
|
+
type: account.type,
|
|
1024
|
+
attempts: account.attemptCount,
|
|
1025
|
+
requests: account.attemptCount,
|
|
1026
|
+
success: account.successCount,
|
|
1027
|
+
errors: account.errorCount,
|
|
1028
|
+
rateLimits: account.rateLimitCount,
|
|
1029
|
+
cooling: (cooldowns[accountKey]?.coolingUntil ?? 0) > now,
|
|
1030
|
+
};
|
|
1031
|
+
}),
|
|
1005
1032
|
primaryAccount,
|
|
1006
1033
|
},
|
|
1007
1034
|
config: params.proxyConfig
|
|
1008
|
-
? {
|
|
1035
|
+
? {
|
|
1036
|
+
hasRouting: !!params.proxyConfig.routing,
|
|
1037
|
+
accountAllowlist: params.accountAllowlist
|
|
1038
|
+
? [...params.accountAllowlist]
|
|
1039
|
+
: null,
|
|
1040
|
+
}
|
|
1009
1041
|
: null,
|
|
1010
1042
|
});
|
|
1011
1043
|
});
|
|
@@ -1057,14 +1089,45 @@ async function initializeProxyOpenTelemetry() {
|
|
|
1057
1089
|
logger.debug(`[proxy] OpenTelemetry init failed (non-fatal): ${error instanceof Error ? error.message : String(error)}`);
|
|
1058
1090
|
}
|
|
1059
1091
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1092
|
+
const BACKGROUND_REFRESH_BASE_COOLDOWN_MS = 30_000;
|
|
1093
|
+
const BACKGROUND_REFRESH_MAX_COOLDOWN_MS = 5 * 60 * 1000;
|
|
1094
|
+
const backgroundRefreshFailures = new Map();
|
|
1095
|
+
const backgroundRejectedRefreshTokens = new Map();
|
|
1096
|
+
let backgroundRefreshInProgress = false;
|
|
1097
|
+
function canAttemptBackgroundRefresh(key, refreshToken) {
|
|
1098
|
+
const rejectedToken = backgroundRejectedRefreshTokens.get(key);
|
|
1099
|
+
if (rejectedToken !== undefined) {
|
|
1100
|
+
if (rejectedToken === refreshToken) {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
backgroundRejectedRefreshTokens.delete(key);
|
|
1104
|
+
}
|
|
1105
|
+
const state = backgroundRefreshFailures.get(key);
|
|
1106
|
+
return !state || Date.now() >= state.coolingUntil;
|
|
1107
|
+
}
|
|
1108
|
+
function recordBackgroundRefreshFailure(key) {
|
|
1109
|
+
const consecutiveFailures = (backgroundRefreshFailures.get(key)?.consecutiveFailures ?? 0) + 1;
|
|
1110
|
+
const delayMs = Math.min(BACKGROUND_REFRESH_MAX_COOLDOWN_MS, BACKGROUND_REFRESH_BASE_COOLDOWN_MS *
|
|
1111
|
+
2 ** Math.min(consecutiveFailures - 1, 4));
|
|
1112
|
+
backgroundRefreshFailures.set(key, {
|
|
1113
|
+
consecutiveFailures,
|
|
1114
|
+
coolingUntil: Date.now() + delayMs,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
async function refreshProxyTokensInBackground(accountAllowlist) {
|
|
1118
|
+
const { needsRefresh, refreshToken, persistTokens, isPermanentRefreshFailure, } = await import("../../lib/proxy/tokenRefresh.js");
|
|
1062
1119
|
const { tokenStore } = await import("../../lib/auth/tokenStore.js");
|
|
1120
|
+
let storedAnthropicAccountCount;
|
|
1063
1121
|
try {
|
|
1064
1122
|
const allKeys = await tokenStore.listProviders();
|
|
1065
1123
|
const anthropicKeys = allKeys.filter((key) => key.startsWith("anthropic:"));
|
|
1124
|
+
storedAnthropicAccountCount = anthropicKeys.length;
|
|
1066
1125
|
for (const key of anthropicKeys) {
|
|
1067
1126
|
try {
|
|
1127
|
+
if (!isAccountAllowed(key, accountAllowlist) ||
|
|
1128
|
+
(await tokenStore.isDisabled(key))) {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1068
1131
|
const tokens = await tokenStore.loadTokens(key);
|
|
1069
1132
|
if (!tokens) {
|
|
1070
1133
|
continue;
|
|
@@ -1075,12 +1138,29 @@ async function refreshProxyTokensInBackground() {
|
|
|
1075
1138
|
refreshToken: tokens.refreshToken,
|
|
1076
1139
|
expiresAt: tokens.expiresAt,
|
|
1077
1140
|
};
|
|
1078
|
-
if (needsRefresh(account)) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1141
|
+
if (!needsRefresh(account)) {
|
|
1142
|
+
backgroundRefreshFailures.delete(key);
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
if (!canAttemptBackgroundRefresh(key, tokens.refreshToken)) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
const result = await refreshToken(account);
|
|
1149
|
+
if (result.success) {
|
|
1150
|
+
await persistTokens({ providerKey: key }, account);
|
|
1151
|
+
backgroundRefreshFailures.delete(key);
|
|
1152
|
+
logger.debug(`[proxy] background token refresh succeeded for ${key}`);
|
|
1153
|
+
}
|
|
1154
|
+
else if (isPermanentRefreshFailure(result)) {
|
|
1155
|
+
await tokenStore.markDisabled(key, "refresh_invalid");
|
|
1156
|
+
backgroundRefreshFailures.delete(key);
|
|
1157
|
+
logger.warn(`[proxy] background refresh credential rejected for ${key}; disabled until explicit login`);
|
|
1158
|
+
}
|
|
1159
|
+
else {
|
|
1160
|
+
recordBackgroundRefreshFailure(key);
|
|
1161
|
+
logger.debug(`[proxy] background token refresh deferred for ${key}`, {
|
|
1162
|
+
status: result.status,
|
|
1163
|
+
});
|
|
1084
1164
|
}
|
|
1085
1165
|
}
|
|
1086
1166
|
catch {
|
|
@@ -1092,6 +1172,10 @@ async function refreshProxyTokensInBackground() {
|
|
|
1092
1172
|
// non-fatal
|
|
1093
1173
|
}
|
|
1094
1174
|
try {
|
|
1175
|
+
if (storedAnthropicAccountCount === undefined ||
|
|
1176
|
+
!shouldLoadFallbackCredential(storedAnthropicAccountCount, LEGACY_ANTHROPIC_ACCOUNT_KEY, accountAllowlist)) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1095
1179
|
const credPath = join(homedir(), ".neurolink", "anthropic-credentials.json");
|
|
1096
1180
|
const { readFileSync } = await import("fs");
|
|
1097
1181
|
const creds = JSON.parse(readFileSync(credPath, "utf8"));
|
|
@@ -1104,32 +1188,108 @@ async function refreshProxyTokensInBackground() {
|
|
|
1104
1188
|
refreshToken: creds.oauth.refreshToken,
|
|
1105
1189
|
expiresAt: creds.oauth.expiresAt,
|
|
1106
1190
|
};
|
|
1107
|
-
|
|
1191
|
+
const legacyKey = LEGACY_ANTHROPIC_ACCOUNT_KEY;
|
|
1192
|
+
if (!needsRefresh(account)) {
|
|
1193
|
+
backgroundRefreshFailures.delete(legacyKey);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (canAttemptBackgroundRefresh(legacyKey, account.refreshToken)) {
|
|
1108
1197
|
const result = await refreshToken(account);
|
|
1109
1198
|
if (result.success) {
|
|
1110
1199
|
await persistTokens(credPath, account);
|
|
1200
|
+
backgroundRefreshFailures.delete(legacyKey);
|
|
1201
|
+
backgroundRejectedRefreshTokens.delete(legacyKey);
|
|
1111
1202
|
logger.debug("[proxy] background token refresh succeeded");
|
|
1112
1203
|
}
|
|
1204
|
+
else if (isPermanentRefreshFailure(result)) {
|
|
1205
|
+
backgroundRefreshFailures.delete(legacyKey);
|
|
1206
|
+
backgroundRejectedRefreshTokens.set(legacyKey, account.refreshToken ?? "");
|
|
1207
|
+
logger.warn("[proxy] background legacy refresh credential rejected; waiting for explicit login");
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
recordBackgroundRefreshFailure(legacyKey);
|
|
1211
|
+
logger.debug("[proxy] background legacy token refresh deferred", {
|
|
1212
|
+
status: result.status,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1113
1215
|
}
|
|
1114
1216
|
}
|
|
1115
1217
|
catch {
|
|
1116
1218
|
// non-fatal
|
|
1117
1219
|
}
|
|
1118
1220
|
}
|
|
1119
|
-
function startProxyBackgroundMaintenance(cleanupLogs) {
|
|
1221
|
+
function startProxyBackgroundMaintenance(cleanupLogs, accountAllowlist) {
|
|
1120
1222
|
const refreshInterval = setInterval(() => {
|
|
1121
|
-
|
|
1223
|
+
if (backgroundRefreshInProgress) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
backgroundRefreshInProgress = true;
|
|
1227
|
+
void refreshProxyTokensInBackground(accountAllowlist)
|
|
1228
|
+
.catch((error) => {
|
|
1229
|
+
logger.debug(`[proxy] background token refresh cycle failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1230
|
+
})
|
|
1231
|
+
.finally(() => {
|
|
1232
|
+
backgroundRefreshInProgress = false;
|
|
1233
|
+
});
|
|
1122
1234
|
}, 30_000);
|
|
1123
1235
|
const logCleanupInterval = setInterval(() => {
|
|
1124
|
-
|
|
1236
|
+
try {
|
|
1237
|
+
cleanupLogs(7, 500);
|
|
1238
|
+
}
|
|
1239
|
+
catch (error) {
|
|
1240
|
+
logger.debug(`[proxy] background log cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1241
|
+
}
|
|
1125
1242
|
}, 60 * 60 * 1000);
|
|
1126
1243
|
return { refreshInterval, logCleanupInterval };
|
|
1127
1244
|
}
|
|
1128
1245
|
function registerProxyShutdownHandlers(params) {
|
|
1246
|
+
let shutdownStarted = false;
|
|
1247
|
+
const closeServer = async () => {
|
|
1248
|
+
const close = params.server.close?.bind(params.server);
|
|
1249
|
+
if (!close) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
await new Promise((resolve, reject) => {
|
|
1253
|
+
let settled = false;
|
|
1254
|
+
const finish = (error) => {
|
|
1255
|
+
if (settled) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
settled = true;
|
|
1259
|
+
clearTimeout(timeout);
|
|
1260
|
+
if (error) {
|
|
1261
|
+
reject(error);
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
resolve();
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
const timeout = setTimeout(() => finish(new Error("Timed out draining the proxy server")), 30_000);
|
|
1268
|
+
timeout.unref?.();
|
|
1269
|
+
try {
|
|
1270
|
+
close((error) => finish(error));
|
|
1271
|
+
}
|
|
1272
|
+
catch (error) {
|
|
1273
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
};
|
|
1129
1277
|
const shutdown = async (signal) => {
|
|
1278
|
+
if (shutdownStarted) {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
shutdownStarted = true;
|
|
1130
1282
|
clearInterval(params.refreshInterval);
|
|
1131
1283
|
clearInterval(params.logCleanupInterval);
|
|
1132
1284
|
logger.always(`\nShutting down proxy (${signal})...`);
|
|
1285
|
+
let exitCode = signal === "SIGINT" ? 0 : 1;
|
|
1286
|
+
try {
|
|
1287
|
+
await closeServer();
|
|
1288
|
+
}
|
|
1289
|
+
catch (error) {
|
|
1290
|
+
exitCode = 1;
|
|
1291
|
+
logger.error(`[proxy] failed to drain server during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
1292
|
+
}
|
|
1133
1293
|
try {
|
|
1134
1294
|
const { flushOpenTelemetry, shutdownOpenTelemetry } = await import("../../lib/services/server/ai/observability/instrumentation.js");
|
|
1135
1295
|
await flushOpenTelemetry();
|
|
@@ -1155,19 +1315,23 @@ function registerProxyShutdownHandlers(params) {
|
|
|
1155
1315
|
}
|
|
1156
1316
|
}
|
|
1157
1317
|
try {
|
|
1158
|
-
|
|
1318
|
+
clearProxyState();
|
|
1159
1319
|
}
|
|
1160
|
-
catch {
|
|
1161
|
-
|
|
1320
|
+
catch (error) {
|
|
1321
|
+
exitCode = 1;
|
|
1322
|
+
logger.error(`[proxy] failed to clear runtime state during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
1162
1323
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1324
|
+
process.exit(exitCode);
|
|
1325
|
+
};
|
|
1326
|
+
const forceExitAfterShutdownFailure = (error) => {
|
|
1327
|
+
logger.error(`[proxy] unexpected shutdown failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
1328
|
+
process.exit(1);
|
|
1165
1329
|
};
|
|
1166
1330
|
process.on("SIGTERM", () => {
|
|
1167
|
-
void shutdown("SIGTERM");
|
|
1331
|
+
void shutdown("SIGTERM").catch(forceExitAfterShutdownFailure);
|
|
1168
1332
|
});
|
|
1169
1333
|
process.on("SIGINT", () => {
|
|
1170
|
-
void shutdown("SIGINT");
|
|
1334
|
+
void shutdown("SIGINT").catch(forceExitAfterShutdownFailure);
|
|
1171
1335
|
});
|
|
1172
1336
|
}
|
|
1173
1337
|
async function startProxyRuntime(params) {
|
|
@@ -1177,9 +1341,11 @@ async function startProxyRuntime(params) {
|
|
|
1177
1341
|
port: params.port,
|
|
1178
1342
|
hostname: params.host,
|
|
1179
1343
|
});
|
|
1180
|
-
|
|
1181
|
-
//
|
|
1182
|
-
|
|
1344
|
+
const managedByLaunchd = isLaunchdManagedProcess();
|
|
1345
|
+
// launchd already owns restart supervision. A second detached supervisor can
|
|
1346
|
+
// outlive its parent and terminate a healthy replacement, so the guard is
|
|
1347
|
+
// reserved for foreground mode where it only cleans stale client settings.
|
|
1348
|
+
const guardPid = params.argv.dev || managedByLaunchd
|
|
1183
1349
|
? undefined
|
|
1184
1350
|
: spawnFailOpenGuard(params.host, params.port, process.pid);
|
|
1185
1351
|
const readinessHost = params.host === "0.0.0.0" ? "127.0.0.1" : params.host;
|
|
@@ -1206,10 +1372,11 @@ async function startProxyRuntime(params) {
|
|
|
1206
1372
|
statusPath: "/status",
|
|
1207
1373
|
envFile: params.loadedEnvFile,
|
|
1208
1374
|
fallbackChain,
|
|
1375
|
+
accountAllowlist: params.accountAllowlist
|
|
1376
|
+
? [...params.accountAllowlist]
|
|
1377
|
+
: undefined,
|
|
1209
1378
|
guardPid,
|
|
1210
|
-
managedBy:
|
|
1211
|
-
? "launchd"
|
|
1212
|
-
: "manual",
|
|
1379
|
+
managedBy: managedByLaunchd ? "launchd" : "manual",
|
|
1213
1380
|
passthrough: params.passthrough,
|
|
1214
1381
|
});
|
|
1215
1382
|
if (params.spinner) {
|
|
@@ -1255,7 +1422,7 @@ async function startProxyRuntime(params) {
|
|
|
1255
1422
|
else {
|
|
1256
1423
|
logger.always(chalk.dim(" ⊘ Dev mode: skipping client auto-configuration"));
|
|
1257
1424
|
}
|
|
1258
|
-
const maintenance = startProxyBackgroundMaintenance(params.cleanupLogs);
|
|
1425
|
+
const maintenance = startProxyBackgroundMaintenance(params.cleanupLogs, params.accountAllowlist);
|
|
1259
1426
|
registerProxyShutdownHandlers({
|
|
1260
1427
|
server,
|
|
1261
1428
|
host: params.host,
|
|
@@ -1276,6 +1443,8 @@ async function startProxyCommandHandler(argv) {
|
|
|
1276
1443
|
setProxyStateDir(devPaths.stateDir);
|
|
1277
1444
|
const { initAccountQuota } = await import("../../lib/proxy/accountQuota.js");
|
|
1278
1445
|
initAccountQuota(devPaths.quotaFile);
|
|
1446
|
+
const { initAccountCooldown } = await import("../../lib/proxy/accountCooldown.js");
|
|
1447
|
+
initAccountCooldown(devPaths.cooldownFile);
|
|
1279
1448
|
// Ensure the dev state directory exists
|
|
1280
1449
|
const { mkdirSync, existsSync } = await import("fs");
|
|
1281
1450
|
if (!existsSync(devPaths.stateDir)) {
|
|
@@ -1291,7 +1460,7 @@ async function startProxyCommandHandler(argv) {
|
|
|
1291
1460
|
// content-filters. Runs once, after env load so it can be tuned via env.
|
|
1292
1461
|
configureProxyKeepAliveDispatcher();
|
|
1293
1462
|
const { neurolink, cleanupLogs } = await createProxyNeurolinkRuntime(devPaths?.logsDir);
|
|
1294
|
-
const { proxyConfig, strategy, modelRouter, passthrough, primaryAccountKey, } = await loadProxyStartConfiguration(argv, spinner);
|
|
1463
|
+
const { proxyConfig, strategy, modelRouter, passthrough, primaryAccountKey, accountAllowlist, } = await loadProxyStartConfiguration(argv, spinner);
|
|
1295
1464
|
if (spinner) {
|
|
1296
1465
|
spinner.text = "Configuring server...";
|
|
1297
1466
|
}
|
|
@@ -1306,6 +1475,7 @@ async function startProxyCommandHandler(argv) {
|
|
|
1306
1475
|
host,
|
|
1307
1476
|
proxyConfig,
|
|
1308
1477
|
primaryAccountKey,
|
|
1478
|
+
accountAllowlist,
|
|
1309
1479
|
});
|
|
1310
1480
|
await initializeProxyOpenTelemetry();
|
|
1311
1481
|
if (spinner) {
|
|
@@ -1320,6 +1490,7 @@ async function startProxyCommandHandler(argv) {
|
|
|
1320
1490
|
port,
|
|
1321
1491
|
strategy,
|
|
1322
1492
|
proxyConfig,
|
|
1493
|
+
accountAllowlist,
|
|
1323
1494
|
loadedEnvFile,
|
|
1324
1495
|
passthrough,
|
|
1325
1496
|
cleanupLogs,
|
|
@@ -1471,6 +1642,7 @@ export const proxyStatusCommand = {
|
|
|
1471
1642
|
url: null,
|
|
1472
1643
|
envFile: null,
|
|
1473
1644
|
fallbackChain: null,
|
|
1645
|
+
accountAllowlist: null,
|
|
1474
1646
|
};
|
|
1475
1647
|
if (state && isProcessRunning(state.pid)) {
|
|
1476
1648
|
status.running = true;
|
|
@@ -1484,6 +1656,7 @@ export const proxyStatusCommand = {
|
|
|
1484
1656
|
status.url = `http://${state.host === "0.0.0.0" ? "localhost" : state.host}:${state.port}`;
|
|
1485
1657
|
status.envFile = state.envFile ?? null;
|
|
1486
1658
|
status.fallbackChain = state.fallbackChain ?? null;
|
|
1659
|
+
status.accountAllowlist = state.accountAllowlist ?? null;
|
|
1487
1660
|
}
|
|
1488
1661
|
// Fetch live stats before rendering (JSON or text)
|
|
1489
1662
|
let liveStats = null;
|
|
@@ -1519,6 +1692,12 @@ export const proxyStatusCommand = {
|
|
|
1519
1692
|
if (status.envFile) {
|
|
1520
1693
|
logger.always(` ${chalk.bold("Env File:")} ${chalk.cyan(status.envFile)}`);
|
|
1521
1694
|
}
|
|
1695
|
+
if (status.accountAllowlist) {
|
|
1696
|
+
const scope = status.accountAllowlist.length > 0
|
|
1697
|
+
? status.accountAllowlist.join(", ")
|
|
1698
|
+
: "none (deny all)";
|
|
1699
|
+
logger.always(` ${chalk.bold("Accounts:")} ${chalk.cyan(scope)}`);
|
|
1700
|
+
}
|
|
1522
1701
|
// Display fallback chain if configured
|
|
1523
1702
|
if (status.fallbackChain && status.fallbackChain.length > 0) {
|
|
1524
1703
|
logger.always("");
|
|
@@ -1676,10 +1855,8 @@ export const proxyGuardCommand = {
|
|
|
1676
1855
|
if (!Number.isFinite(parentPid) || parentPid <= 0) {
|
|
1677
1856
|
return;
|
|
1678
1857
|
}
|
|
1679
|
-
//
|
|
1680
|
-
//
|
|
1681
|
-
// Always on — no flags needed. Hardcoded sensible defaults.
|
|
1682
|
-
// ---------------------------------------------------------------
|
|
1858
|
+
// Automatic package mutation is disabled by default. Operators must opt in
|
|
1859
|
+
// explicitly, and launchd-managed proxy processes do not spawn this guard.
|
|
1683
1860
|
const UPDATE_CHECK_INTERVAL_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
1684
1861
|
const QUIET_THRESHOLD_MS = 120 * 1000; // 2 minutes of silence
|
|
1685
1862
|
const UPDATE_TIMEOUT_MS = 30 * 1000; // 30 seconds to come healthy
|
|
@@ -1697,11 +1874,27 @@ export const proxyGuardCommand = {
|
|
|
1697
1874
|
}
|
|
1698
1875
|
// Auto-update only works on macOS with launchd. On other platforms,
|
|
1699
1876
|
// there's no restart mechanism, so skip the update loop entirely.
|
|
1700
|
-
const canAutoUpdate =
|
|
1877
|
+
const canAutoUpdate = isProxyAutoUpdateEnabled() &&
|
|
1878
|
+
process.platform === "darwin" &&
|
|
1879
|
+
(await isLaunchdManaging());
|
|
1880
|
+
let guardStopping = false;
|
|
1881
|
+
let updateCheckTimeout;
|
|
1882
|
+
let updateCheckInterval;
|
|
1883
|
+
const stopUpdateChecks = () => {
|
|
1884
|
+
guardStopping = true;
|
|
1885
|
+
if (updateCheckTimeout) {
|
|
1886
|
+
clearTimeout(updateCheckTimeout);
|
|
1887
|
+
updateCheckTimeout = undefined;
|
|
1888
|
+
}
|
|
1889
|
+
if (updateCheckInterval) {
|
|
1890
|
+
clearInterval(updateCheckInterval);
|
|
1891
|
+
updateCheckInterval = undefined;
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1701
1894
|
let updateInProgress = false;
|
|
1702
1895
|
let updateRestartInProgress = false;
|
|
1703
1896
|
const runUpdateCheck = async () => {
|
|
1704
|
-
if (updateInProgress) {
|
|
1897
|
+
if (guardStopping || updateInProgress) {
|
|
1705
1898
|
return;
|
|
1706
1899
|
}
|
|
1707
1900
|
updateInProgress = true;
|
|
@@ -1769,6 +1962,9 @@ export const proxyGuardCommand = {
|
|
|
1769
1962
|
return;
|
|
1770
1963
|
}
|
|
1771
1964
|
logger.always(`[guard] traffic quiet, installing @juspay/neurolink@${result.latestVersion} via ${pnpmResolution.bin} (pnpm v${pnpmResolution.version})...`);
|
|
1965
|
+
if (guardStopping || getProcessStatus(parentPid) === "not_running") {
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1772
1968
|
const { execFileSync } = await import("node:child_process");
|
|
1773
1969
|
try {
|
|
1774
1970
|
execFileSync(pnpmResolution.bin, ["add", "-g", `@juspay/neurolink@${result.latestVersion}`], {
|
|
@@ -1831,6 +2027,9 @@ export const proxyGuardCommand = {
|
|
|
1831
2027
|
logger.always(`[guard] WARNING: failed to rewrite plist (restart may use stale path): ${plistErr instanceof Error ? plistErr.message : String(plistErr)}`);
|
|
1832
2028
|
// Continue with restart anyway — the stable bin symlink may still be correct
|
|
1833
2029
|
}
|
|
2030
|
+
if (guardStopping || getProcessStatus(parentPid) === "not_running") {
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
1834
2033
|
// Signal the health loop to not exit when it detects
|
|
1835
2034
|
// the parent PID is gone — we're intentionally restarting.
|
|
1836
2035
|
updateRestartInProgress = true;
|
|
@@ -1838,7 +2037,7 @@ export const proxyGuardCommand = {
|
|
|
1838
2037
|
const uid = process.getuid?.() ?? 501;
|
|
1839
2038
|
try {
|
|
1840
2039
|
// bootout unloads the in-memory job definition. This is required
|
|
1841
|
-
// because
|
|
2040
|
+
// because a forced kickstart reuses the cached plist and ignores any
|
|
1842
2041
|
// on-disk changes (like the trampoline rewrite above).
|
|
1843
2042
|
try {
|
|
1844
2043
|
execFileSync("launchctl", ["bootout", `gui/${uid}/${PLIST_LABEL}`], { timeout: 10_000, stdio: "pipe" });
|
|
@@ -1904,8 +2103,8 @@ export const proxyGuardCommand = {
|
|
|
1904
2103
|
};
|
|
1905
2104
|
// Run first check after a short delay, then on interval
|
|
1906
2105
|
if (canAutoUpdate) {
|
|
1907
|
-
setTimeout(runUpdateCheck, 30_000);
|
|
1908
|
-
setInterval(runUpdateCheck, UPDATE_CHECK_INTERVAL_MS);
|
|
2106
|
+
updateCheckTimeout = setTimeout(runUpdateCheck, 30_000);
|
|
2107
|
+
updateCheckInterval = setInterval(runUpdateCheck, UPDATE_CHECK_INTERVAL_MS);
|
|
1909
2108
|
}
|
|
1910
2109
|
const startedAt = Date.now();
|
|
1911
2110
|
let parentStatus = getProcessStatus(parentPid);
|
|
@@ -1923,31 +2122,32 @@ export const proxyGuardCommand = {
|
|
|
1923
2122
|
// Parent is gone (and we're not mid-update-restart).
|
|
1924
2123
|
// If endpoint is still healthy, another proxy took over.
|
|
1925
2124
|
if (healthy) {
|
|
2125
|
+
stopUpdateChecks();
|
|
1926
2126
|
return;
|
|
1927
2127
|
}
|
|
1928
2128
|
break;
|
|
1929
2129
|
}
|
|
1930
2130
|
if (!healthy && consecutiveUnhealthy >= failureThreshold) {
|
|
1931
|
-
//
|
|
1932
|
-
|
|
2131
|
+
// A detached guard cannot safely decide that a live process should be
|
|
2132
|
+
// replaced. Leave recovery to the foreground operator or launchd.
|
|
2133
|
+
if (!argv.quiet) {
|
|
2134
|
+
logger.always(`[proxy] fail-open guard observed an unhealthy live parent; leaving process supervision unchanged`);
|
|
2135
|
+
}
|
|
2136
|
+
stopUpdateChecks();
|
|
2137
|
+
return;
|
|
1933
2138
|
}
|
|
1934
2139
|
if (maxWaitMs > 0 && Date.now() - startedAt >= maxWaitMs) {
|
|
2140
|
+
stopUpdateChecks();
|
|
1935
2141
|
return;
|
|
1936
2142
|
}
|
|
1937
2143
|
await sleep(pollIntervalMs);
|
|
1938
2144
|
parentStatus = getProcessStatus(parentPid);
|
|
1939
2145
|
}
|
|
2146
|
+
stopUpdateChecks();
|
|
1940
2147
|
const guardHost = host === "0.0.0.0" ? "localhost" : host;
|
|
1941
2148
|
const expectedBaseUrl = `http://${guardHost}:${port}`;
|
|
1942
|
-
//
|
|
1943
|
-
|
|
1944
|
-
if (restarted) {
|
|
1945
|
-
if (!argv.quiet) {
|
|
1946
|
-
logger.always(`[proxy] fail-open guard restarted proxy via launchd`);
|
|
1947
|
-
}
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
// Restart failed or launchd not installed — clean up Claude settings
|
|
2149
|
+
// The parent is confirmed gone and no replacement is healthy. Foreground
|
|
2150
|
+
// guards are cleanup-only; they never restart or signal proxy processes.
|
|
1951
2151
|
const cleared = await clearClaudeProxySettings(expectedBaseUrl);
|
|
1952
2152
|
try {
|
|
1953
2153
|
await clearOpenCodeProxySettings(`${expectedBaseUrl}/v1`);
|
|
@@ -2215,6 +2415,9 @@ ${configArgs}
|
|
|
2215
2415
|
<key>ThrottleInterval</key>
|
|
2216
2416
|
<integer>5</integer>
|
|
2217
2417
|
|
|
2418
|
+
<key>ExitTimeOut</key>
|
|
2419
|
+
<integer>45</integer>
|
|
2420
|
+
|
|
2218
2421
|
<key>StandardOutPath</key>
|
|
2219
2422
|
<string>${join(homedir(), ".neurolink", "logs", "proxy-launchd-stdout.log")}</string>
|
|
2220
2423
|
|